convert rst to markdown

This commit is contained in:
2025-02-27 18:56:07 +07:00
parent ef68fdc2d9
commit 637a4dd9fd
5527 changed files with 189985 additions and 45 deletions
+48
View File
@@ -0,0 +1,48 @@
---
orphan: 'true'
---
# Glossary
:::{glossary}
external id
external identifier
external identifiers
> string identifier stored in `ir.model.data`, can be used to refer
> to a record regardless of its database identifier during data imports
> or export/import roundtrips.
>
> External identifiers are in the form {samp}`{module}.{id}` (e.g.
> `account.invoice_graph`). From within a module, the
> {samp}`{module}.` prefix can be left out.
>
> Sometimes referred to as "xml id" or `xml_id` as XML-based
> {ref}`reference/data` make extensive use of them.
format string
: inspired by [jinja variables], format strings allow more easily
mixing literal content and computed content (expressions): content
between `{{` and `}}` is interpreted as an expression and
evaluated, other content is interpreted as literal strings and
displayed as-is
GIS
Geographic Information System
> any computer system or subsystem to capture, store, manipulate,
> analyze, manage or present spatial and geographical data.
minified
minification
> process of removing extraneous/non-necessary sections of files
> (comments, whitespace) and possibly recompiling them using equivalent
> but shorter structures ([ternary operator] instead of `if/else`) in
> order to reduce network traffic
:::
[jinja variables]: http://jinja.pocoo.org/docs/dev/templates/#variables
[ternary operator]: http://en.wikipedia.org/wiki/%3F:
-43
View File
@@ -1,43 +0,0 @@
:orphan: true
========
Glossary
========
.. glossary::
external id
external identifier
external identifiers
string identifier stored in ``ir.model.data``, can be used to refer
to a record regardless of its database identifier during data imports
or export/import roundtrips.
External identifiers are in the form :samp:`{module}.{id}` (e.g.
``account.invoice_graph``). From within a module, the
:samp:`{module}.` prefix can be left out.
Sometimes referred to as "xml id" or ``xml_id`` as XML-based
:ref:`reference/data` make extensive use of them.
format string
inspired by `jinja variables`_, format strings allow more easily
mixing literal content and computed content (expressions): content
between ``{{`` and ``}}`` is interpreted as an expression and
evaluated, other content is interpreted as literal strings and
displayed as-is
GIS
Geographic Information System
any computer system or subsystem to capture, store, manipulate,
analyze, manage or present spatial and geographical data.
minified
minification
process of removing extraneous/non-necessary sections of files
(comments, whitespace) and possibly recompiling them using equivalent
but shorter structures (`ternary operator`_ instead of ``if/else``) in
order to reduce network traffic
.. _jinja variables: http://jinja.pocoo.org/docs/dev/templates/#variables
.. _ternary operator: http://en.wikipedia.org/wiki/%3F:
@@ -1,31 +1,32 @@
:show-content:
:hide-page-toc:
---
hide-page-toc: true
show-content: true
---
=============
How-to guides
=============
# How-to guides
.. toctree::
howtos/scss_tips
howtos/javascript_field
howtos/javascript_view
howtos/javascript_client_action
howtos/standalone_owl_application
howtos/frontend_owl_components
howtos/website_themes
```{toctree}
howtos/scss_tips
howtos/javascript_field
howtos/javascript_view
howtos/javascript_client_action
howtos/standalone_owl_application
howtos/frontend_owl_components
howtos/website_themes
howtos/web_services
howtos/company
howtos/create_reports
howtos/accounting_localization
howtos/translations
howtos/connect_device
howtos/web_services
howtos/company
howtos/create_reports
howtos/accounting_localization
howtos/translations
howtos/connect_device
howtos/upgrade_custom_db
howtos/upgrade_custom_db
```
Frontend development
====================
## Frontend development
```{eval-rst}
.. cards::
.. card:: Write lean easy-to-maintain CSS
@@ -63,10 +64,11 @@ Frontend development
:target: howtos/website_themes
Learn how to customize your website by creating a custom theme.
```
Server-side development
=======================
## Server-side development
```{eval-rst}
.. cards::
.. card:: Web services
@@ -99,10 +101,11 @@ Server-side development
:target: howtos/connect_device
Learn how to enable a module to detect and communicate with an IoT device.
```
Custom development
==================
## Custom development
```{eval-rst}
.. cards::
.. card:: Upgrade a customized database
@@ -110,3 +113,5 @@ Custom development
Learn how to upgrade a customized database, as well as the code and data of its custom
modules.
```
@@ -0,0 +1,325 @@
# Accounting localization
:::{warning}
This tutorial requires knowledge about how to build a module in Odoo (see
{doc}`../tutorials/server_framework_101`).
:::
## Installation procedure
On installing the [account]({GITHUB_PATH}/addons/account) module, the localization module corresponding to the country code of the company is installed automatically.
In case of no country code set or no localization module found, the [l10n_generic_coa]({GITHUB_PATH}/addons/l10n_generic_coa) (US) localization module is installed by default.
Check [post init hook]({GITHUB_PATH}/addons/account/__init__.py) for details.
For example, [l10n_ch]({GITHUB_PATH}/addons/l10n_ch) will be installed if the company has `Switzerland` as country.
## Building a localization module
The structure of a basic `l10n_XX` module may be described with the following {file}`__manifest__.py` file:
```python
{
"name": "COUNTRY - Accounting",
"version": "1.0.0",
"category": "Accounting/Localizations/Account Charts",
"license": "LGPL-3",
"depends": [
"account",
],
"data": [
"data/other_data.xml",
"views/xxxmodel_views.xml",
],
"demo": [
"demo/demo_company.xml",
]
}
```
Your worktree should look like this
```bash
l10n_xx
├── data
│ ├── template
│ │ ├── account.account-xx.csv
│ │ ├── account.group-xx.csv
│ │ └── account.tax.group-xx.csv
│ └── other_data.xml
├── views
│ └── xxxmodel_views.xml
├── demo
│ └── demo_company.xml
├── models
│ ├── template_xx.py
│ └── __init__.py
├── __init__.py
└── __manifest__.py
```
In the first file {file}`models/template_xx.py`, we set the name for the chart of accounts along with some basic fields.
:::{seealso}
{doc}`Chart Template References </developer/reference/standard_modules/account>`
:::
```{eval-rst}
.. example::
`addons/l10n_be/models/template_be.py <{GITHUB_PATH}/addons/l10n_be/models/template_be.py>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_be/models/template_be.py
:condition: odoo_dir_in_path
:language: python
:start-at: _get_be_template_data
:end-before: _get_be_reconcile_model
```
## Chart of Accounts
### Account tags
:::{seealso}
{ref}`Account Tag References <reference/account_account_tag>`
:::
Tags are a way to sort accounts.
For example, imagine you want to create a financial report having multiple lines but you have no way to find a rule to dispatch the accounts according to their `code`.
The solution is the usage of tags, one for each report line, to filter accounts like you want.
Put the tags in the {file}`data/account_account_tag_data.xml` file.
```{eval-rst}
.. example::
`addons/l10n_lt/data/template/account.account-lt.csv <{GITHUB_PATH}/addons/l10n_lt/data/template/account.account-lt.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_lt/data/template/account.account-lt.csv
:condition: odoo_dir_in_path
:language: csv
:end-at: account_account_template_1201
```
### Accounts
:::{seealso}
- {ref}`Account References <reference/account_account>`
- {doc}`/applications/finance/accounting/get_started/chart_of_accounts`
:::
Obviously, {guilabel}`Chart of Accounts` cannot exist without {guilabel}`Accounts`. You need to specify them in {file}`data/account.account.template.csv`.
```{eval-rst}
.. example::
`addons/l10n_ch/data/template/account.account-ch.csv <{GITHUB_PATH}/addons/l10n_ch/data/template/account.account-ch.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_ch/data/template/account.account-ch.csv
:condition: odoo_dir_in_path
:language: csv
:end-at: ch_coa_1171
```
:::{warning}
- Avoid the usage of `asset_cash` `account_type`!
Indeed, the bank & cash accounts are created directly at the installation of the localization module and then, are linked to an `account.journal`.
- Only one account of type payable/receivable is enough for the generic case. We need to define a PoS receivable account as well however. (linked in the CoA)
- Don't create too many accounts: 200-300 is enough. But mostly, we try to find a good balance where the CoA needs minimal adapting for most companies afterwards.
:::
### Account groups
:::{seealso}
{ref}`Account Group References <reference/account_group>`
:::
Account groups allow describing the hierarchical structure of the chart of accounts. The filter needs to be activated in the report and then when you decollapse into journal entries it will show the parents of the account.
It works with the prefix *start*/*end*, so every account where the code starts with something between *start* and *end* will have this `account.group` as the parent group. Furthermore, the account groups can have a parent account group as well to form the hierarchy.
```{eval-rst}
.. example::
`addons/l10n_il/data/template/account.group-il.csv <{GITHUB_PATH}/addons/l10n_il/data/template/account.group-il.csv>`_
.. csv-table::
:condition: odoo_dir_in_path
:file: {ODOO_RELPATH}/addons/l10n_il/data/template/account.group-il.csv
:widths: 20,20,20,20,20
:header-rows: 1
```
### Taxes
:::{seealso}
- {ref}`Tax References <reference/account_tax>`
- {doc}`/applications/finance/accounting/taxes/`
:::
To add taxes you first need to specify tax groups. You normally need just one tax group for every tax rate, except for the 0% as you need to often distinguish between exempt, 0%, not subject, ... taxes.
This model only has two required fields: `name` and `country`. Create the file {file}`data/template/account.tax.group-xx.csv` and list the groups.
```{eval-rst}
.. example::
`addons/l10n_uk/data/template/account.tax.group-uk.csv <{GITHUB_PATH}/addons/l10n_uk/data/template/account.tax.group-uk.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_uk/data/template/account.tax.group-uk.csv
:condition: odoo_dir_in_path
:language: csv
```
Now you can add the taxes via {file}`data/template/account.tax-xx.csv` file. The first tax you define that is purchase/sale also becomes the default purchase/sale tax for your products.
```{eval-rst}
.. example::
`addons/l10n_ae/data/template/account.tax-ae.csv <{GITHUB_PATH}/addons/l10n_ae/data/template/account.tax-ae.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_ae/data/template/account.tax-ae.csv
:condition: odoo_dir_in_path
:language: xml
:end-at: uae_sale_tax_5_ras_al_khaima
```
### Tax Report
```{raw} html
<div><span class="badge" style="background-color:#AD5E99">Enterprise feature</span><div>
```
The tax report is declared in the {guilabel}`Invoicing` (`account`) app, but the report is only accessible when {guilabel}`Accounting` (`account_accountant`) is installed.
:::{seealso}
- {doc}`/developer/reference/standard_modules/account/account_report_line`
- {doc}`/applications/finance/accounting/reporting/tax_returns`
:::
In the previous section, you noticed the fields `invoice_repartition_line_ids` or `refund_repartition_line_ids` and probably understood nothing about them. Good news: you are not alone on this incomprehension. Bad news: you have to figure it out a bit. The topic is complicated. Indeed:
```{eval-rst}
.. graphviz:: accounting_localization/tax_report.dot
:class: overflow-auto
```
The simple version is that, in the tax template, you indicate in the invoice/refund repartition lines whether the base or a percentage of the tax needs to be reported in which report line (through the *minus/plus_report_line_ids* fields).
It becomes clear also when you check the tax configuration in the Odoo interface (or check the docs {ref}`Tax References <reference/account_tax>`, {ref}`Tax Repartition References <reference/account_tax_repartition>`).
So, once you have properly configured taxes, you just need to add the {file}`data/account_tax_report_data.xml` file with a record for your `account.report`. For it to be considered as a tax report, you need to provide it with the right `root_report_id`.
```xml
<odoo>
<record id="tax_report" model="account.report">
<field name="name">Tax Report</field>
<field name="root_report_id" ref="account.generic_tax_report"/>
<field name="country_id" ref="base.XX"/>
</record>
...
</odoo>
```
... followed by the declaration of its lines, as `account.report.line` records.
```{eval-rst}
.. example::
`addons/l10n_au/data/account_tax_report_data.xml <{GITHUB_PATH}/addons/l10n_au/data/account_tax_report_data.xml>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_au/data/account_tax_report_data.xml
:condition: odoo_dir_in_path
:language: xml
:start-at: tax_report
:end-before: account_tax_report_gstrpt_g3
```
### Fiscal positions
:::{seealso}
- {ref}`Fiscal Position References <reference/account_fiscal_position>`
- {doc}`/applications/finance/accounting/taxes/fiscal_positions`
:::
Specify fiscal positions in the {file}`data/template/account.fiscal.position-xx.csv` file.
```{eval-rst}
.. example::
`addons/l10n_es/data/template/account.fiscal.position-es_common.csv <{GITHUB_PATH}/addons/l10n_es/data/template/account.fiscal.position-es_common.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_es/data/template/account.fiscal.position-es_common.csv
:condition: odoo_dir_in_path
:language: csv
:end-at: account_tax_template_p_iva10_sp_ex
```
## Final steps
Finally, you may add a demo company, so the localization can easily be tested in demo mode.
```{eval-rst}
.. example::
`addons/l10n_ch/demo/demo_company.xml <{GITHUB_PATH}/addons/l10n_ch/demo/demo_company.xml>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_ch/demo/demo_company.xml
:condition: odoo_dir_in_path
:language: xml
:start-after: <odoo>
:end-before: </odoo>
```
## Accounting reports
```{raw} html
<div><span class="badge" style="background-color:#AD5E99">Enterprise feature</span><div>
```
:::{seealso}
{doc}`/applications/finance/accounting/reporting`
:::
Accounting reports should be added via a separate module `l10n_XX_reports` that should go to the [enterprise repository]({GITHUB_ENT_PATH}).
Basic {file}`__manifest__.py` file for such a module looks as following:
```python
{
"name": "COUNTRY - Accounting Reports",
"category": "Accounting/Localizations/Reporting",
"version": "1.0.0",
"license": "OEEL-1",
"depends": [
"l10n_XX", "account_reports"
],
"data": [
"data/balance_sheet.xml",
"data/profit_and_loss.xml",
],
"auto_install": True,
}
```
Functional overview of financial reports is here: {doc}`/applications/finance/accounting/reporting`.
Some good examples:
- [l10n_ch_reports/data/account_financial_html_report_data.xml]({GITHUB_ENT_PATH}/l10n_ch_reports/data/account_financial_html_report_data.xml)
- [l10n_be_reports/data/account_financial_html_report_data.xml]({GITHUB_ENT_PATH}/l10n_be_reports/data/account_financial_html_report_data.xml)
You can check the meaning of the fields here:
- {doc}`/developer/reference/standard_modules/account/account_report`
- {doc}`/developer/reference/standard_modules/account/account_report_line`
If you gave a `root_report_id` to your report, it is now available in its variant selector. If not,
you still need to add a menu item for it. A default menu item can be created from the form view of
the report by clicking on {menuselection}`Actions --> Create Menu Item`. You will then need to
refresh the page to see it. Alternatively, to create a dedicated section for a totally new report in
the {guilabel}`Reporting` menu, you need to create a new `ir.ui.menu` record (usually in the main
`l10n_XX` module) and a new `ir.actions.client` (usually in the new report XML file) that calls the
`account.report` with the new **report id**. Then, set the new menu as `parent_id` field in the
action model.
```{eval-rst}
.. example::
* `ir.ui.menu creation <{GITHUB_PATH}/addons/l10n_be/data/menuitem_data.xml>`_
* `ir.actions.client and menu item creation <{GITHUB_ENT_PATH}/l10n_be_reports/data/partner_vat_listing.xml>`_
```
@@ -1,315 +0,0 @@
=======================
Accounting localization
=======================
.. warning::
This tutorial requires knowledge about how to build a module in Odoo (see
:doc:`../tutorials/server_framework_101`).
Installation procedure
======================
On installing the `account <{GITHUB_PATH}/addons/account>`_ module, the localization module corresponding to the country code of the company is installed automatically.
In case of no country code set or no localization module found, the `l10n_generic_coa <{GITHUB_PATH}/addons/l10n_generic_coa>`_ (US) localization module is installed by default.
Check `post init hook <{GITHUB_PATH}/addons/account/__init__.py>`_ for details.
For example, `l10n_ch <{GITHUB_PATH}/addons/l10n_ch>`_ will be installed if the company has ``Switzerland`` as country.
Building a localization module
==============================
The structure of a basic ``l10n_XX`` module may be described with the following :file:`__manifest__.py` file:
.. code-block:: python
{
"name": "COUNTRY - Accounting",
"version": "1.0.0",
"category": "Accounting/Localizations/Account Charts",
"license": "LGPL-3",
"depends": [
"account",
],
"data": [
"data/other_data.xml",
"views/xxxmodel_views.xml",
],
"demo": [
"demo/demo_company.xml",
]
}
Your worktree should look like this
.. code-block:: bash
l10n_xx
├── data
│ ├── template
│ │ ├── account.account-xx.csv
│ │ ├── account.group-xx.csv
│ │ └── account.tax.group-xx.csv
│ └── other_data.xml
├── views
│ └── xxxmodel_views.xml
├── demo
│ └── demo_company.xml
├── models
│ ├── template_xx.py
│ └── __init__.py
├── __init__.py
└── __manifest__.py
In the first file :file:`models/template_xx.py`, we set the name for the chart of accounts along with some basic fields.
.. seealso::
:doc:`Chart Template References </developer/reference/standard_modules/account>`
.. example::
`addons/l10n_be/models/template_be.py <{GITHUB_PATH}/addons/l10n_be/models/template_be.py>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_be/models/template_be.py
:condition: odoo_dir_in_path
:language: python
:start-at: _get_be_template_data
:end-before: _get_be_reconcile_model
Chart of Accounts
=================
Account tags
------------
.. seealso::
:ref:`Account Tag References <reference/account_account_tag>`
Tags are a way to sort accounts.
For example, imagine you want to create a financial report having multiple lines but you have no way to find a rule to dispatch the accounts according to their ``code``.
The solution is the usage of tags, one for each report line, to filter accounts like you want.
Put the tags in the :file:`data/account_account_tag_data.xml` file.
.. example::
`addons/l10n_lt/data/template/account.account-lt.csv <{GITHUB_PATH}/addons/l10n_lt/data/template/account.account-lt.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_lt/data/template/account.account-lt.csv
:condition: odoo_dir_in_path
:language: csv
:end-at: account_account_template_1201
Accounts
--------
.. seealso::
- :ref:`Account References <reference/account_account>`
- :doc:`/applications/finance/accounting/get_started/chart_of_accounts`
Obviously, :guilabel:`Chart of Accounts` cannot exist without :guilabel:`Accounts`. You need to specify them in :file:`data/account.account.template.csv`.
.. example::
`addons/l10n_ch/data/template/account.account-ch.csv <{GITHUB_PATH}/addons/l10n_ch/data/template/account.account-ch.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_ch/data/template/account.account-ch.csv
:condition: odoo_dir_in_path
:language: csv
:end-at: ch_coa_1171
.. warning::
- Avoid the usage of `asset_cash` ``account_type``!
Indeed, the bank & cash accounts are created directly at the installation of the localization module and then, are linked to an ``account.journal``.
- Only one account of type payable/receivable is enough for the generic case. We need to define a PoS receivable account as well however. (linked in the CoA)
- Don't create too many accounts: 200-300 is enough. But mostly, we try to find a good balance where the CoA needs minimal adapting for most companies afterwards.
Account groups
--------------
.. seealso::
:ref:`Account Group References <reference/account_group>`
Account groups allow describing the hierarchical structure of the chart of accounts. The filter needs to be activated in the report and then when you decollapse into journal entries it will show the parents of the account.
It works with the prefix *start*/*end*, so every account where the code starts with something between *start* and *end* will have this ``account.group`` as the parent group. Furthermore, the account groups can have a parent account group as well to form the hierarchy.
.. example::
`addons/l10n_il/data/template/account.group-il.csv <{GITHUB_PATH}/addons/l10n_il/data/template/account.group-il.csv>`_
.. csv-table::
:condition: odoo_dir_in_path
:file: {ODOO_RELPATH}/addons/l10n_il/data/template/account.group-il.csv
:widths: 20,20,20,20,20
:header-rows: 1
Taxes
-----
.. seealso::
- :ref:`Tax References <reference/account_tax>`
- :doc:`/applications/finance/accounting/taxes/`
To add taxes you first need to specify tax groups. You normally need just one tax group for every tax rate, except for the 0% as you need to often distinguish between exempt, 0%, not subject, ... taxes.
This model only has two required fields: `name` and `country`. Create the file :file:`data/template/account.tax.group-xx.csv` and list the groups.
.. example::
`addons/l10n_uk/data/template/account.tax.group-uk.csv <{GITHUB_PATH}/addons/l10n_uk/data/template/account.tax.group-uk.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_uk/data/template/account.tax.group-uk.csv
:condition: odoo_dir_in_path
:language: csv
Now you can add the taxes via :file:`data/template/account.tax-xx.csv` file. The first tax you define that is purchase/sale also becomes the default purchase/sale tax for your products.
.. example::
`addons/l10n_ae/data/template/account.tax-ae.csv <{GITHUB_PATH}/addons/l10n_ae/data/template/account.tax-ae.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_ae/data/template/account.tax-ae.csv
:condition: odoo_dir_in_path
:language: xml
:end-at: uae_sale_tax_5_ras_al_khaima
Tax Report
----------
.. raw:: html
<div><span class="badge" style="background-color:#AD5E99">Enterprise feature</span><div>
The tax report is declared in the :guilabel:`Invoicing` (`account`) app, but the report is only accessible when :guilabel:`Accounting` (`account_accountant`) is installed.
.. seealso::
- :doc:`/developer/reference/standard_modules/account/account_report_line`
- :doc:`/applications/finance/accounting/reporting/tax_returns`
In the previous section, you noticed the fields `invoice_repartition_line_ids` or `refund_repartition_line_ids` and probably understood nothing about them. Good news: you are not alone on this incomprehension. Bad news: you have to figure it out a bit. The topic is complicated. Indeed:
.. graphviz:: accounting_localization/tax_report.dot
:class: overflow-auto
The simple version is that, in the tax template, you indicate in the invoice/refund repartition lines whether the base or a percentage of the tax needs to be reported in which report line (through the *minus/plus_report_line_ids* fields).
It becomes clear also when you check the tax configuration in the Odoo interface (or check the docs :ref:`Tax References <reference/account_tax>`, :ref:`Tax Repartition References <reference/account_tax_repartition>`).
So, once you have properly configured taxes, you just need to add the :file:`data/account_tax_report_data.xml` file with a record for your `account.report`. For it to be considered as a tax report, you need to provide it with the right `root_report_id`.
.. code-block:: xml
<odoo>
<record id="tax_report" model="account.report">
<field name="name">Tax Report</field>
<field name="root_report_id" ref="account.generic_tax_report"/>
<field name="country_id" ref="base.XX"/>
</record>
...
</odoo>
... followed by the declaration of its lines, as `account.report.line` records.
.. example::
`addons/l10n_au/data/account_tax_report_data.xml <{GITHUB_PATH}/addons/l10n_au/data/account_tax_report_data.xml>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_au/data/account_tax_report_data.xml
:condition: odoo_dir_in_path
:language: xml
:start-at: tax_report
:end-before: account_tax_report_gstrpt_g3
Fiscal positions
----------------
.. seealso::
- :ref:`Fiscal Position References <reference/account_fiscal_position>`
- :doc:`/applications/finance/accounting/taxes/fiscal_positions`
Specify fiscal positions in the :file:`data/template/account.fiscal.position-xx.csv` file.
.. example::
`addons/l10n_es/data/template/account.fiscal.position-es_common.csv <{GITHUB_PATH}/addons/l10n_es/data/template/account.fiscal.position-es_common.csv>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_es/data/template/account.fiscal.position-es_common.csv
:condition: odoo_dir_in_path
:language: csv
:end-at: account_tax_template_p_iva10_sp_ex
Final steps
===========
Finally, you may add a demo company, so the localization can easily be tested in demo mode.
.. example::
`addons/l10n_ch/demo/demo_company.xml <{GITHUB_PATH}/addons/l10n_ch/demo/demo_company.xml>`_
.. literalinclude:: {ODOO_RELPATH}/addons/l10n_ch/demo/demo_company.xml
:condition: odoo_dir_in_path
:language: xml
:start-after: <odoo>
:end-before: </odoo>
Accounting reports
==================
.. raw:: html
<div><span class="badge" style="background-color:#AD5E99">Enterprise feature</span><div>
.. seealso::
:doc:`/applications/finance/accounting/reporting`
Accounting reports should be added via a separate module `l10n_XX_reports` that should go to the `enterprise repository <{GITHUB_ENT_PATH}>`_.
Basic :file:`__manifest__.py` file for such a module looks as following:
.. code-block:: python
{
"name": "COUNTRY - Accounting Reports",
"category": "Accounting/Localizations/Reporting",
"version": "1.0.0",
"license": "OEEL-1",
"depends": [
"l10n_XX", "account_reports"
],
"data": [
"data/balance_sheet.xml",
"data/profit_and_loss.xml",
],
"auto_install": True,
}
Functional overview of financial reports is here: :doc:`/applications/finance/accounting/reporting`.
Some good examples:
* `l10n_ch_reports/data/account_financial_html_report_data.xml <{GITHUB_ENT_PATH}/l10n_ch_reports/data/account_financial_html_report_data.xml>`_
* `l10n_be_reports/data/account_financial_html_report_data.xml <{GITHUB_ENT_PATH}/l10n_be_reports/data/account_financial_html_report_data.xml>`_
You can check the meaning of the fields here:
* :doc:`/developer/reference/standard_modules/account/account_report`
* :doc:`/developer/reference/standard_modules/account/account_report_line`
If you gave a `root_report_id` to your report, it is now available in its variant selector. If not,
you still need to add a menu item for it. A default menu item can be created from the form view of
the report by clicking on :menuselection:`Actions --> Create Menu Item`. You will then need to
refresh the page to see it. Alternatively, to create a dedicated section for a totally new report in
the :guilabel:`Reporting` menu, you need to create a new `ir.ui.menu` record (usually in the main
`l10n_XX` module) and a new `ir.actions.client` (usually in the new report XML file) that calls the
`account.report` with the new **report id**. Then, set the new menu as `parent_id` field in the
action model.
.. example::
* `ir.ui.menu creation <{GITHUB_PATH}/addons/l10n_be/data/menuitem_data.xml>`_
* `ir.actions.client and menu item creation <{GITHUB_ENT_PATH}/l10n_be_reports/data/partner_vat_listing.xml>`_
+233
View File
@@ -0,0 +1,233 @@
(reference-howtos-company)=
# Multi-company Guidelines
:::{warning}
This tutorial requires good knowledge of Odoo.
Please refer to the {doc}`../tutorials/server_framework_101` tutorial first if needed.
:::
As of version 13.0, a user can be logged in to multiple companies at once. This allows the user to
access information from multiple companies, but also to create/edit records in a multi-company
environment.
If not managed correctly, it may be the source of a lot of inconsistent multi-company behaviors.
For instance, a user logged in to both companies A and B could create a sales order in company A and
add products belonging to company B to it. It is only when the user logs out from company B that
access errors will occur for the sales order.
To correctly manage multi-company behaviors, Odoo's ORM provides multiple features:
- {ref}`Company-dependent fields <howto/company/company_dependent>`
- {ref}`Multi-company consistency <howto/company/check_company>`
- {ref}`Default company <howto/company/default_company>`
- {ref}`Views <howto/company/views>`
- {ref}`Security rules <howto/company/security>`
(howto-company-company-dependent)=
## Company-dependent fields
When a record is available from multiple companies, we must expect that different values will be
assigned to a given field depending on the company from which the value is set.
For the field of the same record to support several values, it must be defined with the attribute
`company_dependent` set to `True`.
```python
from odoo import api, fields, models
class Record(models.Model):
_name = 'record.public'
info = fields.Text()
company_info = fields.Text(company_dependent=True)
display_info = fields.Text(string='Infos', compute='_compute_display_info')
@api.depends_context('company')
def _compute_display_info(self):
for record in self:
record.display_info = record.info + record.company_info
```
:::{note}
The `_compute_display_info` method is decorated with `depends_context('company')`
(see {attr}`~odoo.api.depends_context`) to ensure that the computed field is recomputed
depending on the current company (`self.env.company`).
:::
When a company-dependent field is read, the current company is used to retrieve its value. In other
words, if a user is logged in to companies A and B with A as the main company and creates a record for
company B, the value of company-dependent fields will be that of company A.
To read the values of company-dependent fields set by another company than the current one, we need
to ensure the company we are using is the correct one. This can be done with {meth}`~odoo.models.Model.with_company`,
which updates the current company.
```python
# Accessed as the main company (self.env.company)
val = record.company_dependent_field
# Accessed as the desired company (company_B)
val = record.with_company(company_B).company_dependent_field
# record.with_company(company_B).env.company == company_B
```
:::{warning}
Whenever you are computing/creating/... things that may behave differently
in different companies, you should make sure whatever you are doing is done
in the right company. It doesn't cost much to always use `with_company` to
avoid problems later.
```python
@api.onchange('field_name')
def _onchange_field_name(self):
self = self.with_company(self.company_id)
...
@api.depends('field_2')
def _compute_field_3(self):
for record in self:
record = record.with_company(record.company_id)
...
```
:::
(howto-company-check-company)=
## Multi-company consistency
When a record is made shareable between several companies by the means of a `company_id` field, we
must take care that it cannot be linked to the record of another company through a relational field.
For instance, we do not want to have a sales order and its invoice belonging to different companies.
To ensure this multi-company consistency, you must:
- Set the class attribute `_check_company_auto` to `True`.
- Define relational fields with the attribute `check_company` set to `True` if their model has a
`company_id` field.
On each {meth}`~odoo.models.Model.create` and {meth}`~odoo.models.Model.write`, automatic checks
will be triggered to ensure the multi-company consistency of the record.
```python
from odoo import fields, models
class Record(models.Model):
_name = 'record.shareable'
_check_company_auto = True
company_id = fields.Many2one('res.company')
other_record_id = fields.Many2one('other.record', check_company=True)
```
:::{note}
The field `company_id` must not be defined with `check_company=True`.
:::
```{eval-rst}
.. currentmodule:: odoo.models
```
```{eval-rst}
.. automethod:: Model._check_company
```
:::{warning}
The `check_company` feature performs a strict check! It means that if a record has no
`company_id` (i.e., the field is not required), it cannot be linked to a record whose
`company_id` is set.
:::
:::{note}
When no domain is defined on the field and `check_company` is set to `True`, a default domain is
added: `['|', '('company_id', '=', False), ('company_id', '=', company_id)]`
:::
(howto-company-default-company)=
## Default company
When the field `company_id` is made required on a model, a good practice is to set a default
company. It eases the setup flow for the user or even guarantees its validity when the company is
hidden from view. Indeed, the company is usually hidden if the user does not have access to
multiple companies (i.e., when the user does not have the group `base.group_multi_company`).
```python
from odoo import api, fields, models
class Record(models.Model):
_name = 'record.restricted'
_check_company_auto = True
company_id = fields.Many2one(
'res.company', required=True, default=lambda self: self.env.company
)
other_record_id = fields.Many2one('other.record', check_company=True)
```
(howto-company-views)=
## Views
As stated in {ref}`above <howto/company/default_company>`, the company is usually hidden
from view if the user does not have access to multiple companies. This is assessed with the group
`base.group_multi_company`.
```xml
<record model="ir.ui.view" id="record_form_view">
<field name="name">record.restricted.form</field>
<field name="model">record.restricted</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="company_id" groups="base.group_multi_company"/>
<field name="other_record_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
```
(howto-company-security)=
## Security rules
When working with records shared across companies or restricted to a single company, we must take
care that a user does not have access to records belonging to other companies.
This is achieved with security rules based on `company_ids`, which contain the current companies of
the user (the companies the user checked in the multi-company widget).
```xml
<!-- Shareable Records -->
<record model="ir.rule" id="record_shared_company_rule">
<field name="name">Shared Record: multi-company</field>
<field name="model_id" ref="model_record_shared"/>
<field name="global" eval="True"/>
<field name="domain_force">
['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
</field>
</record>
```
```xml
<!-- Company-restricted Records -->
<record model="ir.rule" id="record_restricted_company_rule">
<field name="name">Restricted Record: multi-company</field>
<field name="model_id" ref="model_record_restricted"/>
<field name="global" eval="True"/>
<field name="domain_force">
[('company_id', 'in', company_ids)]
</field>
</record>
```
```{eval-rst}
.. todo:: check_company on company_dependent fields.
```
-229
View File
@@ -1,229 +0,0 @@
.. _reference/howtos/company:
========================
Multi-company Guidelines
========================
.. warning::
This tutorial requires good knowledge of Odoo.
Please refer to the :doc:`../tutorials/server_framework_101` tutorial first if needed.
As of version 13.0, a user can be logged in to multiple companies at once. This allows the user to
access information from multiple companies, but also to create/edit records in a multi-company
environment.
If not managed correctly, it may be the source of a lot of inconsistent multi-company behaviors.
For instance, a user logged in to both companies A and B could create a sales order in company A and
add products belonging to company B to it. It is only when the user logs out from company B that
access errors will occur for the sales order.
To correctly manage multi-company behaviors, Odoo's ORM provides multiple features:
- :ref:`Company-dependent fields <howto/company/company_dependent>`
- :ref:`Multi-company consistency <howto/company/check_company>`
- :ref:`Default company <howto/company/default_company>`
- :ref:`Views <howto/company/views>`
- :ref:`Security rules <howto/company/security>`
.. _howto/company/company_dependent:
Company-dependent fields
------------------------
When a record is available from multiple companies, we must expect that different values will be
assigned to a given field depending on the company from which the value is set.
For the field of the same record to support several values, it must be defined with the attribute
`company_dependent` set to `True`.
.. code-block:: python
from odoo import api, fields, models
class Record(models.Model):
_name = 'record.public'
info = fields.Text()
company_info = fields.Text(company_dependent=True)
display_info = fields.Text(string='Infos', compute='_compute_display_info')
@api.depends_context('company')
def _compute_display_info(self):
for record in self:
record.display_info = record.info + record.company_info
.. note:: The `_compute_display_info` method is decorated with `depends_context('company')`
(see :attr:`~odoo.api.depends_context`) to ensure that the computed field is recomputed
depending on the current company (`self.env.company`).
When a company-dependent field is read, the current company is used to retrieve its value. In other
words, if a user is logged in to companies A and B with A as the main company and creates a record for
company B, the value of company-dependent fields will be that of company A.
To read the values of company-dependent fields set by another company than the current one, we need
to ensure the company we are using is the correct one. This can be done with :meth:`~odoo.models.Model.with_company`,
which updates the current company.
.. code-block:: python
# Accessed as the main company (self.env.company)
val = record.company_dependent_field
# Accessed as the desired company (company_B)
val = record.with_company(company_B).company_dependent_field
# record.with_company(company_B).env.company == company_B
.. warning::
Whenever you are computing/creating/... things that may behave differently
in different companies, you should make sure whatever you are doing is done
in the right company. It doesn't cost much to always use `with_company` to
avoid problems later.
.. code-block:: python
@api.onchange('field_name')
def _onchange_field_name(self):
self = self.with_company(self.company_id)
...
@api.depends('field_2')
def _compute_field_3(self):
for record in self:
record = record.with_company(record.company_id)
...
.. _howto/company/check_company:
Multi-company consistency
-------------------------
When a record is made shareable between several companies by the means of a `company_id` field, we
must take care that it cannot be linked to the record of another company through a relational field.
For instance, we do not want to have a sales order and its invoice belonging to different companies.
To ensure this multi-company consistency, you must:
* Set the class attribute `_check_company_auto` to `True`.
* Define relational fields with the attribute `check_company` set to `True` if their model has a
`company_id` field.
On each :meth:`~odoo.models.Model.create` and :meth:`~odoo.models.Model.write`, automatic checks
will be triggered to ensure the multi-company consistency of the record.
.. code-block:: python
from odoo import fields, models
class Record(models.Model):
_name = 'record.shareable'
_check_company_auto = True
company_id = fields.Many2one('res.company')
other_record_id = fields.Many2one('other.record', check_company=True)
.. note:: The field `company_id` must not be defined with `check_company=True`.
.. currentmodule:: odoo.models
.. automethod:: Model._check_company
.. warning:: The `check_company` feature performs a strict check! It means that if a record has no
`company_id` (i.e., the field is not required), it cannot be linked to a record whose
`company_id` is set.
.. note::
When no domain is defined on the field and `check_company` is set to `True`, a default domain is
added: `['|', '('company_id', '=', False), ('company_id', '=', company_id)]`
.. _howto/company/default_company:
Default company
---------------
When the field `company_id` is made required on a model, a good practice is to set a default
company. It eases the setup flow for the user or even guarantees its validity when the company is
hidden from view. Indeed, the company is usually hidden if the user does not have access to
multiple companies (i.e., when the user does not have the group `base.group_multi_company`).
.. code-block:: python
from odoo import api, fields, models
class Record(models.Model):
_name = 'record.restricted'
_check_company_auto = True
company_id = fields.Many2one(
'res.company', required=True, default=lambda self: self.env.company
)
other_record_id = fields.Many2one('other.record', check_company=True)
.. _howto/company/views:
Views
-----
As stated in :ref:`above <howto/company/default_company>`, the company is usually hidden
from view if the user does not have access to multiple companies. This is assessed with the group
`base.group_multi_company`.
.. code-block:: xml
<record model="ir.ui.view" id="record_form_view">
<field name="name">record.restricted.form</field>
<field name="model">record.restricted</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="company_id" groups="base.group_multi_company"/>
<field name="other_record_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
.. _howto/company/security:
Security rules
--------------
When working with records shared across companies or restricted to a single company, we must take
care that a user does not have access to records belonging to other companies.
This is achieved with security rules based on `company_ids`, which contain the current companies of
the user (the companies the user checked in the multi-company widget).
.. code-block:: xml
<!-- Shareable Records -->
<record model="ir.rule" id="record_shared_company_rule">
<field name="name">Shared Record: multi-company</field>
<field name="model_id" ref="model_record_shared"/>
<field name="global" eval="True"/>
<field name="domain_force">
['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
</field>
</record>
.. code-block:: xml
<!-- Company-restricted Records -->
<record model="ir.rule" id="record_restricted_company_rule">
<field name="name">Restricted Record: multi-company</field>
<field name="model_id" ref="model_record_restricted"/>
<field name="global" eval="True"/>
<field name="domain_force">
[('company_id', 'in', company_ids)]
</field>
</record>
.. todo:: check_company on company_dependent fields.
@@ -1,6 +1,4 @@
=====================
Connect with a device
=====================
# Connect with a device
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
@@ -17,21 +15,20 @@ be located on the connected Odoo instance. Each module can contain an
`iot_handlers` directory that will be copied to the IoT Box. The structure of
this directory is the following
.. code-block:: text
```text
your_module
├── ...
└── iot_handlers
├── drivers
│ ├── DriverName.py
│ └── ...
└── interfaces
├── InterfaceName.py
└── ...
```
your_module
├── ...
└── iot_handlers
├── drivers
│ ├── DriverName.py
│ └── ...
└── interfaces
├── InterfaceName.py
└── ...
Detect Devices
==============
## Detect Devices
Devices connected to the IoT Box are detected through `Interfaces`. There is an
Interface for each supported connection type (USB, Bluetooth, Video,
@@ -41,8 +38,7 @@ 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.
Interface
---------
### Interface
The role of the Interface is to maintain a list of devices connected through a
determined connection type. Creating a new interface requires
@@ -53,25 +49,25 @@ determined connection type. Creating a new interface requires
containing data about each detected device. This data will be given as
argument to the constructors and `supported` method of the Drivers.
.. note::
Setting the `_loop_delay` attribute will modify the interval between calls
to `get_devices`. By default, this interval is set to 3 seconds.
:::{note}
Setting the `_loop_delay` attribute will modify the interval between calls
to `get_devices`. By default, this interval is set to 3 seconds.
:::
.. code-block:: python
```python
from odoo.addons.hw_drivers.interface import Interface
from odoo.addons.hw_drivers.interface import Interface
class InterfaceName(Interface):
connection_type = 'ConnectionType'
class InterfaceName(Interface):
connection_type = 'ConnectionType'
def get_devices(self):
return {
'device_identifier_1': {...},
...
}
```
def get_devices(self):
return {
'device_identifier_1': {...},
...
}
Driver
------
### Driver
Once the interface has retrieved the list of detected devices, it will loop
through all of the Drivers that have the same `connection_type` attribute and
@@ -79,11 +75,12 @@ 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.
.. note::
`supported` methods of drivers are given a priority order. The `supported`
method of a child class will always be tested before the one of its parent.
This priority can be adjusted by modifying the `priority` attribute of the
Driver.
:::{note}
`supported` methods of drivers are given a priority order. The `supported`
method of a child class will always be tested before the one of its parent.
This priority can be adjusted by modifying the `priority` attribute of the
Driver.
:::
Creating a new Driver requires:
@@ -92,25 +89,24 @@ Creating a new Driver requires:
- Setting the `device_type`, `device_connection` and `device_name` attributes.
- Defining the `supported` method
.. code-block:: python
```python
from odoo.addons.hw_drivers.driver import Driver
from odoo.addons.hw_drivers.driver import Driver
class DriverName(Driver):
connection_type = 'ConnectionType'
class DriverName(Driver):
connection_type = 'ConnectionType'
def __init__(self, identifier, device):
super(NewDriver, self).__init__(identifier, device)
self.device_type = 'DeviceType'
self.device_connection = 'DeviceConnection'
self.device_name = 'DeviceName'
def __init__(self, identifier, device):
super(NewDriver, self).__init__(identifier, device)
self.device_type = 'DeviceType'
self.device_connection = 'DeviceConnection'
self.device_name = 'DeviceName'
@classmethod
def supported(cls, device):
...
```
@classmethod
def supported(cls, device):
...
Communicate With Devices
========================
## 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
@@ -118,76 +114,76 @@ 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`_
\- 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
```javascript
var DeviceProxy = require('iot.DeviceProxy');
var DeviceProxy = require('iot.DeviceProxy');
var iot_device = new DeviceProxy({
iot_ip: iot_ip,
identifier: device_identifier
});
```
var iot_device = new DeviceProxy({
iot_ip: iot_ip,
identifier: device_identifier
});
Actions
-------
### 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.
:::{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);
```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
```python
def action(self, data):
...
```
def action(self, data):
...
Longpolling
-----------
### 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
```javascript
iot_device.add_listener(this._onValueChange.bind(this));
iot_device.add_listener(this._onValueChange.bind(this));
_onValueChange: function (result) {
...
}
_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
```python
from odoo.addons.hw_drivers.event_manager import event_manager
from odoo.addons.hw_drivers.event_manager import event_manager
class DriverName(Driver):
connection_type = 'ConnectionType'
class DriverName(Driver):
connection_type = 'ConnectionType'
def methodName(self):
self.data = {
'value': 0.5,
...
}
event_manager.device_changed(self)
```
def methodName(self):
self.data = {
'value': 0.5,
...
}
event_manager.device_changed(self)
@@ -1,27 +1,24 @@
=========================
Create customized reports
=========================
# Create customized reports
SQL views are a technique for creating customized reports to show data that cannot be
shown with existing models' fields and views. In other words, this technique helps avoid
unnecessary creation and calculation of additional fields solely for data analysis
purposes.
Create a model
==============
## Create a model
A SQL view is created in a similar manner as a standard model:
.. code-block:: python
from odoo import fields, models
```python
from odoo import fields, models
class ModuleReport(models.Model):
_name = 'module.report'
_description = "Module Report"
_rec_name = 'module_field'
_auto = False
class ModuleReport(models.Model):
_name = 'module.report'
_description = "Module Report"
_rec_name = 'module_field'
_auto = False
```
Where the attributes:
@@ -32,11 +29,11 @@ Where the attributes:
and its fields are defined in the same way as a standard model, except every field is
marked as `readonly=True`.
.. note::
Don't forget to add your new model to your security file.
:::{note}
Don't forget to add your new model to your security file.
:::
Populate the model
==================
## Populate the model
There are 2 ways to populate a SQL view's table:
@@ -46,10 +43,11 @@ There are 2 ways to populate a SQL view's table:
Regardless of which way is used, a SQL query will be executed to populate the model.
Therefore, any SQL commands can be used to collect and/or calculate the data needed
and you are expected to keep in mind that you are bypassing the ORM (i.e. it is a
good idea to read through :ref:`reference/security` if you haven't already). The columns
good idea to read through {ref}`reference/security` if you haven't already). The columns
returned from the `SELECT` will populate the model's fields, so ensure that your column
names match your field names, or use alias names that match.
```{eval-rst}
.. tabs::
.. tab:: Overriding `BaseModel.init()`
@@ -98,22 +96,24 @@ names match your field names, or use alias names that match.
.. seealso::
`Example: a SQL view using _table_query
<{GITHUB_PATH}/addons/account/report/account_invoice_report.py>`_
```
Use the model
=============
## Use the model
Views and menu items for your SQL views are created and used in the same way as any
other Odoo model. You are all set to start using your SQL view. Have fun!
Extra tips
==========
## Extra tips
.. tip::
A common mistake in SQL views is not considering the duplication of certain data
due to table JOINs. This can lead to miscounting when using a field's `aggregator`
and/or the pivot view. It is best to test your SQL view with sufficient data to ensure the
resulting field values are as you expect.
:::{tip}
A common mistake in SQL views is not considering the duplication of certain data
due to table JOINs. This can lead to miscounting when using a field's `aggregator`
and/or the pivot view. It is best to test your SQL view with sufficient data to ensure the
resulting field values are as you expect.
:::
:::{tip}
If you have a field that you do not want as a measure (i.e., in your pivot or graph views), add
`store=False` to it, and it will not show.
:::
.. tip::
If you have a field that you do not want as a measure (i.e., in your pivot or graph views), add
`store=False` to it, and it will not show.
@@ -1,12 +1,9 @@
============================================
Use Owl components on the portal and website
============================================
# Use Owl components on the portal and website
In this article, you will learn how you can leverage Owl components on the portal
and website.
Overview
========
## Overview
To use Owl components on the website or portal, you will need to do a few things:
@@ -14,45 +11,42 @@ To use Owl components on the website or portal, you will need to do a few things
- Add that component to the `web.assets_frontend` bundle
- Add an `<owl-component>` tag to a website or portal page to use the component
1. Creating the Owl component
=============================
## 1. Creating the Owl component
To keep things simple, let's start with a very straightforward component that just renders
"Hello, World!". This will let us know at a glance if our setup is working.
First, create the template in :file:`/your_module/static/src/portal_component/your_component.xml`.
First, create the template in {file}`/your_module/static/src/portal_component/your_component.xml`.
.. code-block:: xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="your_module.YourComponent">
Hello, World!
</t>
</templates>
```
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="your_module.YourComponent">
Hello, World!
</t>
</templates>
Then create the JavaScript file for that component in :file:`/your_module/static/src/portal_component/your_component.js`,
Then create the JavaScript file for that component in {file}`/your_module/static/src/portal_component/your_component.js`,
and add it to the `public_components` registry:
.. code-block:: js
```js
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry"
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry"
export class YourComponent extends Component {
static template = "your_module.YourComponent";
static props = {};
}
export class YourComponent extends Component {
static template = "your_module.YourComponent";
static props = {};
}
registry.category("public_components").add("your_module.YourComponent", YourComponent);
```
registry.category("public_components").add("your_module.YourComponent", YourComponent);
:::{seealso}
{ref}`Owl components reference<frontend/components>`.
:::
.. seealso::
:ref:`Owl components reference<frontend/components>`.
2. Adding your component to the `web.assets_frontend` bundle
============================================================
## 2. Adding your component to the `web.assets_frontend` bundle
The `web.assets_frontend` bundle is the assets bundle that is used by the portal and
website, you'll want to add your component's code to that bundle so that the public
@@ -60,54 +54,53 @@ components service can find your component and mount it. In your module's manife
in the assets section, add an entry for `web.assets_frontend` and add your component's
files:
.. code-block:: py
{
# ...
'assets': {
'web.assets_frontend': [
'your_module/static/src/portal_component/**/*',
],
}
```py
{
# ...
'assets': {
'web.assets_frontend': [
'your_module/static/src/portal_component/**/*',
],
}
}
```
.. seealso::
:ref:`Module manifest reference<reference/module/manifest>`.
:::{seealso}
{ref}`Module manifest reference<reference/module/manifest>`.
:::
3. Adding an `<owl-component>` tag to a page
============================================
## 3. Adding an `<owl-component>` tag to a page
Now we need add an `<owl-component>` tag that will serve as the target for the component
to be mounted. For the sake of this example, we'll add it directly to the portal's
home page with an xpath in :file:`/your_module/views/templates.xml`.
home page with an xpath in {file}`/your_module/views/templates.xml`.
.. code-block:: xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="your_module.portal_my_home" inherit_id="portal.portal_my_home">
<xpath expr="//*[hasclass('o_portal_my_home')]" position="before">
<owl-component name="your_module.YourComponent" />
</xpath>
</template>
</odoo>
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="your_module.portal_my_home" inherit_id="portal.portal_my_home">
<xpath expr="//*[hasclass('o_portal_my_home')]" position="before">
<owl-component name="your_module.YourComponent" />
</xpath>
</template>
</odoo>
```
Don't forget to add this file to the data section of your assets bundle:
.. code-block:: py
{
# ...
'data': {
'views/templates.xml',
}
```py
{
# ...
'data': {
'views/templates.xml',
}
}
```
And that's it! If you open the home page of the portal you should see the message
"Hello, World!" at the top of the page.
Points of caution
=================
## Points of caution
Owl components are rendered entirely in JavaScript by the browser. This can cause
some issues:
@@ -118,8 +111,7 @@ some issues:
For these reasons, you should only use Owl components on the portal and website for
specific use cases described below.
Layout shift
------------
### Layout shift
When a page initially renders content, and that content subsequently moves ("shifts")
within the page, this is referred to as a layout shift. When using Owl components in
@@ -138,13 +130,11 @@ various ways, e.g. by positioning it below all other existing elements, not havi
other interactive elements around, or reserving a fixed space for the Owl component
using CSS.
:::{seealso}
[Cumulative Layout Shift on web.dev](https://web.dev/articles/cls)
:::
.. seealso::
`Cumulative Layout Shift on web.dev <https://web.dev/articles/cls>`_
Poorer indexing by search engines
---------------------------------
### Poorer indexing by search engines
When search engines build their index of the content of the web, they use web crawlers
to find pages and analyze their content to show these pages in their
@@ -158,15 +148,13 @@ can have on your search engine scores. While it's unlikely to make or break your
strategy, you should still only use Owl components when they are adding real value
over server-side rendering.
When to use Owl components on the portal and website
====================================================
## When to use Owl components on the portal and website
As explained in the previous sections, using Owl component can slightly degrade user
experience if you're not careful and may also hinder your SEO. So when should you
choose to use Owl components in these places? Here are some general guidelines.
When you don't care about SEO
-----------------------------
### When you don't care about SEO
If a page cannot be indexed by search engines because it's not available to the public,
e.g. anything in the user portal, SEO performance is not a concern, as web crawlers
@@ -175,8 +163,7 @@ care about indexing, e.g. if you want to have a page where the user can choose a
and time for an appointment, you probably don't want search engines to index the dates
on which an appointment is possible at a specific moment in time.
When you need strong interactivity
----------------------------------
### When you need strong interactivity
The decision to use Owl is a trade-off between the previously mentioned disadvantages
and the effort that Owl saves you by making it easy to build richly interactive user
@@ -184,3 +171,4 @@ experiences. The main reason to use Owl is when you want to build an interface t
can react in real time to user inputs without requiring the page to reload. If you
mainly want to show static content to the user, you probably shouldn't use an Owl
component.
@@ -0,0 +1,47 @@
(howtos-javascript-client-action)=
# Create a client action
A client action triggers an action that is entirely implemented in the client side.
One of the benefits of using a client action is the ability to create highly customized interfaces
with ease. A client action is typically defined by an OWL component; we can also use the web
framework and use services, core components, hooks,...
1. Create the {ref}`client action <reference/actions/client>`, don't forget to
make it accessible.
```xml
<record model="ir.actions.client" id="my_client_action">
<field name="name">My Client Action</field>
<field name="tag">my_module.MyClientAction</field>
</record>
```
2. Create a component that represents the client action.
```{code-block} js
:caption: :file:`my_client_action.js`
import { registry } from "@web/core/registry";
import { Component } from "@odoo/owl";
class MyClientAction extends Component {
static template = "my_module.clientaction";
}
// remember the tag name we put in the first step
registry.category("actions").add("my_module.MyClientAction", MyClientAction);
```
```{code-block} xml
:caption: :file:`my_client_action.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_tshirt.clientaction">
Hello world
</t>
</templates>
```
@@ -1,47 +0,0 @@
.. _howtos/javascript_client_action:
======================
Create a client action
======================
A client action triggers an action that is entirely implemented in the client side.
One of the benefits of using a client action is the ability to create highly customized interfaces
with ease. A client action is typically defined by an OWL component; we can also use the web
framework and use services, core components, hooks,...
#. Create the :ref:`client action <reference/actions/client>`, don't forget to
make it accessible.
.. code-block:: xml
<record model="ir.actions.client" id="my_client_action">
<field name="name">My Client Action</field>
<field name="tag">my_module.MyClientAction</field>
</record>
#. Create a component that represents the client action.
.. code-block:: js
:caption: :file:`my_client_action.js`
import { registry } from "@web/core/registry";
import { Component } from "@odoo/owl";
class MyClientAction extends Component {
static template = "my_module.clientaction";
}
// remember the tag name we put in the first step
registry.category("actions").add("my_module.MyClientAction", MyClientAction);
.. code-block:: xml
:caption: :file:`my_client_action.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_tshirt.clientaction">
Hello world
</t>
</templates>
@@ -0,0 +1,102 @@
# Customize a field
## Subclass an existing field component
Let's take an example where we want to extends the `BooleanField` to create a boolean field
displaying "Late!" in red whenever the checkbox is checked.
1. Create a new widget component extending the desired field component.
```{code-block} javascript
:caption: :file:`late_order_boolean_field.js`
import { registry } from "@web/core/registry";
import { BooleanField } from "@web/views/fields/boolean/boolean_field";
import { Component, xml } from "@odoo/owl";
class LateOrderBooleanField extends BooleanField {
static template = "my_module.LateOrderBooleanField";
}
```
2. Create the field template.
The component uses a new template with the name `my_module.LateOrderBooleanField`. Create it by
inheriting the current template of the `BooleanField`.
```{code-block} xml
:caption: :file:`late_order_boolean_field.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="my_module.LateOrderBooleanField" t-inherit="web.BooleanField">
<xpath expr="//CheckBox" position="after">
<span t-if="props.value" class="text-danger"> Late! </span>
</xpath>
</t>
</templates>
```
3. Register the component to the fields registry.
```{code-block}
:caption: :file:`late_order_boolean_field.js`
registry.category("fields").add("late_boolean", LateOrderBooleanField);
```
4. Add the widget in the view arch as an attribute of the field.
```xml
<field name="somefield" widget="late_boolean"/>
```
## Create a new field component
Assume that we want to create a field that displays a simple text in red.
1. Create a new Owl component representing our new field
```{code-block} js
:caption: :file:`my_text_field.js`
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
export class MyTextField extends Component {
static template = xml`
<input t-att-id="props.id" class="text-danger" t-att-value="props.value" onChange.bind="onChange" />
`;
static props = { ...standardFieldProps };
static supportedTypes = ["char"];
/**
* @param {boolean} newValue
*/
onChange(newValue) {
this.props.update(newValue);
}
}
```
The imported `standardFieldProps` contains the standard props passed by the `View` such as
the `update` function to update the value, the `type` of the field in the model, the
`readonly` boolean, and others.
2. In the same file, register the component to the fields registry.
```{code-block} js
:caption: :file:`my_text_field.js`
registry.category("fields").add("my_text_field", MyTextField);
```
This maps the widget name in the arch to its actual component.
3. Add the widget in the view arch as an attribute of the field.
```xml
<field name="somefield" widget="my_text_field"/>
```
@@ -1,101 +0,0 @@
=================
Customize a field
=================
Subclass an existing field component
====================================
Let's take an example where we want to extends the `BooleanField` to create a boolean field
displaying "Late!" in red whenever the checkbox is checked.
#. Create a new widget component extending the desired field component.
.. code-block:: javascript
:caption: :file:`late_order_boolean_field.js`
import { registry } from "@web/core/registry";
import { BooleanField } from "@web/views/fields/boolean/boolean_field";
import { Component, xml } from "@odoo/owl";
class LateOrderBooleanField extends BooleanField {
static template = "my_module.LateOrderBooleanField";
}
#. Create the field template.
The component uses a new template with the name `my_module.LateOrderBooleanField`. Create it by
inheriting the current template of the `BooleanField`.
.. code-block:: xml
:caption: :file:`late_order_boolean_field.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="my_module.LateOrderBooleanField" t-inherit="web.BooleanField">
<xpath expr="//CheckBox" position="after">
<span t-if="props.value" class="text-danger"> Late! </span>
</xpath>
</t>
</templates>
#. Register the component to the fields registry.
.. code-block::
:caption: :file:`late_order_boolean_field.js`
registry.category("fields").add("late_boolean", LateOrderBooleanField);
#. Add the widget in the view arch as an attribute of the field.
.. code-block:: xml
<field name="somefield" widget="late_boolean"/>
Create a new field component
============================
Assume that we want to create a field that displays a simple text in red.
#. Create a new Owl component representing our new field
.. code-block:: js
:caption: :file:`my_text_field.js`
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
export class MyTextField extends Component {
static template = xml`
<input t-att-id="props.id" class="text-danger" t-att-value="props.value" onChange.bind="onChange" />
`;
static props = { ...standardFieldProps };
static supportedTypes = ["char"];
/**
* @param {boolean} newValue
*/
onChange(newValue) {
this.props.update(newValue);
}
}
The imported `standardFieldProps` contains the standard props passed by the `View` such as
the `update` function to update the value, the `type` of the field in the model, the
`readonly` boolean, and others.
#. In the same file, register the component to the fields registry.
.. code-block:: js
:caption: :file:`my_text_field.js`
registry.category("fields").add("my_text_field", MyTextField);
This maps the widget name in the arch to its actual component.
#. Add the widget in the view arch as an attribute of the field.
.. code-block:: xml
<field name="somefield" widget="my_text_field"/>
+264
View File
@@ -0,0 +1,264 @@
# Customize a view type
## Subclass 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 in a few steps:
1. Extend the kanban controller/renderer/model and register it in the view registry.
```{code-block} js
:caption: :file:`custom_kanban_controller.js`
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { kanbanView } from "@web/views/kanban/kanban_view";
import { registry } from "@web/core/registry";
// the controller usually contains the Layout and the renderer.
class CustomKanbanController extends KanbanController {
static template = "my_module.CustomKanbanView";
// Your logic here, override or insert new methods...
// if you override setup(), don't forget to call super.setup()
}
export const customKanbanView = {
...kanbanView, // contains the default Renderer/Controller/Model
Controller: CustomKanbanController,
};
// Register it to the views registry
registry.category("views").add("custom_kanban", customKanbanView);
```
In our custom kanban, we defined a new template. We can either inherit the kanban controller
template and add our template pieces or we can define a completely new template.
```{code-block} xml
:caption: :file:`custom_kanban_controller.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="my_module.CustomKanbanView" t-inherit="web.KanbanView">
<xpath expr="//Layout" position="before">
<div>
Hello world !
</div>
</xpath>
</t>
</templates>
```
2. Use the view with the `js_class` attribute in arch.
```xml
<kanban js_class="custom_kanban">
<templates>
<t t-name="kanban-box">
<!--Your comment-->
</t>
</templates>
</kanban>
```
The possibilities for extending views are endless. While we have only extended the controller
here, you can also extend the renderer to add new buttons, modify how records are presented, or
customize the dropdown, as well as extend other components such as the model and `buttonTemplate`.
## Create a new view from scratch
Creating a new view is an advanced topic. This guide highlight only the essential steps.
1. Create the controller.
> The primary role of a controller is to facilitate the coordination between various components
> of a view, such as the Renderer, Model, and Layout.
```{code-block} js
:caption: :file:`beautiful_controller.js`
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillStart, useState} from "@odoo/owl";
export class BeautifulController extends Component {
static template = "my_module.View";
static components = { Layout };
setup() {
this.orm = useService("orm");
// The controller create the model and make it reactive so whenever this.model is
// accessed and edited then it'll cause a rerendering
this.model = useState(
new this.props.Model(
this.orm,
this.props.resModel,
this.props.fields,
this.props.archInfo,
this.props.domain
)
);
onWillStart(async () => {
await this.model.load();
});
}
}
```
The template of the Controller displays the control panel with Layout and also the
renderer.
```{code-block} xml
:caption: :file:`beautiful_controller.xml`
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.View">
<Layout display="props.display" className="'h-100 overflow-auto'">
<t t-component="props.Renderer" records="model.records" propsYouWant="'Hello world'"/>
</Layout>
</t>
</templates>
```
2. Create the renderer.
> The primary function of a renderer is to generate a visual representation of data by rendering
> the view that includes records.
```{code-block} js
:caption: :file:`beautiful_renderer.js`
import { Component } from "@odoo/owl";
export class BeautifulRenderer extends Component {
static template = "my_module.Renderer";
}
```
```{code-block} xml
:caption: :file:`beautiful_renderer.xml`
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.Renderer">
<t t-esc="props.propsYouWant"/>
<t t-foreach="props.records" t-as="record" t-key="record.id">
// Show records
</t>
</t>
</templates>
```
3. Create the model.
The role of the model is to retrieve and manage all the necessary data in the view.
```{code-block} js
:caption: :file:`beautiful_model.js`
import { KeepLast } from "@web/core/utils/concurrency";
export class BeautifulModel {
constructor(orm, resModel, fields, archInfo, domain) {
this.orm = orm;
this.resModel = resModel;
// We can access arch information parsed by the beautiful arch parser
const { fieldFromTheArch } = archInfo;
this.fieldFromTheArch = fieldFromTheArch;
this.fields = fields;
this.domain = domain;
this.keepLast = new KeepLast();
}
async load() {
// The keeplast protect against concurrency call
const { length, records } = await this.keepLast.add(
this.orm.webSearchRead(this.resModel, this.domain, [this.fieldsFromTheArch], {})
);
this.records = records;
this.recordsLength = length;
}
}
```
:::{note}
For advanced cases, instead of creating a model from scratch, it is also possible to use
`RelationalModel`, which is used by other views.
:::
4. Create the arch parser.
The role of the arch parser is to parse the arch view so the view has access to the information.
```{code-block} js
:caption: :file:`beautiful_arch_parser.js`
import { XMLParser } from "@web/core/utils/xml";
export class BeautifulArchParser extends XMLParser {
parse(arch) {
const xmlDoc = this.parseXML(arch);
const fieldFromTheArch = xmlDoc.getAttribute("fieldFromTheArch");
return {
fieldFromTheArch,
};
}
}
```
5. Create the view and combine all the pieces together, then register the view in the views
registry.
```{code-block} js
:caption: :file:`beautiful_view.js`
import { registry } from "@web/core/registry";
import { BeautifulController } from "./beautiful_controller";
import { BeautifulArchParser } from "./beautiful_arch_parser";
import { BeautifylModel } from "./beautiful_model";
import { BeautifulRenderer } from "./beautiful_renderer";
export const beautifulView = {
type: "beautiful",
display_name: "Beautiful",
icon: "fa fa-picture-o", // the icon that will be displayed in the Layout panel
multiRecord: true,
Controller: BeautifulController,
ArchParser: BeautifulArchParser,
Model: BeautifulModel,
Renderer: BeautifulRenderer,
props(genericProps, view) {
const { ArchParser } = view;
const { arch } = genericProps;
const archInfo = new ArchParser().parse(arch);
return {
...genericProps,
Model: view.Model,
Renderer: view.Renderer,
archInfo,
};
},
};
registry.category("views").add("beautifulView", beautifulView);
```
6. Declare the {ref}`view <reference/view_records/structure>` in the arch.
```xml
...
<record id="my_beautiful_view" model="ir.ui.view">
<field name="name">my_view</field>
<field name="model">my_model</field>
<field name="arch" type="xml">
<beautiful fieldFromTheArch="res.partner"/>
</field>
</record>
...
```
@@ -1,258 +0,0 @@
=====================
Customize a view type
=====================
Subclass 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 in a few steps:
#. Extend the kanban controller/renderer/model and register it in the view registry.
.. code-block:: js
:caption: :file:`custom_kanban_controller.js`
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { kanbanView } from "@web/views/kanban/kanban_view";
import { registry } from "@web/core/registry";
// the controller usually contains the Layout and the renderer.
class CustomKanbanController extends KanbanController {
static template = "my_module.CustomKanbanView";
// Your logic here, override or insert new methods...
// if you override setup(), don't forget to call super.setup()
}
export const customKanbanView = {
...kanbanView, // contains the default Renderer/Controller/Model
Controller: CustomKanbanController,
};
// Register it to the views registry
registry.category("views").add("custom_kanban", customKanbanView);
In our custom kanban, we defined a new template. We can either inherit the kanban controller
template and add our template pieces or we can define a completely new template.
.. code-block:: xml
:caption: :file:`custom_kanban_controller.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="my_module.CustomKanbanView" t-inherit="web.KanbanView">
<xpath expr="//Layout" position="before">
<div>
Hello world !
</div>
</xpath>
</t>
</templates>
#. Use the view with the `js_class` attribute in arch.
.. code-block:: xml
<kanban js_class="custom_kanban">
<templates>
<t t-name="kanban-box">
<!--Your comment-->
</t>
</templates>
</kanban>
The possibilities for extending views are endless. While we have only extended the controller
here, you can also extend the renderer to add new buttons, modify how records are presented, or
customize the dropdown, as well as extend other components such as the model and `buttonTemplate`.
Create a new view from scratch
==============================
Creating a new view is an advanced topic. This guide highlight only the essential steps.
#. Create the controller.
The primary role of a controller is to facilitate the coordination between various components
of a view, such as the Renderer, Model, and Layout.
.. code-block:: js
:caption: :file:`beautiful_controller.js`
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillStart, useState} from "@odoo/owl";
export class BeautifulController extends Component {
static template = "my_module.View";
static components = { Layout };
setup() {
this.orm = useService("orm");
// The controller create the model and make it reactive so whenever this.model is
// accessed and edited then it'll cause a rerendering
this.model = useState(
new this.props.Model(
this.orm,
this.props.resModel,
this.props.fields,
this.props.archInfo,
this.props.domain
)
);
onWillStart(async () => {
await this.model.load();
});
}
}
The template of the Controller displays the control panel with Layout and also the
renderer.
.. code-block:: xml
:caption: :file:`beautiful_controller.xml`
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.View">
<Layout display="props.display" className="'h-100 overflow-auto'">
<t t-component="props.Renderer" records="model.records" propsYouWant="'Hello world'"/>
</Layout>
</t>
</templates>
#. Create the renderer.
The primary function of a renderer is to generate a visual representation of data by rendering
the view that includes records.
.. code-block:: js
:caption: :file:`beautiful_renderer.js`
import { Component } from "@odoo/owl";
export class BeautifulRenderer extends Component {
static template = "my_module.Renderer";
}
.. code-block:: xml
:caption: :file:`beautiful_renderer.xml`
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.Renderer">
<t t-esc="props.propsYouWant"/>
<t t-foreach="props.records" t-as="record" t-key="record.id">
// Show records
</t>
</t>
</templates>
#. Create the model.
The role of the model is to retrieve and manage all the necessary data in the view.
.. code-block:: js
:caption: :file:`beautiful_model.js`
import { KeepLast } from "@web/core/utils/concurrency";
export class BeautifulModel {
constructor(orm, resModel, fields, archInfo, domain) {
this.orm = orm;
this.resModel = resModel;
// We can access arch information parsed by the beautiful arch parser
const { fieldFromTheArch } = archInfo;
this.fieldFromTheArch = fieldFromTheArch;
this.fields = fields;
this.domain = domain;
this.keepLast = new KeepLast();
}
async load() {
// The keeplast protect against concurrency call
const { length, records } = await this.keepLast.add(
this.orm.webSearchRead(this.resModel, this.domain, [this.fieldsFromTheArch], {})
);
this.records = records;
this.recordsLength = length;
}
}
.. note::
For advanced cases, instead of creating a model from scratch, it is also possible to use
`RelationalModel`, which is used by other views.
#. Create the arch parser.
The role of the arch parser is to parse the arch view so the view has access to the information.
.. code-block:: js
:caption: :file:`beautiful_arch_parser.js`
import { XMLParser } from "@web/core/utils/xml";
export class BeautifulArchParser extends XMLParser {
parse(arch) {
const xmlDoc = this.parseXML(arch);
const fieldFromTheArch = xmlDoc.getAttribute("fieldFromTheArch");
return {
fieldFromTheArch,
};
}
}
#. Create the view and combine all the pieces together, then register the view in the views
registry.
.. code-block:: js
:caption: :file:`beautiful_view.js`
import { registry } from "@web/core/registry";
import { BeautifulController } from "./beautiful_controller";
import { BeautifulArchParser } from "./beautiful_arch_parser";
import { BeautifylModel } from "./beautiful_model";
import { BeautifulRenderer } from "./beautiful_renderer";
export const beautifulView = {
type: "beautiful",
display_name: "Beautiful",
icon: "fa fa-picture-o", // the icon that will be displayed in the Layout panel
multiRecord: true,
Controller: BeautifulController,
ArchParser: BeautifulArchParser,
Model: BeautifulModel,
Renderer: BeautifulRenderer,
props(genericProps, view) {
const { ArchParser } = view;
const { arch } = genericProps;
const archInfo = new ArchParser().parse(arch);
return {
...genericProps,
Model: view.Model,
Renderer: view.Renderer,
archInfo,
};
},
};
registry.category("views").add("beautifulView", beautifulView);
#. Declare the :ref:`view <reference/view_records/structure>` in the arch.
.. code-block:: xml
...
<record id="my_beautiful_view" model="ir.ui.view">
<field name="name">my_view</field>
<field name="model">my_model</field>
<field name="arch" type="xml">
<beautiful fieldFromTheArch="res.partner"/>
</field>
</record>
...
@@ -1,6 +1,4 @@
===============================
Write lean easy-to-maintain CSS
===============================
# Write lean easy-to-maintain CSS
There are many ways to lean and simplify SCSS. The first step is to establish if custom code is
needed at all.
@@ -9,29 +7,27 @@ Odoo's webclient has been designed to be modular, meaning that (potentially all)
shared across views. Check the code before creating a new class. Chances are that there is already a
class or an HTML tag doing exactly what you're looking for.
On top of that, Odoo relies on `Bootstrap
<https://getbootstrap.com/docs/5.1/getting-started/introduction/>`_ (BS), one of the most complete
On top of that, Odoo relies on [Bootstrap](https://getbootstrap.com/docs/5.1/getting-started/introduction/) (BS), one of the most complete
CSS frameworks available. The framework has been customized in order to match Odoo's design (both
community and enterprise versions), meaning that you can use any BS class directly in Odoo and
achieve a visual result that is consistent with our UI.
.. warning::
- The fact that a class achieves the desired visual result doesn't necessarily mean that it's the
right one for the job. Be aware of classes triggering JS behaviors, for example.
- Be careful about class semantics. Applying a **button class** to a **title** is not only
semantically wrong, it may also lead to migration issues and visual inconsistencies.
:::{warning}
- The fact that a class achieves the desired visual result doesn't necessarily mean that it's the
right one for the job. Be aware of classes triggering JS behaviors, for example.
- Be careful about class semantics. Applying a **button class** to a **title** is not only
semantically wrong, it may also lead to migration issues and visual inconsistencies.
:::
The following sections describe tips to strip-down SCSS lines **when custom-code is the only way to
go**.
.. _tutorials/scss_tips/browser_defaults:
(tutorials-scss-tips-browser-defaults)=
Browser defaults
================
## Browser defaults
By default, each browser renders content using a *user agent stylesheet*. To overcome
inconsistencies between browsers, some of these rules are overridden by `Bootstrap Reboot
<https://getbootstrap.com/docs/5.1/content/reboot/>`_.
inconsistencies between browsers, some of these rules are overridden by [Bootstrap Reboot](https://getbootstrap.com/docs/5.1/content/reboot/).
At this stage all "browser-specific-decoration" rules have been stripped away, but a big chunk of
rules defining basic layout information is maintained (or reinforced by *Reboot* for consistency
@@ -39,6 +35,7 @@ reasons).
You can rely on these rules.
```{eval-rst}
.. example::
Applying `display: block;` to a `<div/>` is normally not necessary.
@@ -49,7 +46,9 @@ You can rely on these rules.
display: block;
/* not needed 99% of the time */
}
```
```{eval-rst}
.. example::
In this instance, you may opt to switching the HTML tag rather than adding a new CSS rule.
@@ -61,9 +60,11 @@ You can rely on these rules.
/* replace <span> with <div> instead
to get 'display: block' by default */
}
```
Here's a non-comprehensive list of default rules:
```{eval-rst}
.. list-table::
:header-rows: 1
@@ -84,21 +85,25 @@ Here's a non-comprehensive list of default rules:
| `:after {content: close-quote}`
* - ...
- ...
```
.. seealso::
`Bootstrap Reboot on GitHub
<https://github.com/twbs/bootstrap/blob/1a6fdfae6b/scss/_reboot.scss>`_
:::{seealso}
[Bootstrap Reboot on GitHub](https://github.com/twbs/bootstrap/blob/1a6fdfae6b/scss/_reboot.scss)
:::
.. _tutorials/scss_tips/html_tags:
(tutorials-scss-tips-html-tags)=
HTML tags
=========
## HTML tags
It may seem obvious, but the simplest and most **consistent** way of making text look like a title
is to use a header tag (`<h1>`, `<h2>`, ...). Besides reboot rules, mostly all tags carry decorative
styles defined by Odoo.
```{eval-rst}
.. rst-class:: bg-light
```
```{eval-rst}
.. example::
.. container:: alert alert-danger
@@ -158,18 +163,19 @@ styles defined by Odoo.
.o_module_custom_subtitle {
animation: 2s linear 1s mycustomAnimation;
}
```
.. note::
Besides reducing the amount of code, a modular-design approach (use classes, tags, mixins...)
keeps the visual result consistent and easily **maintainable**.
:::{note}
Besides reducing the amount of code, a modular-design approach (use classes, tags, mixins...)
keeps the visual result consistent and easily **maintainable**.
Following the last example, if Odoo titles' design changes, these changes will be applied in the
`o_module_custom_title` element too since it's using an `<h5>` tag.
Following the last example, if Odoo titles' design changes, these changes will be applied in the
`o_module_custom_title` element too since it's using an `<h5>` tag.
:::
.. _tutorials/scss_tips/utility_classes:
(tutorials-scss-tips-utility-classes)=
Utility classes
===============
## Utility classes
Our framework defines a multitude of utility classes designed to cover almost all
layout/design/interaction needs. The simple fact that a class already exists justifies its use over
@@ -177,31 +183,30 @@ custom CSS whenever possible.
Take the example of `position-relative`.
.. code-block:: css
position-relative {
position: relative !important;
}
```css
position-relative {
position: relative !important;
}
```
Since a utility-class is defined, any CSS line with the declaration `position: relative` is
**potentially** redundant.
Odoo relies on the default `Bootstrap utility-classes
<https://getbootstrap.com/docs/5.1/utilities/background/>`_ stack and defines its own using
`Bootstrap API <https://getbootstrap.com/docs/5.1/utilities/api/>`_.
Odoo relies on the default [Bootstrap utility-classes](https://getbootstrap.com/docs/5.1/utilities/background/) stack and defines its own using
[Bootstrap API](https://getbootstrap.com/docs/5.1/utilities/api/).
.. seealso::
- `Bootstrap utility classes <https://getbootstrap.com/docs/5.1/utilities/api/>`_
- `Odoo custom utilities on github
<{GITHUB_PATH}/addons/web/static/src/scss/utilities_custom.scss>`_
:::{seealso}
- [Bootstrap utility classes](https://getbootstrap.com/docs/5.1/utilities/api/)
- [Odoo custom utilities on github]({GITHUB_PATH}/addons/web/static/src/scss/utilities_custom.scss)
:::
.. _tutorials/scss_tips/utility_classes/downside:
(tutorials-scss-tips-utility-classes-downside)=
Handling utility-classes verbosity
----------------------------------
### Handling utility-classes verbosity
The downside of utility-classes is the potential lack of readability.
```{eval-rst}
.. example::
.. code-block:: html
@@ -210,6 +215,7 @@ The downside of utility-classes is the potential lack of readability.
{{props.readonly ? 'o_myComponent_disabled' : ''}}
card d-lg-block position-absolute {{props.active ?
'o_myComponent_active' : ''}} myComponent px-3"/>
```
To overcome the issue you may combine different approaches:
@@ -217,6 +223,7 @@ To overcome the issue you may combine different approaches:
- use new lines for each attribute;
- order classes using the convention `[odoo component] [bootstrap component] [css declaration order]`.
```{eval-rst}
.. example::
.. code-block:: html
@@ -228,6 +235,9 @@ To overcome the issue you may combine different approaches:
}"
class="myComponent card position-absolute d-flex d-lg-block border px-3 px-lg-2"
/>
```
:::{seealso}
{ref}`Odoo CSS properties order <contributing/coding_guidelines/scss/properties_order>`
:::
.. seealso::
:ref:`Odoo CSS properties order <contributing/coding_guidelines/scss/properties_order>`
@@ -0,0 +1,164 @@
# Create a standalone Owl application
For any number of reasons, you may want to have a standalone Owl application that isn't a part of
the web client. One example in Odoo is the self-ordering application, that lets customers order
food from their phone. In this chapter we will go into what's required to achieve something like this.
## Overview
To have a standalone Owl app, a few things are required:
- a root component for the application
- an assets bundle that contains the setup code
- a QWeb view that calls the assets bundle
- a controller that renders the view
## 1. Root component
To keep things simple, let's start with a very straightforward component that just renders
"Hello, World!". This will let us know at a glance if our setup is working.
First, create the template in {file}`/your_module/static/src/standalone_app/root.xml`.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="your_module.Root">
Hello, World!
</t>
</templates>
```
Then create the JavaScript file for that component in {file}`/your_module/static/src/standalone_app/root.js`.
```js
import { Component } from "@odoo/owl";
export class Root extends Component {
static template = "your_module.Root";
static props = {};
}
```
It's generally a good idea to have the application setup code that mounts the component in a separate
file. Create the JavaScript file that will mount the app in {file}`/your_module/static/src/standalone_app/app.js`.
```js
import { whenReady } from "@odoo/owl";
import { mountComponent } from "@web/env";
import { Root } from "./root";
whenReady(() => mountComponent(Root, document.body));
```
The `mountComponent` utility function will take care of creating the Owl application and configuring
it correctly: it will create an environment, start the {ref}`services<frontend/services>`, make sure
the app is translated and give the app access to the templates from your assets bundle, among other
things.
:::{seealso}
{ref}`Owl components reference<frontend/components>`.
:::
## 2. Creating an assets bundle containing our code
In the manifest of your module, create a new {ref}`assets bundle<reference/assets_bundle>`.
It should include the `web._assets_core` bundle, which contains the Odoo JavaScript
framework and the core libraries it needs (e.g. Owl and luxon), after which you can have a
glob that adds all the files for your application in the bundle.
```{code-block} py
:emphasize-lines: 9-10
{
# ...
'assets': {
'your_module.assets_standalone_app': [
('include', 'web._assets_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web._assets_bootstrap'),
('include', 'web._assets_core'),
'your_module/static/src/standalone_app/**/*',
],
}
}
```
The other lines are bundles and scss files that are required to make Bootstrap work. They are
mandatory, as the components of the web framework use bootstrap classes for their styling and
layout.
:::{caution}
Make sure that the files for your standalone app are only added to this bundle, if you already
have a definition for `web.assets_backend` or `web.assets_frontend` and they have globs, make
sure these globs don't match the files for your standalone app, otherwise the startup code for
your app will conflict with the existing startup code in those bundles.
:::
:::{seealso}
{ref}`Module manifest reference<reference/module/manifest>`.
:::
## 3. XML view that calls the assets bundle
Now that we have created our assets bundle, we need to create a
{ref}`QWeb view <reference/view_architectures/qweb>` that uses that assets bundle.
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="your_module.standalone_app">&lt;!DOCTYPE html&gt;
<html>
<head>
<script type="text/javascript">
var odoo = {
csrf_token: "<t t-nocache="The csrf token must always be up to date." t-esc="request.csrf_token(None)"/>",
debug: "<t t-out="debug"/>",
__session_info__: <t t-esc="json.dumps(session_info)"/>,
};
</script>
<t t-call-assets="your_module.assets_standalone_app" />
</head>
<body/>
</html>
</template>
</odoo>
```
This template only does two things: it initializes the `odoo` global variable, then calls the assets
bundle we just defined. Initializing the `odoo` global variable is a necessary step. This variable
should be an object that contains the following:
- The CSRF token, which is required to interact with HTTP controllers in many cases.
- The debug value, which is used in many places to add additional logging or developer-friendly checks.
- `__session_info__`, that contains information from the server that is always needed and for which
we don't want to perform an additional request. More on this in the next section.
## 4. Controller that renders the view
Now that we have the view, we need to make it accessible to the user. For that purpose, we will create
an {ref}`HTTP controller<reference/controllers>` that renders that view and returns it to the user.
```py
from odoo.http import request, route, Controller
class YourController(Controller):
@route("/your_module/standalone_app", auth="public")
def standalone_app(self):
return request.render(
'your_module.standalone_app',
{
'session_info': request.env['ir.http'].get_frontend_session_info(),
}
)
```
Notice how we're giving the template `session_info`. We get it from the `get_frontend_session_info`
method, and it will end up containing information used by the web framework, such as the current
user's ID if they are logged in, the server version, the Odoo edition, etc.
At this point, if you open the url `/your_module/standalone_app` in your brower, you should
see a blank page with the text "Hello, World!". At this point, you can start actually writing the
code for your app.
@@ -1,167 +0,0 @@
===================================
Create a standalone Owl application
===================================
For any number of reasons, you may want to have a standalone Owl application that isn't a part of
the web client. One example in Odoo is the self-ordering application, that lets customers order
food from their phone. In this chapter we will go into what's required to achieve something like this.
Overview
========
To have a standalone Owl app, a few things are required:
- a root component for the application
- an assets bundle that contains the setup code
- a QWeb view that calls the assets bundle
- a controller that renders the view
1. Root component
=================
To keep things simple, let's start with a very straightforward component that just renders
"Hello, World!". This will let us know at a glance if our setup is working.
First, create the template in :file:`/your_module/static/src/standalone_app/root.xml`.
.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="your_module.Root">
Hello, World!
</t>
</templates>
Then create the JavaScript file for that component in :file:`/your_module/static/src/standalone_app/root.js`.
.. code-block:: js
import { Component } from "@odoo/owl";
export class Root extends Component {
static template = "your_module.Root";
static props = {};
}
It's generally a good idea to have the application setup code that mounts the component in a separate
file. Create the JavaScript file that will mount the app in :file:`/your_module/static/src/standalone_app/app.js`.
.. code-block:: js
import { whenReady } from "@odoo/owl";
import { mountComponent } from "@web/env";
import { Root } from "./root";
whenReady(() => mountComponent(Root, document.body));
The `mountComponent` utility function will take care of creating the Owl application and configuring
it correctly: it will create an environment, start the :ref:`services<frontend/services>`, make sure
the app is translated and give the app access to the templates from your assets bundle, among other
things.
.. seealso::
:ref:`Owl components reference<frontend/components>`.
2. Creating an assets bundle containing our code
================================================
In the manifest of your module, create a new :ref:`assets bundle<reference/assets_bundle>`.
It should include the `web._assets_core` bundle, which contains the Odoo JavaScript
framework and the core libraries it needs (e.g. Owl and luxon), after which you can have a
glob that adds all the files for your application in the bundle.
.. code-block:: py
:emphasize-lines: 9-10
{
# ...
'assets': {
'your_module.assets_standalone_app': [
('include', 'web._assets_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web._assets_bootstrap'),
('include', 'web._assets_core'),
'your_module/static/src/standalone_app/**/*',
],
}
}
The other lines are bundles and scss files that are required to make Bootstrap work. They are
mandatory, as the components of the web framework use bootstrap classes for their styling and
layout.
.. caution::
Make sure that the files for your standalone app are only added to this bundle, if you already
have a definition for `web.assets_backend` or `web.assets_frontend` and they have globs, make
sure these globs don't match the files for your standalone app, otherwise the startup code for
your app will conflict with the existing startup code in those bundles.
.. seealso::
:ref:`Module manifest reference<reference/module/manifest>`.
3. XML view that calls the assets bundle
========================================
Now that we have created our assets bundle, we need to create a
:ref:`QWeb view <reference/view_architectures/qweb>` that uses that assets bundle.
.. code-block:: xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="your_module.standalone_app">&lt;!DOCTYPE html&gt;
<html>
<head>
<script type="text/javascript">
var odoo = {
csrf_token: "<t t-nocache="The csrf token must always be up to date." t-esc="request.csrf_token(None)"/>",
debug: "<t t-out="debug"/>",
__session_info__: <t t-esc="json.dumps(session_info)"/>,
};
</script>
<t t-call-assets="your_module.assets_standalone_app" />
</head>
<body/>
</html>
</template>
</odoo>
This template only does two things: it initializes the `odoo` global variable, then calls the assets
bundle we just defined. Initializing the `odoo` global variable is a necessary step. This variable
should be an object that contains the following:
- The CSRF token, which is required to interact with HTTP controllers in many cases.
- The debug value, which is used in many places to add additional logging or developer-friendly checks.
- `__session_info__`, that contains information from the server that is always needed and for which
we don't want to perform an additional request. More on this in the next section.
4. Controller that renders the view
===================================
Now that we have the view, we need to make it accessible to the user. For that purpose, we will create
an :ref:`HTTP controller<reference/controllers>` that renders that view and returns it to the user.
.. code-block:: py
from odoo.http import request, route, Controller
class YourController(Controller):
@route("/your_module/standalone_app", auth="public")
def standalone_app(self):
return request.render(
'your_module.standalone_app',
{
'session_info': request.env['ir.http'].get_frontend_session_info(),
}
)
Notice how we're giving the template `session_info`. We get it from the `get_frontend_session_info`
method, and it will end up containing information used by the web framework, such as the current
user's ID if they are logged in, the server version, the Odoo edition, etc.
At this point, if you open the url `/your_module/standalone_app` in your brower, you should
see a blank page with the text "Hello, World!". At this point, you can start actually writing the
code for your app.
+289
View File
@@ -0,0 +1,289 @@
(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.
```{eval-rst}
.. 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.png
: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.api.Environment._`
and {func}`odoo.tools.translate._`:
```python
title = self.env._("Bank Accounts")
# old API for backward-compatibility
from odoo.tools import _
title = _("Bank Accounts")
```
In JavaScript, the wrapping function is generally {js:func}`odoo.web._t`:
```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 the {class}`odoo.tools.translate.LazyTranslate`
factory 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.
```python
from odoo.tools import LazyTranslate
_lt = LazyTranslate(__name__)
LAZY_TEXT = _lt("some text")
```
:::{note}
Translations of a module are **not** exposed to the front end by default and
thus are not accessible from JavaScript. In order to achieve that, the
module name has to be either prefixed with `website` (just like
`website_sale`, `website_event` etc.) or explicitly register by implementing
{func}`_get_translation_frontend_modules_name` for the `ir.http` model.
This could look like the following:
```
from odoo import models
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
@classmethod
def _get_translation_frontend_modules_name(cls):
modules = super()._get_translation_frontend_modules_name()
return modules + ['your_module']
```
:::
### Context
To translate, the translation function needs to know the *language* and the
*module* name. When using `Environment._` the language is known and you
may pass the module name as a parameter, otherwise it's extracted from the
caller.
In case of `odoo.tools.translate._`, the language and the module are
extracted from the context. For this, we inspect the caller's local variables.
The drawback of this method is that it is error-prone: we try to find the
context variable or `self.env`, however these may not exist if you use
translations outside of model methods; i.e. it does not work inside regular
functions or python comprehensions.
Lazy translations are bound to the module during their creation and the
language is resolved when evaluating using `str()`.
Note that you can also pass a lazy translation to `Envionrment._`
to translate it without any magic language resolution.
### 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 as a parameter of the translation lookup (this
will fallback on source in case of missing placeholder in the translation):
```
_("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 %(count)s invoice", count=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 %(count)s invoices", count=invoice_count)
else:
msg = _("You have one invoice")
```
### 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'),
};
```
[msginit]: https://www.gnu.org/software/gettext/manual/gettext.html#Creating
[po file]: https://en.wikipedia.org/wiki/Gettext#Translating
[poedit]: https://poedit.net/
-268
View File
@@ -1,268 +0,0 @@
.. _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.png
: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.api.Environment._`
and :func:`odoo.tools.translate._`:
.. code-block:: python
title = self.env._("Bank Accounts")
# old API for backward-compatibility
from odoo.tools import _
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 the :class:`odoo.tools.translate.LazyTranslate`
factory 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.
.. code-block:: python
from odoo.tools import LazyTranslate
_lt = LazyTranslate(__name__)
LAZY_TEXT = _lt("some text")
.. note::
Translations of a module are **not** exposed to the front end by default and
thus are not accessible from JavaScript. In order to achieve that, the
module name has to be either prefixed with `website` (just like
`website_sale`, `website_event` etc.) or explicitly register by implementing
:func:`_get_translation_frontend_modules_name` for the `ir.http` model.
This could look like the following::
from odoo import models
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
@classmethod
def _get_translation_frontend_modules_name(cls):
modules = super()._get_translation_frontend_modules_name()
return modules + ['your_module']
Context
-------
To translate, the translation function needs to know the *language* and the
*module* name. When using ``Environment._`` the language is known and you
may pass the module name as a parameter, otherwise it's extracted from the
caller.
In case of ``odoo.tools.translate._``, the language and the module are
extracted from the context. For this, we inspect the caller's local variables.
The drawback of this method is that it is error-prone: we try to find the
context variable or ``self.env``, however these may not exist if you use
translations outside of model methods; i.e. it does not work inside regular
functions or python comprehensions.
Lazy translations are bound to the module during their creation and the
language is resolved when evaluating using ``str()``.
Note that you can also pass a lazy translation to ``Envionrment._``
to translate it without any magic language resolution.
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 as a parameter of the translation lookup (this
will fallback on source in case of missing placeholder in the translation)::
_("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 %(count)s invoice", count=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 %(count)s invoices", count=invoice_count)
else:
msg = _("You have one invoice")
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/
@@ -1,24 +1,22 @@
=============================
Upgrade a customized database
=============================
# Upgrade a customized database
Upgrading to a new version of Odoo can be challenging, especially if the database you work on
contains custom modules. This page intent is to explain the technical process of upgrading a
database with customized modules. Refer to :doc:`Upgrade documentation </administration/upgrade>`
database with customized modules. Refer to {doc}`Upgrade documentation </administration/upgrade>`
for guidance on how to upgrade a database without customized modules.
We consider a custom module, any module that extends the standard code of Odoo and that was not
built with the Studio app. Before upgrading such a module, or before requesting its upgrade, have a
look at the :ref:`upgrade-sla` to make sure who's responsible for it.
look at the {ref}`upgrade-sla` to make sure who's responsible for it.
While working on what we refer to as the **custom upgrade** of your database, keep in mind the goals
of an upgrade:
#. Stay supported
#. Get the latest features
#. Enjoy the performance improvement
#. Reduce the technical debt
#. Benefit from security improvements
1. Stay supported
2. Get the latest features
3. Enjoy the performance improvement
4. Reduce the technical debt
5. Benefit from security improvements
With every new version of Odoo, changes are introduced. These changes can impact modules on which
customization have been developed. This is the reason why upgrading a database that contains custom
@@ -26,17 +24,16 @@ modules requires additional steps in order to upgrade the source code.
These are the steps to follow to upgrade customized databases:
#. :ref:`Stop the developments and challenge them <upgrade_custom/stop_developments>`.
#. :ref:`Request an upgraded database <upgrade_custom/request_upgrade>`.
#. :ref:`Make your module installable on an empty database <upgrade_custom/empty_database>`.
#. :ref:`Make your module installable on the upgraded database <upgrade_custom/upgraded_database>`.
#. :ref:`Test extensively and do a rehearsal <upgrade_custom/testing_rehearsal>`.
#. :ref:`Upgrade the production database <upgrade_custom/production>`.
1. {ref}`Stop the developments and challenge them <upgrade_custom/stop_developments>`.
2. {ref}`Request an upgraded database <upgrade_custom/request_upgrade>`.
3. {ref}`Make your module installable on an empty database <upgrade_custom/empty_database>`.
4. {ref}`Make your module installable on the upgraded database <upgrade_custom/upgraded_database>`.
5. {ref}`Test extensively and do a rehearsal <upgrade_custom/testing_rehearsal>`.
6. {ref}`Upgrade the production database <upgrade_custom/production>`.
.. _upgrade_custom/stop_developments:
(upgrade-custom-stop-developments)=
Step 1: Stop the developments
=============================
## Step 1: Stop the developments
Starting an upgrade requires commitment and development resources. If developments keep being made
at the same time, those features will need to be re-upgraded and tested every time you change them.
@@ -49,30 +46,28 @@ Challenge the developments as much as possible and find functional workarounds.
between your developments and the standard version of Odoo will lead to an eased upgrade process
and reduce technical debt.
.. note::
You can find information on the changes between versions in the `Release Notes
<https:/odoo.com/page/release-notes>`_.
:::{note}
You can find information on the changes between versions in the [Release Notes](https:/odoo.com/page/release-notes).
:::
.. _upgrade_custom/request_upgrade:
(upgrade-custom-request-upgrade)=
Step 2: Request an upgraded database
====================================
## Step 2: Request an upgraded database
Once the developments have stopped for the custom modules and the implemented features have been
challenged to remove redundancy and unnecessary code, the next step is to request an upgraded test
database. To do so, follow the steps mentioned in :ref:`upgrade-request-test`, depending on the
database. To do so, follow the steps mentioned in {ref}`upgrade-request-test`, depending on the
hosting type of your database.
The purpose of this stage is not to start working with the custom modules in the upgraded database,
but to make sure the standard upgrade process works seamlessly, and the test database is delivered
properly. If that's not the case, and the upgrade request fails, request the assistance of Odoo via
the `support page <https://odoo.com/help?stage=migration>`_ by selecting the option related to
the [support page](https://odoo.com/help?stage=migration) by selecting the option related to
testing the upgrade.
.. _upgrade_custom/empty_database:
(upgrade-custom-empty-database)=
Step 3: Empty database
======================
## Step 3: Empty database
Before working on an upgraded test database, we recommend to make the custom developments work on an
empty database in the targeted version of your upgrade. This ensures that the customization is
@@ -86,15 +81,14 @@ the custom modules and that can raise unwanted issues in this stage of the upgra
To make custom modules work on an empty database we advise to follow these steps:
#. :ref:`upgrade_custom/empty_database/modules_installable`
#. :ref:`upgrade_custom/empty_database/test_fixes`
#. :ref:`upgrade_custom/empty_database/clean_code`
#. :ref:`Make standard tests run successfully <upgrade_custom/empty_database/standard_test>`
1. {ref}`upgrade_custom/empty_database/modules_installable`
2. {ref}`upgrade_custom/empty_database/test_fixes`
3. {ref}`upgrade_custom/empty_database/clean_code`
4. {ref}`Make standard tests run successfully <upgrade_custom/empty_database/standard_test>`
.. _upgrade_custom/empty_database/modules_installable:
(upgrade-custom-empty-database-modules-installable)=
Make custom modules installable
-------------------------------
### Make custom modules installable
The first step is to make the custom modules installable in the new Odoo version.
This means, starting by ensuring there are no tracebacks or warnings during their installation.
@@ -104,16 +98,20 @@ fix the tracebacks and warnings that arise from that.
This process will help detect issues during the installation of the modules. For example:
- Invalid module dependencies.
- Syntax change: assets declaration, OWL updates, attrs.
- References to standard fields, models, views not existing anymore or renamed.
- Xpath that moved or were removed from views.
- Methods renamed or removed.
- ...
.. _upgrade_custom/empty_database/test_fixes:
(upgrade-custom-empty-database-test-fixes)=
Test and fixes
--------------
### Test and fixes
Once there are no more tracebacks when installing the modules, the next step is to test them.
Even if the custom modules are installable on an empty database, this does not guarantee there are
@@ -138,24 +136,22 @@ test coverage, and ensure that the changes and fixes introduced do not break the
If there are tests already implemented in the customization, make sure they are upgraded to the new
Odoo version and run successfully, fixing issues that might be present.
.. _upgrade_custom/empty_database/clean_code:
(upgrade-custom-empty-database-clean-code)=
Clean the code
--------------
### Clean the code
At this stage of the upgrade process, we also suggest to clean the code as much as possible.
This includes:
- Remove redundant and unnecessary code.
- Remove features that are now part of Odoo standard, as described in
:ref:`upgrade_custom/stop_developments`.
{ref}`upgrade_custom/stop_developments`.
- Clean commented code if it is not needed anymore.
- Refactor the code (functions, fields, views, reports, etc.) if needed.
.. _upgrade_custom/empty_database/standard_test:
(upgrade-custom-empty-database-standard-test)=
Standard tests
--------------
### Standard tests
Once the previous steps are completed, we advise to make sure all standard tests associated to the
dependencies of the custom module pass.
@@ -168,72 +164,72 @@ In case there are standard test failing, we suggest to analyze the reason for th
- The customization did not take into account a special flow: Adapt your customization to ensure it
works for all the standard workflows.
(upgrade-custom-upgraded-database)=
.. _upgrade_custom/upgraded_database:
Step 4: Upgraded database
=========================
## Step 4: Upgraded database
Once the custom modules are installable and working properly in an empty database, it is time to
make them work on an :ref:`upgraded database <upgrade-request-test>`.
make them work on an {ref}`upgraded database <upgrade-request-test>`.
To make sure the custom code is working flawlessly in the new version, follow these steps:
- :ref:`upgrade_custom/upgraded_database/migrate_data`
- :ref:`upgrade_custom/upgraded_database/test_custom`
- {ref}`upgrade_custom/upgraded_database/migrate_data`
- {ref}`upgrade_custom/upgraded_database/test_custom`
.. _upgrade_custom/upgraded_database/migrate_data:
(upgrade-custom-upgraded-database-migrate-data)=
Migrate the data
----------------
### Migrate the data
During the upgrade of the custom modules, you might have to use :doc:`upgrade scripts
During the upgrade of the custom modules, you might have to use {doc}`upgrade scripts
<../reference/upgrades/upgrade_scripts>` to reflect changes from the source code to their
corresponding data. Together with the upgrade scripts, you can also make use of the
:doc:`../reference/upgrades/upgrade_utils` and its helper functions.
{doc}`../reference/upgrades/upgrade_utils` and its helper functions.
- Any technical data that was renamed during the upgrade of the custom code (models, fields,
external identifiers) should be renamed using upgrade scripts to avoid data loss during the
module upgrade. See also: :meth:`rename_field`, :meth:`rename_model`, :meth:`rename_xmlid`.
module upgrade. See also: {meth}`rename_field`, {meth}`rename_model`, {meth}`rename_xmlid`.
- Data from standard models removed from the source code of the newer Odoo version and from the
database during the standard upgrade process might need to be recovered from the old model table
if it is still present.
.. example::
Custom fields for model ``sale.subscription`` are not automatically migrated from Odoo 15 to
Odoo 16 (when the model was merged into ``sale.order``). In this case, a SQL query can be
executed on an upgrade script to move the data from one table to the other. Take into account
that all columns/fields must already exist, so consider doing this in a ``post-`` script (See
:ref:`upgrade-scripts/phases`).
.. spoiler::
.. code-block:: python
def migrate(cr, version):
cr.execute(
"""
UPDATE sale_order so
SET custom_field = ss.custom_field
FROM sale_subscription ss
WHERE ss.new_sale_order_id = so.id
"""
)
Check the documentation for more information on :doc:`../reference/upgrades/upgrade_scripts`.
> ```{eval-rst}
> .. example::
> Custom fields for model ``sale.subscription`` are not automatically migrated from Odoo 15 to
> Odoo 16 (when the model was merged into ``sale.order``). In this case, a SQL query can be
> executed on an upgrade script to move the data from one table to the other. Take into account
> that all columns/fields must already exist, so consider doing this in a ``post-`` script (See
> :ref:`upgrade-scripts/phases`).
>
> .. spoiler::
>
> .. code-block:: python
>
> def migrate(cr, version):
> cr.execute(
> """
> UPDATE sale_order so
> SET custom_field = ss.custom_field
> FROM sale_subscription ss
> WHERE ss.new_sale_order_id = so.id
> """
> )
>
> Check the documentation for more information on :doc:`../reference/upgrades/upgrade_scripts`.
> ```
Upgrade scripts can also be used to:
- Ease the processing time of an upgrade. For example, to store the value of computed stored fields
on models with an excessive number of records by using SQL queries.
- Recompute fields in case the computation of their value has changed. See also
:meth:`recompute_fields`.
- Uninstall unwanted custom modules. See also :meth:`remove_module`.
{meth}`recompute_fields`.
- Uninstall unwanted custom modules. See also {meth}`remove_module`.
- Correct faulty data or wrong configurations.
Running and testing upgrade scripts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#### Running and testing upgrade scripts
```{eval-rst}
.. tabs::
.. group-tab:: Odoo Online
@@ -264,11 +260,11 @@ Running and testing upgrade scripts
.. important::
As mentioned in the :doc:`CLI documentation </developer/reference/cli>`, the command used
to call the CLI depends on how you installed Odoo.
```
.. _upgrade_custom/upgraded_database/test_custom:
(upgrade-custom-upgraded-database-test-custom)=
Test the custom modules
-----------------------
### Test the custom modules
To make sure the custom modules work properly with your data in the upgraded database, they need to
be tested as well. This helps ensure both the standard and the custom data stored in the database
@@ -280,22 +276,21 @@ Things to pay attention to:
disabled. You can find the information on disabled views on the Upgrade report. This view needs to
be activated again (or removed if not useful anymore). To achieve this, we recommend the use of
upgrade scripts.
- :doc:`Module data <../tutorials/define_module_data>` not updated: Custom records that have the
``noupdate`` flag are not updated when upgrading the module in the new database. For the custom
- {doc}`Module data <../tutorials/define_module_data>` not updated: Custom records that have the
`noupdate` flag are not updated when upgrading the module in the new database. For the custom
data that needs to be updated due to changes in the new version, we recommend to use upgrade
scripts to do so. See also: :meth:`update_record_from_xml`.
scripts to do so. See also: {meth}`update_record_from_xml`.
.. _upgrade_custom/testing_rehearsal:
(upgrade-custom-testing-rehearsal)=
Step 5: Testing and rehearsal
=============================
## Step 5: Testing and rehearsal
When the custom modules are working properly in the upgraded database, it is crucial to do another
round of testing to assess the database usability and detect any issues that might have gone
unnoticed in previous tests. For further information about testing the upgraded database, check
:ref:`upgrade-testing`.
{ref}`upgrade-testing`.
As mentioned in :ref:`upgrade-production`, both standard upgrade scripts and your database are
As mentioned in {ref}`upgrade-production`, both standard upgrade scripts and your database are
constantly evolving. Therefore it is highly recommended to frequently request new upgraded test
databases and ensure that the upgrade process is still successful.
@@ -303,10 +298,10 @@ In addition to that, make a full rehearsal of the upgrade process the day before
production database to avoid undesired behavior during the upgrade and to detect any issue that
might have occurred with the migrated data.
.. _upgrade_custom/production:
(upgrade-custom-production)=
Step 6: Production upgrade
==========================
## Step 6: Production upgrade
Once you are confident about upgrading your production database, follow the process described on
:ref:`upgrade-production`, depending on the hosting type of your database.
{ref}`upgrade-production`, depending on the hosting type of your database.
+152
View File
@@ -0,0 +1,152 @@
# Web Services
The web-service module offers a common interface for all web services:
- XML-RPC
- JSON-RPC
Business objects can also be accessed via the distributed object
mechanism. They can all be modified via the client interface with contextual
views.
Odoo is accessible through XML-RPC/JSON-RPC interfaces, for which libraries
exist in many languages.
## XML-RPC Library
The following example is a Python 3 program that interacts with an Odoo
server with the library `xmlrpc.client`:
```
import xmlrpc.client
root = 'http://%s:%d/xmlrpc/' % (HOST, PORT)
uid = xmlrpc.client.ServerProxy(root + 'common').login(DB, USER, PASS)
print("Logged in as %s (uid: %d)" % (USER, uid))
# Create a new note
sock = xmlrpc.client.ServerProxy(root + 'object')
args = {
'color' : 8,
'memo' : 'This is a note',
'create_uid': uid,
}
note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args)
```
```{eval-rst}
.. exercise:: Add a new service to the client
Write a Python program able to send XML-RPC requests to a PC running
Odoo (yours, or your instructor's). This program should display all
the sessions, and their corresponding number of seats. It should also
create a new session for one of the courses.
.. only:: solutions
.. code-block:: python
import functools
import xmlrpc.client
HOST = 'localhost'
PORT = 8069
DB = 'openacademy'
USER = 'admin'
PASS = 'admin'
ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT)
# 1. Login
uid = xmlrpc.client.ServerProxy(ROOT + 'common').login(DB,USER,PASS)
print("Logged in as %s (uid:%d)" % (USER,uid))
call = functools.partial(
xmlrpc.client.ServerProxy(ROOT + 'object').execute,
DB, uid, PASS)
# 2. Read the sessions
sessions = call('openacademy.session','search_read', [], ['name','seats'])
for session in sessions:
print("Session %s (%s seats)" % (session['name'], session['seats']))
# 3.create a new session
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : 2,
})
Instead of using a hard-coded course id, the code can look up a course
by name::
# 3.create a new session for the "Functional" course
course_id = call('openacademy.course', 'search', [('name','ilike','Functional')])[0]
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : course_id,
})
```
:::{seealso}
- {doc}`../reference/external_api`: The in-depth tutorial on XML-RPC, with examples spanning multiple programming languages.
:::
## JSON-RPC Library
The following example is a Python 3 program that interacts with an Odoo server
with the standard Python libraries `urllib.request` and `json`. This
example assumes the **Productivity** app (`note`) is installed:
```
import json
import random
import urllib.request
HOST = 'localhost'
PORT = 8069
DB = 'openacademy'
USER = 'admin'
PASS = 'admin'
def json_rpc(url, method, params):
data = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": random.randint(0, 1000000000),
}
req = urllib.request.Request(url=url, data=json.dumps(data).encode(), headers={
"Content-Type":"application/json",
})
reply = json.loads(urllib.request.urlopen(req).read().decode('UTF-8'))
if reply.get("error"):
raise Exception(reply["error"])
return reply["result"]
def call(url, service, method, *args):
return json_rpc(url, "call", {"service": service, "method": method, "args": args})
# log in the given database
url = "http://%s:%s/jsonrpc" % (HOST, PORT)
uid = call(url, "common", "login", DB, USER, PASS)
# create a new note
args = {
'color': 8,
'memo': 'This is another note',
'create_uid': uid,
}
note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args)
```
Examples can be easily adapted from XML-RPC to JSON-RPC.
:::{note}
There are a number of high-level APIs in various languages to access Odoo
systems without *explicitly* going through XML-RPC or JSON-RPC, such as:
- <https://github.com/akretion/ooor>
- <https://github.com/OCA/odoorpc>
- <https://github.com/nicolas-van/openerp-client-lib>
- <http://pythonhosted.org/OdooRPC>
- <https://github.com/abhishek-jaiswal/php-openerp-lib>
:::
-148
View File
@@ -1,148 +0,0 @@
============
Web Services
============
The web-service module offers a common interface for all web services:
- XML-RPC
- JSON-RPC
Business objects can also be accessed via the distributed object
mechanism. They can all be modified via the client interface with contextual
views.
Odoo is accessible through XML-RPC/JSON-RPC interfaces, for which libraries
exist in many languages.
XML-RPC Library
---------------
The following example is a Python 3 program that interacts with an Odoo
server with the library ``xmlrpc.client``::
import xmlrpc.client
root = 'http://%s:%d/xmlrpc/' % (HOST, PORT)
uid = xmlrpc.client.ServerProxy(root + 'common').login(DB, USER, PASS)
print("Logged in as %s (uid: %d)" % (USER, uid))
# Create a new note
sock = xmlrpc.client.ServerProxy(root + 'object')
args = {
'color' : 8,
'memo' : 'This is a note',
'create_uid': uid,
}
note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args)
.. exercise:: Add a new service to the client
Write a Python program able to send XML-RPC requests to a PC running
Odoo (yours, or your instructor's). This program should display all
the sessions, and their corresponding number of seats. It should also
create a new session for one of the courses.
.. only:: solutions
.. code-block:: python
import functools
import xmlrpc.client
HOST = 'localhost'
PORT = 8069
DB = 'openacademy'
USER = 'admin'
PASS = 'admin'
ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT)
# 1. Login
uid = xmlrpc.client.ServerProxy(ROOT + 'common').login(DB,USER,PASS)
print("Logged in as %s (uid:%d)" % (USER,uid))
call = functools.partial(
xmlrpc.client.ServerProxy(ROOT + 'object').execute,
DB, uid, PASS)
# 2. Read the sessions
sessions = call('openacademy.session','search_read', [], ['name','seats'])
for session in sessions:
print("Session %s (%s seats)" % (session['name'], session['seats']))
# 3.create a new session
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : 2,
})
Instead of using a hard-coded course id, the code can look up a course
by name::
# 3.create a new session for the "Functional" course
course_id = call('openacademy.course', 'search', [('name','ilike','Functional')])[0]
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : course_id,
})
.. seealso::
- :doc:`../reference/external_api`: The in-depth tutorial on XML-RPC, with examples spanning multiple programming languages.
JSON-RPC Library
----------------
The following example is a Python 3 program that interacts with an Odoo server
with the standard Python libraries ``urllib.request`` and ``json``. This
example assumes the **Productivity** app (``note``) is installed::
import json
import random
import urllib.request
HOST = 'localhost'
PORT = 8069
DB = 'openacademy'
USER = 'admin'
PASS = 'admin'
def json_rpc(url, method, params):
data = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": random.randint(0, 1000000000),
}
req = urllib.request.Request(url=url, data=json.dumps(data).encode(), headers={
"Content-Type":"application/json",
})
reply = json.loads(urllib.request.urlopen(req).read().decode('UTF-8'))
if reply.get("error"):
raise Exception(reply["error"])
return reply["result"]
def call(url, service, method, *args):
return json_rpc(url, "call", {"service": service, "method": method, "args": args})
# log in the given database
url = "http://%s:%s/jsonrpc" % (HOST, PORT)
uid = call(url, "common", "login", DB, USER, PASS)
# create a new note
args = {
'color': 8,
'memo': 'This is another note',
'create_uid': uid,
}
note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args)
Examples can be easily adapted from XML-RPC to JSON-RPC.
.. note::
There are a number of high-level APIs in various languages to access Odoo
systems without *explicitly* going through XML-RPC or JSON-RPC, such as:
* https://github.com/akretion/ooor
* https://github.com/OCA/odoorpc
* https://github.com/nicolas-van/openerp-client-lib
* http://pythonhosted.org/OdooRPC
* https://github.com/abhishek-jaiswal/php-openerp-lib
@@ -1,14 +1,15 @@
:show-content:
:hide-page-toc:
:show-toc:
---
hide-page-toc: true
show-content: true
show-toc: true
---
==============
Website themes
==============
# Website themes
.. image:: website_themes/web-design.png
:alt: Artistic illustration of "Web design"
:width: 600
```{image} website_themes/web-design.png
:alt: Artistic illustration of "Web design"
:width: 600
```
The Odoo Website Builder is an excellent tool for creating a website fully integrated with other
Odoo apps. Using the theme's options and building blocks is easy and allows you to personalize your
@@ -20,18 +21,20 @@ core files, and this way, preserve the editing options of the Website Builder.
The information compiled in this documentation is based on our past experiences - both failures and
successes. We invite you to use it as a base to build your own website and adapt it to your needs.
.. toctree::
:maxdepth: 2
```{toctree}
:maxdepth: 2
website_themes/setup
website_themes/theming
website_themes/layout
website_themes/navigation
website_themes/pages
website_themes/building_blocks
website_themes/shapes
website_themes/gradients
website_themes/animations
website_themes/forms
website_themes/translations
website_themes/going_live
```
website_themes/setup
website_themes/theming
website_themes/layout
website_themes/navigation
website_themes/pages
website_themes/building_blocks
website_themes/shapes
website_themes/gradients
website_themes/animations
website_themes/forms
website_themes/translations
website_themes/going_live
@@ -1,11 +1,8 @@
==========
Animations
==========
# Animations
Eye-catching animations can bring your website to life.
On appearance
=============
## On appearance
In standard, you can add animations to columns when they appear, thanks to the Website Builder. Odoo
detects when your element is in the viewport and launches the animation. A large selection of
@@ -29,16 +26,18 @@ attribute.
**Use**
.. code-block:: xml
```xml
<div class="col-lg-6 o_animate o_anim_fade_in o_animate_both_scroll" style="animation-duration: 2s !important; animation-delay: 1s !important;">
<h2>A Section Subtitle</h2>
<p>Write one or two paragraphs describing your product or services.</p>
</div>
```
<div class="col-lg-6 o_animate o_anim_fade_in o_animate_both_scroll" style="animation-duration: 2s !important; animation-delay: 1s !important;">
<h2>A Section Subtitle</h2>
<p>Write one or two paragraphs describing your product or services.</p>
</div>
```{image} animations/animations.png
:alt: Animation options
```
.. image:: animations/animations.png
:alt: Animation options
:::{seealso}
[Website Animate](https://github.com/odoo/odoo/blob/34c0c9c1ae00aba391932129d4cefd027a9c6bbd/addons/website/static/src/scss/website.scss#L1638)
:::
.. seealso::
`Website Animate
<https://github.com/odoo/odoo/blob/34c0c9c1ae00aba391932129d4cefd027a9c6bbd/addons/website/static/src/scss/website.scss#L1638>`_
@@ -0,0 +1,511 @@
# Building blocks
Building blocks, also known as snippets, are how users design and layout pages. They are important
XML elements of your design.
The building blocks are classified into four categories:
1. **Structure blocks**: to give a basic structure to the website
2. **Feature blocks**: to describe the features of a product or service
3. **Dynamic Content blocks**: blocks that are animated or interact with the backend
4. **Inner Content blocks**: blocks used inside other building blocks
In this chapter, you will learn how to create custom building blocks and options.
## File structure
The layout's file structure is the following.
```
views
├── snippets
│ └── options.xml
│ └── s_snippet_name.xml
```
The styles' file structure is the following.
```
static
├── src
│ └── snippets
│ └── options.scss
│ └── s_snippet_name
│ └── 000.js
│ └── 000.scss
│ └── 000.xml
│ └── option.js
```
:::{seealso}
[XML templates of the different snippets]({GITHUB_PATH}/addons/website/views/snippets/snippets.xml)
:::
:::{admonition} Demo page
<http://localhost:8069/website/demo/snippets>
:::
## Layout
Snippets are editable by the user using the Website Builder. Some Bootstrap classes are important as
**they trigger some Website Builder options**.
### Wrapper
The standard main container of any snippet is a `section`. Any section element can be edited like a
block of content that you can move or duplicate.
```xml
<section class="s_snippet_name" data-name="..." data-snippet="...">
<!-- Content -->
</section>
```
For inner content snippets, any other HTML tag can be used.
```xml
<div class="s_snippet_name" data-name="..." data-snippet="...">
<!-- Content -->
</div>
```
```{eval-rst}
.. todo:: Missing description in table ...
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - class
- Unique class name for this snippet
* - data-name
- Displayed in the right panel as the name of the snippet. If not found, it will fall back to
*Block*.
* - data-snippet
- Used by the system to identify the snippet
```
The system automatically adds the `data-name` and `data-snippet` attributes during the drag and
drop based on the template's name.
:::{warning}
Those attributes should be specifically added when a snippet is declared on a theme page.
:::
:::{warning}
Avoid adding a `section` tag inside another `section` tag: this will trigger twice the Website
Builder's options. You can use inner content snippets instead.
:::
### Columns
Any large Bootstrap columns directly descending from a `.row` element (respecting Bootstrap
structure) will be triggered by the Website Builder to make them resizable.
```css
.row > .col-lg-*
```
Add padding on columns and sections.
```xml
class="pt80 pb80"
```
Add a background based on the color palette for columns and sections.
```xml
class="o_cc o_cc*"
```
Make an element not editable.
```xml
<div class="o_not_editable">
```
Enable the columns selector.
```xml
<div class="container s_allow_columns">
```
Disable the columns option.
```xml
<div class="row s_nb_column_fixed">
```
Disable the size option of all child columns.
```xml
<div class="row s_col_no_resize">
```
Disable the size option for one column.
```xml
<div class="col-lg-* s_col_no_resize">
```
Disable the background color option for all columns.
```xml
<div class="row s_col_no_bgcolor">
```
Disable the background color option of one column.
```xml
<div class="col-lg-* s_col_no_bgcolor">
```
Add parallax effect.
```xml
<section class="parallax s_parallax_is_fixed s_parallax_no_overflow_hidden" data-scroll-background-ratio="1">
<span class="s_parallax_bg oe_img_bg o_bg_img_center" style="background-image: url('...'); background-position: 50% 75%;"/>
<div class="container">
<!-- Content -->
</div>
</section>
```
Add a black color filter with an opacity of 50%.
```xml
<section>
<div class="o_we_bg_filter bg-black-50"/>
<div class="container">
<!-- Content -->
</div>
</section>
```
Add a white color filter with an opacity of 85%.
```xml
<section>
<div class="o_we_bg_filter bg-white-85"/>
<div class="container">
<!-- Content -->
</div>
</section>
```
Add a custom color filter.
```xml
<section>
<div class="o_we_bg_filter" style="background-color: rgba(39, 110, 114, 0.54) !important;"/>
<div class="container">
<!-- Content -->
</div>
</section>
```
Add a custom gradient filter.
```xml
<section>
<div class="o_we_bg_filter" style="background-image: linear-gradient(135deg, rgba(255, 204, 51, 0.5) 0%, rgba(226, 51, 255, 0.5) 100%) !important;"/>
<div class="container">
<!-- Content -->
</div>
</section>
```
## Styles
### Compatibility system
When a snippet has a `data-vcss` or `data-vjs` attribute, it means it is an updated version, not the
original one.
```xml
<section class="s_snippet_name" data-vcss="..." data-js="...">
<!-- Content -->
</section>
```
The `data-vcss` and `data-js` attributes indicate to the system which file version to load for that
snippet (e.g., {file}`001.js`, {file}`002.scss`).
## Custom
Create the snippet's content.
**Declaration**
```{code-block} xml
:caption: '``/website_airproof/views/snippets/s_airproof_snippet.xml``'
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="s_airproof_snippet" name="...">
<section class="s_airproof_snippet">
<!-- Content -->
</section>
</template>
</odoo>
```
:::{warning}
`data-name` and `data-snippet` attributes have to be specified when a snippet is declared on a
theme page.
:::
:::{tip}
- Use Bootstrap native classes as much as possible.
- Prefix all your custom classes.
- Use underscore lowercase notation to name classes, e.g., `.x_nav`, `.x_nav_item`.
- Avoid using ID tag.
:::
Add your custom snippet to the list of default snippets, so the user can drag and drop it on the
page, directly from the edit panel.
```{code-block} xml
:caption: '``/website_airproof/views/snippets/options.xml``'
<template id="snippets" inherit_id="website.snippets" name="Custom Snippets">
<xpath expr="//*[@id='default_snippets']" position="before">
<t id="x_theme_snippets">
<div id="x_theme_snippets_category" class="o_panel">
<div class="o_panel_header">Theme</div>
<div class="o_panel_body">
<t t-snippet="website_airproof.s_airproof_snippet" t-thumbnail="/website_airproof/static/src/img/wbuilder/s_airproof_snippet.svg">
<keywords>Snippet</keywords>
</t>
</div>
</div>
</t>
</xpath>
</template>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - t-snippet
- The template to use
* - t-thumbnail
- The path to the snippet thumbnail
```
### Options
Options allow users to edit a snippet's appearance using the Website Builder. You can create
snippet options easily and automatically add them to the Website Builder.
### Groups properties
Options are wrapped in groups. Groups can have properties that define how the included options
interact with the user interface.
`data-selector` binds all the options included in the group to a particular element. It can be used
in combination with `data-target` and `data-exclude`.
```xml
<div data-selector="section, h1, .custom_class, #custom_id">
```
`data-js` binds custom JavaScript methods.
```xml
<div data-js="CustomMethodName" data-selector="...">
```
`data-drop-in` defines the list of elements where the snippet can be dropped into.
```{eval-rst}
.. todo:: no css selector ...
```
```xml
<div data-selector="..." data-drop-in="...">
```
`data-drop-near` defines the list of elements where the snippet can be dropped beside.
```xml
<div data-selector="..." data-drop-near="...">
```
### SCSS options
Options can apply standard or custom CSS classes to the snippet. Depending on the method that you
choose, the user interface will behave differently.
`data-select-class="..."`
More `data-select-class` in the same group defines a list of classes the user can apply. Only one
option can be enabled at a time.
```{code-block} xml
:caption: '``/website_airproof/views/snippets/options.xml``'
<template id="snippet_options" inherit_id="website.snippet_options" name="...">
<xpath expr="." position="inside">
<div data-selector="h1, h2, h3, h4, h5, h6">
<we-select string="Headings">
<we-button data-select-class="">Default</we-button>
<we-button data-select-class="x_custom_class_01">01</we-button>
<we-button data-select-class="x_custom_class_02">02</we-button>
</we-select>
</div>
</xpath>
</template>
```
:::{seealso}
[XML templates of the different snippets]({GITHUB_PATH}/addons/website/views/snippets/snippets.xml)
:::
### JavaScript options
The `data-js` attribute can be assigned to an options group in order to define a custom method.
```javascript
import options from 'web_editor.snippets.options';
options.registry.CustomMethodName = options.Class.extend({
//
});
```
The Website Builder provides several events you can use to trigger your custom functions.
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Event
- Description
* - start
- Occurs when the publisher selects the snippet for the first time in an editing session or
when the snippet is drag-and-dropped on the page.
* - onFocus
- Occurs each time the snippet is selected by the user or when the snippet is drag-and-dropped
on the page.
* - onBlur
- Occurs when a snippet loses focus.
* - onClone
- Occurs just after a snippet is duplicated.
* - onRemove
- Occurs just before the snippet is removed.
* - onBuilt
- Occurs just after the snippet is drag-and-dropped on a drop zone. When this event is
triggered, the content is already inserted in the page.
* - cleanForSave
- Occurs before the publisher saves the page.
```
### Dynamic Content templates
By default, Dynamic Content blocks have a selection of templates available in the Website Builder.
You can also add your own template to the list.
#### Blog posts
```{code-block} xml
:caption: '``/website_airproof/views/snippets/options.xml``'
<template id="dynamic_filter_template_blog_post_airproof" name="...">
<div t-foreach="records" t-as="data" class="s_blog_posts_post">
<t t-set="record" t-value="data['_record']"/>
<!-- Content -->
</div>
</template>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - id
- The ID of the template. Has to start with `dynamic_filter_template_blog_post_`
* - name
- Human-readable name of the template
```
#### Products
```{code-block} xml
:caption: '``/website_airproof/views/snippets/options.xml``'
<template id="dynamic_filter_template_product_product_airproof" name="...">
<t t-foreach="records" t-as="data" data-number-of-elements="4" data-number-of-elements-sm="1" data-number-of-elements-fetch="8">
<t t-set="record" t-value="data['_record']"/>
<!-- Content -->
</t>
</template>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 40 60
* - Attribute
- Description
* - id
- The ID of the template. Has to start with `dynamic_filter_template_product_product_`
* - name
- Human-readable name of the template
* - data-number-of-elements
- Number of products per slide on desktop
* - data-number-of-elements-sm
- Number of products per slide on mobile
* - data-number-of-elements-fetch
- The total amount of fetched products
```
#### Events
```{code-block} xml
:caption: '``/website_airproof/views/snippets/options.xml``'
<template id="dynamic_filter_template_event_event_airproof" name="...">
<div t-foreach="records" t-as="data" class="s_events_event">
<t t-set="record" t-value="data['_record']._set_tz_context()"/>
<!-- Content -->
</div>
</template>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - id
- The ID of the template. Has to start with `dynamic_filter_template_event_event_`
* - name
- Human-readable name of the template
```
@@ -1,501 +0,0 @@
===============
Building blocks
===============
Building blocks, also known as snippets, are how users design and layout pages. They are important
XML elements of your design.
The building blocks are classified into four categories:
#. **Structure blocks**: to give a basic structure to the website
#. **Feature blocks**: to describe the features of a product or service
#. **Dynamic Content blocks**: blocks that are animated or interact with the backend
#. **Inner Content blocks**: blocks used inside other building blocks
In this chapter, you will learn how to create custom building blocks and options.
File structure
==============
The layout's file structure is the following.
::
views
├── snippets
│ └── options.xml
│ └── s_snippet_name.xml
The styles' file structure is the following.
::
static
├── src
│ └── snippets
│ └── options.scss
│ └── s_snippet_name
│ └── 000.js
│ └── 000.scss
│ └── 000.xml
│ └── option.js
.. seealso::
`XML templates of the different snippets
<{GITHUB_PATH}/addons/website/views/snippets/snippets.xml>`_
.. admonition:: Demo page
http://localhost:8069/website/demo/snippets
Layout
======
Snippets are editable by the user using the Website Builder. Some Bootstrap classes are important as
**they trigger some Website Builder options**.
Wrapper
-------
The standard main container of any snippet is a `section`. Any section element can be edited like a
block of content that you can move or duplicate.
.. code-block:: xml
<section class="s_snippet_name" data-name="..." data-snippet="...">
<!-- Content -->
</section>
For inner content snippets, any other HTML tag can be used.
.. code-block:: xml
<div class="s_snippet_name" data-name="..." data-snippet="...">
<!-- Content -->
</div>
.. todo:: Missing description in table ...
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - class
- Unique class name for this snippet
* - data-name
- Displayed in the right panel as the name of the snippet. If not found, it will fall back to
*Block*.
* - data-snippet
- Used by the system to identify the snippet
The system automatically adds the `data-name` and `data-snippet` attributes during the drag and
drop based on the template's name.
.. warning::
Those attributes should be specifically added when a snippet is declared on a theme page.
.. warning::
Avoid adding a `section` tag inside another `section` tag: this will trigger twice the Website
Builder's options. You can use inner content snippets instead.
Columns
-------
Any large Bootstrap columns directly descending from a `.row` element (respecting Bootstrap
structure) will be triggered by the Website Builder to make them resizable.
.. code-block:: css
.row > .col-lg-*
Add padding on columns and sections.
.. code-block:: xml
class="pt80 pb80"
Add a background based on the color palette for columns and sections.
.. code-block:: xml
class="o_cc o_cc*"
Make an element not editable.
.. code-block:: xml
<div class="o_not_editable">
Enable the columns selector.
.. code-block:: xml
<div class="container s_allow_columns">
Disable the columns option.
.. code-block:: xml
<div class="row s_nb_column_fixed">
Disable the size option of all child columns.
.. code-block:: xml
<div class="row s_col_no_resize">
Disable the size option for one column.
.. code-block:: xml
<div class="col-lg-* s_col_no_resize">
Disable the background color option for all columns.
.. code-block:: xml
<div class="row s_col_no_bgcolor">
Disable the background color option of one column.
.. code-block:: xml
<div class="col-lg-* s_col_no_bgcolor">
Add parallax effect.
.. code-block:: xml
<section class="parallax s_parallax_is_fixed s_parallax_no_overflow_hidden" data-scroll-background-ratio="1">
<span class="s_parallax_bg oe_img_bg o_bg_img_center" style="background-image: url('...'); background-position: 50% 75%;"/>
<div class="container">
<!-- Content -->
</div>
</section>
Add a black color filter with an opacity of 50%.
.. code-block:: xml
<section>
<div class="o_we_bg_filter bg-black-50"/>
<div class="container">
<!-- Content -->
</div>
</section>
Add a white color filter with an opacity of 85%.
.. code-block:: xml
<section>
<div class="o_we_bg_filter bg-white-85"/>
<div class="container">
<!-- Content -->
</div>
</section>
Add a custom color filter.
.. code-block:: xml
<section>
<div class="o_we_bg_filter" style="background-color: rgba(39, 110, 114, 0.54) !important;"/>
<div class="container">
<!-- Content -->
</div>
</section>
Add a custom gradient filter.
.. code-block:: xml
<section>
<div class="o_we_bg_filter" style="background-image: linear-gradient(135deg, rgba(255, 204, 51, 0.5) 0%, rgba(226, 51, 255, 0.5) 100%) !important;"/>
<div class="container">
<!-- Content -->
</div>
</section>
Styles
======
Compatibility system
--------------------
When a snippet has a `data-vcss` or `data-vjs` attribute, it means it is an updated version, not the
original one.
.. code-block:: xml
<section class="s_snippet_name" data-vcss="..." data-js="...">
<!-- Content -->
</section>
The `data-vcss` and `data-js` attributes indicate to the system which file version to load for that
snippet (e.g., :file:`001.js`, :file:`002.scss`).
Custom
======
Create the snippet's content.
**Declaration**
.. code-block:: xml
:caption: ``/website_airproof/views/snippets/s_airproof_snippet.xml``
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="s_airproof_snippet" name="...">
<section class="s_airproof_snippet">
<!-- Content -->
</section>
</template>
</odoo>
.. warning::
`data-name` and `data-snippet` attributes have to be specified when a snippet is declared on a
theme page.
.. tip::
- Use Bootstrap native classes as much as possible.
- Prefix all your custom classes.
- Use underscore lowercase notation to name classes, e.g., `.x_nav`, `.x_nav_item`.
- Avoid using ID tag.
Add your custom snippet to the list of default snippets, so the user can drag and drop it on the
page, directly from the edit panel.
.. code-block:: xml
:caption: ``/website_airproof/views/snippets/options.xml``
<template id="snippets" inherit_id="website.snippets" name="Custom Snippets">
<xpath expr="//*[@id='default_snippets']" position="before">
<t id="x_theme_snippets">
<div id="x_theme_snippets_category" class="o_panel">
<div class="o_panel_header">Theme</div>
<div class="o_panel_body">
<t t-snippet="website_airproof.s_airproof_snippet" t-thumbnail="/website_airproof/static/src/img/wbuilder/s_airproof_snippet.svg">
<keywords>Snippet</keywords>
</t>
</div>
</div>
</t>
</xpath>
</template>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - t-snippet
- The template to use
* - t-thumbnail
- The path to the snippet thumbnail
Options
-------
Options allow users to edit a snippet's appearance using the Website Builder. You can create
snippet options easily and automatically add them to the Website Builder.
Groups properties
-----------------
Options are wrapped in groups. Groups can have properties that define how the included options
interact with the user interface.
`data-selector` binds all the options included in the group to a particular element. It can be used
in combination with `data-target` and `data-exclude`.
.. code-block:: xml
<div data-selector="section, h1, .custom_class, #custom_id">
`data-js` binds custom JavaScript methods.
.. code-block:: xml
<div data-js="CustomMethodName" data-selector="...">
`data-drop-in` defines the list of elements where the snippet can be dropped into.
.. todo:: no css selector ...
.. code-block:: xml
<div data-selector="..." data-drop-in="...">
`data-drop-near` defines the list of elements where the snippet can be dropped beside.
.. code-block:: xml
<div data-selector="..." data-drop-near="...">
SCSS options
------------
Options can apply standard or custom CSS classes to the snippet. Depending on the method that you
choose, the user interface will behave differently.
`data-select-class="..."`
More `data-select-class` in the same group defines a list of classes the user can apply. Only one
option can be enabled at a time.
.. code-block:: xml
:caption: ``/website_airproof/views/snippets/options.xml``
<template id="snippet_options" inherit_id="website.snippet_options" name="...">
<xpath expr="." position="inside">
<div data-selector="h1, h2, h3, h4, h5, h6">
<we-select string="Headings">
<we-button data-select-class="">Default</we-button>
<we-button data-select-class="x_custom_class_01">01</we-button>
<we-button data-select-class="x_custom_class_02">02</we-button>
</we-select>
</div>
</xpath>
</template>
.. seealso::
`XML templates of the different snippets
<{GITHUB_PATH}/addons/website/views/snippets/snippets.xml>`_
JavaScript options
------------------
The `data-js` attribute can be assigned to an options group in order to define a custom method.
.. code-block:: javascript
import options from 'web_editor.snippets.options';
options.registry.CustomMethodName = options.Class.extend({
//
});
The Website Builder provides several events you can use to trigger your custom functions.
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Event
- Description
* - start
- Occurs when the publisher selects the snippet for the first time in an editing session or
when the snippet is drag-and-dropped on the page.
* - onFocus
- Occurs each time the snippet is selected by the user or when the snippet is drag-and-dropped
on the page.
* - onBlur
- Occurs when a snippet loses focus.
* - onClone
- Occurs just after a snippet is duplicated.
* - onRemove
- Occurs just before the snippet is removed.
* - onBuilt
- Occurs just after the snippet is drag-and-dropped on a drop zone. When this event is
triggered, the content is already inserted in the page.
* - cleanForSave
- Occurs before the publisher saves the page.
Dynamic Content templates
-------------------------
By default, Dynamic Content blocks have a selection of templates available in the Website Builder.
You can also add your own template to the list.
Blog posts
~~~~~~~~~~
.. code-block:: xml
:caption: ``/website_airproof/views/snippets/options.xml``
<template id="dynamic_filter_template_blog_post_airproof" name="...">
<div t-foreach="records" t-as="data" class="s_blog_posts_post">
<t t-set="record" t-value="data['_record']"/>
<!-- Content -->
</div>
</template>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - id
- The ID of the template. Has to start with `dynamic_filter_template_blog_post_`
* - name
- Human-readable name of the template
Products
~~~~~~~~
.. code-block:: xml
:caption: ``/website_airproof/views/snippets/options.xml``
<template id="dynamic_filter_template_product_product_airproof" name="...">
<t t-foreach="records" t-as="data" data-number-of-elements="4" data-number-of-elements-sm="1" data-number-of-elements-fetch="8">
<t t-set="record" t-value="data['_record']"/>
<!-- Content -->
</t>
</template>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 40 60
* - Attribute
- Description
* - id
- The ID of the template. Has to start with `dynamic_filter_template_product_product_`
* - name
- Human-readable name of the template
* - data-number-of-elements
- Number of products per slide on desktop
* - data-number-of-elements-sm
- Number of products per slide on mobile
* - data-number-of-elements-fetch
- The total amount of fetched products
Events
~~~~~~
.. code-block:: xml
:caption: ``/website_airproof/views/snippets/options.xml``
<template id="dynamic_filter_template_event_event_airproof" name="...">
<div t-foreach="records" t-as="data" class="s_events_event">
<t t-set="record" t-value="data['_record']._set_tz_context()"/>
<!-- Content -->
</div>
</template>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - id
- The ID of the template. Has to start with `dynamic_filter_template_event_event_`
* - name
- Human-readable name of the template
@@ -0,0 +1,130 @@
# Forms
Forms in Odoo are very powerful. They are directly integrated with other applications and can be
used for many different purposes.
In this chapter, you will discover how to:
- Add a form in your custom theme.
- Change the action of the form.
- Stylize the form thanks to Bootstrap variables.
## Default form
To add a form to your page, you can simply copy and paste the code generated by the Website Builder
in your view.
It should look something like the following.
```xml
<form action="/website/form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-pre-fill="true" data-success-mode="redirect" data-success-page="/contactus-thank-you" data-model_name="mail.mail">
<div class="s_website_form_rows row s_col_no_bgcolor">
<div class="form-group s_website_form_field col-12 s_website_form_dnone" data-name="Field">
<!-- form fields -->
</div>
</div>
</form>
```
## Actions
There is a `data-model_name` in the form tag. It enables you to define different actions for your
form.
Send an email (this action is used by default).
```xml
<form data-model_name="mail.mail">
```
Apply for a job.
```xml
<form data-model_name="hr.applicant">
```
Create a customer.
```xml
<form data-model_name="res.partner">
```
Create a ticket.
```xml
<form data-model_name="helpdesk.ticket">
```
Create an opportunity.
```xml
<form data-model_name="crm.lead">
```
Create a task.
```xml
<form data-model_name="project.task">
```
## Success
You can also define what happens once the form is submitted thanks to the `data-success-mode`.
Redirect the user to a page defined in the `data-success-page`.
```xml
<form data-success-mode="redirect" data-success-page="/contactus-thank-you">
```
Show a message (on the same page).
```xml
<form data-success-mode="message">
```
You can add your success message directly under the form tag. Always add the `d-none` class to make
sure that your success message is hidden if the form hasn't been submitted.
```xml
<div class="s_website_form_end_message d-none">
<div class="oe_structure">
<section class="s_text_block pt64 pb64" data-snippet="s_text_block">
<div class="container">
<h2 class="text-center">This is a success!</h2>
</div>
</section>
</div>
</div>
```
## Bootstrap variables
As you already know, the Website Builder creates content based on Bootstrap. This is also true for
forms. Below you can find a selection of Bootstrap variables, or check out the [full list of
variables](https://github.com/twbs/bootstrap/blob/main/scss/_variables.scss).
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/bootstrap_overridden.scss``'
$input-padding-y: $input-btn-padding-y !default;
$input-padding-x: $input-btn-padding-x !default;
$input-font-family: $input-btn-font-family !default;
$input-font-size: $input-btn-font-size !default;
$input-font-weight: $font-weight-base !default;
$input-line-height: $input-btn-line-height !default;
$input-color: $gray-700 !default;
$input-border-color: $gray-400 !default;
$input-border-width: $input-btn-border-width !default;
$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;
$input-border-radius: $border-radius !default;
$input-focus-bg: $input-bg !default;
$input-focus-border-color: lighten($component-active-bg, 25%) !default;
$input-focus-color: $input-color !default;
$input-focus-width: $input-btn-focus-width !default;
$input-focus-box-shadow: $input-btn-focus-box-shadow !default;
```
@@ -1,134 +0,0 @@
=====
Forms
=====
Forms in Odoo are very powerful. They are directly integrated with other applications and can be
used for many different purposes.
In this chapter, you will discover how to:
- Add a form in your custom theme.
- Change the action of the form.
- Stylize the form thanks to Bootstrap variables.
Default form
============
To add a form to your page, you can simply copy and paste the code generated by the Website Builder
in your view.
It should look something like the following.
.. code-block:: xml
<form action="/website/form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-pre-fill="true" data-success-mode="redirect" data-success-page="/contactus-thank-you" data-model_name="mail.mail">
<div class="s_website_form_rows row s_col_no_bgcolor">
<div class="form-group s_website_form_field col-12 s_website_form_dnone" data-name="Field">
<!-- form fields -->
</div>
</div>
</form>
Actions
=======
There is a `data-model_name` in the form tag. It enables you to define different actions for your
form.
Send an email (this action is used by default).
.. code-block:: xml
<form data-model_name="mail.mail">
Apply for a job.
.. code-block:: xml
<form data-model_name="hr.applicant">
Create a customer.
.. code-block:: xml
<form data-model_name="res.partner">
Create a ticket.
.. code-block:: xml
<form data-model_name="helpdesk.ticket">
Create an opportunity.
.. code-block:: xml
<form data-model_name="crm.lead">
Create a task.
.. code-block:: xml
<form data-model_name="project.task">
Success
=======
You can also define what happens once the form is submitted thanks to the `data-success-mode`.
Redirect the user to a page defined in the `data-success-page`.
.. code-block:: xml
<form data-success-mode="redirect" data-success-page="/contactus-thank-you">
Show a message (on the same page).
.. code-block:: xml
<form data-success-mode="message">
You can add your success message directly under the form tag. Always add the `d-none` class to make
sure that your success message is hidden if the form hasn't been submitted.
.. code-block:: xml
<div class="s_website_form_end_message d-none">
<div class="oe_structure">
<section class="s_text_block pt64 pb64" data-snippet="s_text_block">
<div class="container">
<h2 class="text-center">This is a success!</h2>
</div>
</section>
</div>
</div>
Bootstrap variables
===================
As you already know, the Website Builder creates content based on Bootstrap. This is also true for
forms. Below you can find a selection of Bootstrap variables, or check out the `full list of
variables <https://github.com/twbs/bootstrap/blob/main/scss/_variables.scss>`_.
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/bootstrap_overridden.scss``
$input-padding-y: $input-btn-padding-y !default;
$input-padding-x: $input-btn-padding-x !default;
$input-font-family: $input-btn-font-family !default;
$input-font-size: $input-btn-font-size !default;
$input-font-weight: $font-weight-base !default;
$input-line-height: $input-btn-line-height !default;
$input-color: $gray-700 !default;
$input-border-color: $gray-400 !default;
$input-border-width: $input-btn-border-width !default;
$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;
$input-border-radius: $border-radius !default;
$input-focus-bg: $input-bg !default;
$input-focus-border-color: lighten($component-active-bg, 25%) !default;
$input-focus-color: $input-color !default;
$input-focus-width: $input-btn-focus-width !default;
$input-focus-box-shadow: $input-btn-focus-box-shadow !default;
@@ -0,0 +1,39 @@
# Going live
Once you have finished all the web design and development work, it's time to deploy it on a
development or production database.
## Module import
### Odoo SaaS
Follow these steps the first time you import a module:
1. Create a ZIP file of your module.
2. Connect to the project database.
3. Enable the {ref}`developer mode <developer-mode>`.
4. Go to {guilabel}`Apps`, search for `base_import_module`, and install it if necessary.
5. Click on {guilabel}`Import Module` in the menu.
6. Upload your ZIP file, tick {guilabel}`Force init`, and click the {guilabel}`Import App` button.
If you need to re-import a module after making some changes, follow the same steps, but before
importing the module, open the developer menu and select {guilabel}`Become Superuser`. To leave the
Superuser mode, log out and log back in.
:::{warning}
The ZIP file size must be less than 50 MB.
:::
:::{seealso}
- [Odoo eLearning: Register a Free Domain Name](https://www.odoo.com/slides/slide/register-a-free-domain-name-1663)
:::
### Odoo.sh
Go to {guilabel}`Apps` and click on {guilabel}`Update Apps List` in the menu. Search for your module
in the list and install it.
:::{seealso}
{doc}`Introduction to Odoo.sh <../../../administration/odoo_sh/overview/introduction>`
:::
@@ -1,40 +0,0 @@
==========
Going live
==========
Once you have finished all the web design and development work, it's time to deploy it on a
development or production database.
Module import
=============
Odoo SaaS
---------
Follow these steps the first time you import a module:
#. Create a ZIP file of your module.
#. Connect to the project database.
#. Enable the :ref:`developer mode <developer-mode>`.
#. Go to :guilabel:`Apps`, search for `base_import_module`, and install it if necessary.
#. Click on :guilabel:`Import Module` in the menu.
#. Upload your ZIP file, tick :guilabel:`Force init`, and click the :guilabel:`Import App` button.
If you need to re-import a module after making some changes, follow the same steps, but before
importing the module, open the developer menu and select :guilabel:`Become Superuser`. To leave the
Superuser mode, log out and log back in.
.. warning::
The ZIP file size must be less than 50 MB.
.. seealso::
- `Odoo eLearning: Register a Free Domain Name <https://www.odoo.com/slides/slide/register-a-free-domain-name-1663>`_
Odoo.sh
-------
Go to :guilabel:`Apps` and click on :guilabel:`Update Apps List` in the menu. Search for your module
in the list and install it.
.. seealso::
:doc:`Introduction to Odoo.sh <../../../administration/odoo_sh/overview/introduction>`
@@ -0,0 +1,50 @@
# Gradients
In this chapter, you will discover how to:
- Add a gradient to a section or a title.
- Add your own gradient to the Website Builder palette.
## Standard
In standard, you can select several gradients directly from the Website Builder. However, for custom
themes, you must add the gradients directly in the section tag with a style attribute.
**Use**
```xml
<section class="s_text_image" data-snippet="s_text_image" data-name="Text - Image" style="background-image: linear-gradient(135deg, rgb(255, 204, 51) 0%, rgb(226, 51, 255) 100%) !important;">
<!-- Content -->
</section>
```
To apply a gradient to text, use a font tag with the `text-gradient` class.
```xml
<h2>
<font class="text-gradient" style="background-image: linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%);">A Section Subtitle</font>
</h2>
```
## Custom
You can also add your own custom gradients to the Website Builder. This way, the user can easily
use them without manually recreating them.
```{code-block} xml
:caption: '``/website_airproof/data/presets.xml``'
<record id="colorpicker" model="ir.ui.view">
<field name="key">website_airproof.colorpicker</field>
<field name="name">Custom Gradients</field>
<field name="type">qweb</field>
<field name="inherit_id" ref="web_editor.colorpicker"/>
<field name="website_id">1</field>
<field name="arch" type="xml">
<xpath expr="//*[@data-name='predefined_gradients']/*" position="before">
<button class="w-50 o_we_color_btn" style="background-image: linear-gradient(145deg, rgb(5, 85, 94) 0%, rgb(0, 131, 148) 100%);" data-color="linear-gradient(145deg, rgb(5, 85, 94) 0%, rgb(0, 131, 148) 100%)"></button>
</xpath>
</field>
</record>
```
@@ -1,52 +0,0 @@
=========
Gradients
=========
In this chapter, you will discover how to:
- Add a gradient to a section or a title.
- Add your own gradient to the Website Builder palette.
Standard
========
In standard, you can select several gradients directly from the Website Builder. However, for custom
themes, you must add the gradients directly in the section tag with a style attribute.
**Use**
.. code-block:: xml
<section class="s_text_image" data-snippet="s_text_image" data-name="Text - Image" style="background-image: linear-gradient(135deg, rgb(255, 204, 51) 0%, rgb(226, 51, 255) 100%) !important;">
<!-- Content -->
</section>
To apply a gradient to text, use a font tag with the `text-gradient` class.
.. code-block:: xml
<h2>
<font class="text-gradient" style="background-image: linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%);">A Section Subtitle</font>
</h2>
Custom
======
You can also add your own custom gradients to the Website Builder. This way, the user can easily
use them without manually recreating them.
.. code-block:: xml
:caption: ``/website_airproof/data/presets.xml``
<record id="colorpicker" model="ir.ui.view">
<field name="key">website_airproof.colorpicker</field>
<field name="name">Custom Gradients</field>
<field name="type">qweb</field>
<field name="inherit_id" ref="web_editor.colorpicker"/>
<field name="website_id">1</field>
<field name="arch" type="xml">
<xpath expr="//*[@data-name='predefined_gradients']/*" position="before">
<button class="w-50 o_we_color_btn" style="background-image: linear-gradient(145deg, rgb(5, 85, 94) 0%, rgb(0, 131, 148) 100%);" data-color="linear-gradient(145deg, rgb(5, 85, 94) 0%, rgb(0, 131, 148) 100%)"></button>
</xpath>
</field>
</record>
@@ -0,0 +1,633 @@
# Layout
In this chapter, you will learn how to:
- Create a custom header.
- Create a custom footer.
- Modify a standard template.
- Add a copyright section.
- Improve your website's responsiveness.
## Default
An Odoo page combines cross-page and unique elements. Cross-page elements are the same on every
page, while unique elements are only related to a specific page. By default, a page has two
cross-page elements, the header and the footer, and a unique main element that contains the specific
content of that page.
```xml
<div id="wrapwrap">
<header/>
<main>
<div id="wrap" class="oe_structure">
<!-- Page Content -->
</div>
</main>
<footer/>
</div>
```
Any Odoo XML file starts with encoding specifications. After that, you must write your code inside
an `<odoo>` tag.
```xml
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
...
</odoo>
```
:::{note}
Using precise file names is important to find information through all modules quickly. File names
should only contain lowercase alphanumerics and underscores.
Always add an empty line at the end of your file. This can be done automatically by configuring
your IDE.
:::
## XPath
XPath (XML Path Language) is an expression language that enables you to navigate through elements
and attributes in an XML document easily. XPath is used to extend standard Odoo templates.
A view is coded the following way.
```xml
<template id="..." inherit_id="..." name="...">
<!-- Content -->
</template>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - id
- ID of the modified view
* - inherited_id
- ID of the standard view
* - name
- Human-readable name of the modified view
```
For each XPath, you modify two attributes: **expression** and **position**.
```{eval-rst}
.. example::
.. code-block:: xml
:caption: ``/website_airproof/views/website_templates.xml``
<template id="layout" inherit_id="website.layout" name="Welcome Message">
<xpath expr="//header" position="before">
<!-- Content -->
</xpath>
</template>
This XPath adds a welcome message right before the page content.
```
:::{warning}
Be careful when replacing default elements' attributes. As your theme extends the default one,
your changes will take priority over any future Odoo update.
:::
:::{note}
- You should update your module every time you create a new template or record.
- *XML IDs* of inheriting views should use the same *ID* as the original record. It helps to find
all inheritance at a glance. As final *XML IDs* are prefixed by the module that creates them,
there is no overlap.
:::
### Expressions
XPath uses path expressions to select nodes in an XML document. Selectors are used inside the
expression to target the right element. The most useful ones are listed below.
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Descendent selectors
- Description
* - /
- Selects from the root node.
* - //
- Selects nodes in the document from the current node that matches the selection no matter
where they are.
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute selectors
- Description
* - \*
- Selects any XML tag. `*` can be replaced by a specific tag if the selection needs to be
more precise.
* - \*[@id="id"]
- Selects a specific ID.
* - \*[hasclass("class")]
- Selects a specific class.
* - \*[@name="name"]
- Selects a tag with a specific name.
* - \*[@t-call="t-call"]
- Selects a specific t-call.
```
### Position
The position defines where the code is placed inside the template. The possible values are listed
below:
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Position
- Description
* - replace
- Replaces the targeted node with the XPath content.
* - inside
- Adds the XPath content inside the targeted node.
* - before
- Adds the XPath content before the targeted node.
* - after
- Adds the XPath content after the targeted node.
* - attributes
- Adds the XPath content inside an attribute.
```
```{eval-rst}
.. example::
This XPath adds a `<div>` before the `<nav>` that is a direct child of the `<header>`.
.. code-block:: xml
<xpath expr="//header/nav" position="before">
<div>Some content before the header</div>
</xpath>
This XPath adds `x_airproof_header` in the class attribute of the header. You also need to define
a `separator` attribute to add a space before the class you are adding.
.. code-block:: xml
<xpath expr="//header" position="attributes">
<attribute name="class" add="x_airproof_header" separator=" "/>
</xpath>
This XPath removes `x_airproof_header` in the class attribute of the header. In this case, you
don't need to use the `separator` attribute.
.. code-block:: xml
<xpath expr="//header" position="attributes">
<attribute name="class" remove="x_airproof_header" />
</xpath>
This XPath removes the first element with a `.breadcrumb` class.
.. code-block:: xml
<xpath expr="//*[hasclass('breadcrumb')]" position="replace"/>
This XPath adds an extra `<li>` element after the last child of the `<ul>` element.
.. code-block:: xml
<xpath expr="//ul" position="inside">
<li>Last element of the list</li>
</xpath>
```
:::{seealso}
You can find more information about XPath in this [cheat sheet](https://devhints.io/xpath).
:::
## QWeb
QWeb is the primary templating engine used by Odoo. It is an XML templating engine mainly used to
generate HTML fragments and pages.
:::{seealso}
{doc}`QWeb templates documentation <../../reference/frontend/qweb>`.
:::
## Background
You can define a color or an image as the background of your website.
**Colors**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-color-palettes: map-merge($o-color-palettes,
(
'airproof': (
'o-cc1-bg': 'o-color-5',
'o-cc5-bg': 'o-color-1',
),
)
);
```
**Image/pattern**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-website-values-palettes: (
(
'body-image': '/website_airproof/static/src/img/background-lines.svg',
'body-image-type': 'image' or 'pattern'
)
);
```
## Header
By default, the header contains a responsive navigation menu and the company's logo. You can easily
add new elements or create your own template.
### Standard
Enable one of the header default templates.
:::{important}
Don't forget that you may need to disable the active header template first.
:::
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-website-values-palettes: (
(
'header-template': 'Contact',
),
);
```
```{code-block} xml
:caption: '``/website_airproof/data/presets.xml``'
<record id="website.template_header_contact" model="ir.ui.view">
<field name="active" eval="True"/>
</record>
```
### Custom
Create your own template and add it to the list.
:::{important}
Don't forget that you may need to disable the active header template first.
:::
**Option**
Use the following code to add an option for your new custom header on the Website Builder.
```{code-block} xml
:caption: '``/website_airproof/data/presets.xml``'
<template id="template_header_opt" inherit_id="website.snippet_options" name="Header Template - Option">
<xpath expr="//we-select[@data-variable='header-template']" position="inside">
<we-button title="airproof"
data-customize-website-views="website_airproof.header"
data-customize-website-variable="'airproof'" data-img="/website_airproof/static/src/img/wbuilder/template_header_opt.svg"/>
</xpath>
</template>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - data-customize-website-views
- The template to enable
* - data-customize-website-variable
- The name given to the variable
* - data-img
- The thumbnail of the custom template shown in the templates selection on the Website Builder
```
Now you have to explicitly define that you want to use your custom template in the Odoo SASS
variables.
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-website-values-palettes: (
(
'header-template': 'airproof',
),
);
```
**Layout**
```{code-block} xml
:caption: '``/website_airproof/views/website_templates.xml``'
<record id="header" model="ir.ui.view">
<field name="name">Airproof Header</field>
<field name="type">qweb</field>
<field name="key">website_airproof.header</field>
<field name="inherit_id" ref="website.layout"/>
<field name="mode">extension</field>
<field name="arch" type="xml">
<xpath expr="//header//nav" position="replace">
<!-- Static Content -->
<!-- Components -->
<!-- Editable areas -->
</xpath>
</field>
</record>
```
### Components
In your custom header, you can call several sub-templates using the `t-call` directive from QWeb:
#### Logo
```xml
<t t-call="website.placeholder_header_brand">
<t t-set="_link_class" t-valuef="..."/>
</t>
```
Don't forget to record the logo of your website in the database.
```{code-block} xml
:caption: '``/website_airproof/data/images.xml``'
<record id="website.default_website" model="website">
<field name="logo" type="base64" file="website_airproof/static/src/img/content/logo.png"/>
</record>
```
#### Menu
```xml
<t t-foreach="website.menu_id.child_id" t-as="submenu">
<t t-call="website.submenu">
<t t-set="item_class" t-valuef="nav-item"/>
<t t-set="link_class" t-valuef="nav-link"/>
</t>
</t>
```
#### Sign in
```xml
<t t-call="portal.placeholder_user_sign_in">
<t t-set="_item_class" t-valuef="nav-item"/>
<t t-set="_link_class" t-valuef="nav-link"/>
</t>
```
#### User dropdown
```xml
<t t-call="portal.user_dropdown">
<t t-set="_user_name" t-value="true"/>
<t t-set="_icon" t-value="false"/>
<t t-set="_avatar" t-value="false"/>
<t t-set="_item_class" t-valuef="nav-item dropdown"/>
<t t-set="_link_class" t-valuef="nav-link"/>
<t t-set="_dropdown_menu_class" t-valuef="..."/>
</t>
```
#### Language selector
```xml
<t t-call="website.placeholder_header_language_selector">
<t t-set="_div_classes" t-valuef="..."/>
</t>
```
#### Call to action
```xml
<t t-call="website.placeholder_header_call_to_action">
<t t-set="_div_classes" t-valuef="..."/>
</t>
```
#### Navbar toggler
```xml
<t t-call="website.navbar_toggler">
<t t-set="_toggler_class" t-valuef="..."/>
</t>
```
:::{seealso}
You can add a {ref}`header overlay <header_overlay>` to position your header over the content of
your page. It has to be done on each page individually.
:::
## Footer
By default, the footer contains a section with some static content. You can easily add new elements
or create your own template.
### Standard
Enable one of the default footer templates. Don't forget that you may need to disable the active
footer template first.
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-website-values-palettes: (
(
'header-template': 'Contact',
),
);
```
```{code-block} xml
:caption: '``/website_airproof/data/presets.xml``'
<record id="website.template_header_contact" model="ir.ui.view">
<field name="active" eval="True"/>
</record>
```
### Custom
Create your own template and add it to the list. Don't forget that you may need to disable the
active footer template first.
**Option**
```{code-block} xml
:caption: '``/website_airproof/data/presets.xml``'
<template id="template_header_opt" inherit_id="website.snippet_options" name="Footer Template - Option">
<xpath expr="//we-select[@data-variable='footer-template']" position="inside">
<we-button title="airproof"
data-customize-website-views="website_airproof.footer"
data-customize-website-variable="'airproof'"
data-img="/website_airproof/static/src/img/wbuilder/template_header_opt.svg"/>
</xpath>
</template>
```
**Declaration**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-website-values-palettes: (
(
'footer-template': 'airproof',
),
);
```
**Layout**
```{code-block} xml
:caption: '``/website_airproof/views/website_templates.xml``'
<record id="footer" model="ir.ui.view">
<field name="name">Airproof Footer</field>
<field name="type">qweb</field>
<field name="key">website_airproof.footer</field>
<field name="inherit_id" ref="website.layout"/>
<field name="mode">extension</field>
<field name="arch" type="xml">
<xpath expr="//div[@id='footer']" position="replace">
<div id="footer" class="oe_structure oe_structure_solo" t-ignore="true" t-if="not no_footer">
<!-- Content -->
</div>
</xpath>
</field>
</record>
```
## Copyright
There is only one template available at the moment for the copyright bar.
To replace the content or modify its structure, you can add your own code to the following XPath.
```{code-block} xml
:caption: '``/website_airproof/views/website_templates.xml``'
<template id="copyright" inherit_id="website.layout">
<xpath expr="//div[hasclass('o_footer_copyright')]" position="replace">
<div class="o_footer_copyright" data-name="Copyright">
<!-- Content -->
</div>
</xpath>
</template>
```
## Drop zone
Instead of defining the complete layout of a page, you can create building blocks (snippets) and
let the user choose where to drag and drop them, creating the page layout on their own. We call
this *modular design*.
You can define an empty area that the user can fill with snippets.
```xml
<div id="oe_structure_layout_01" class="oe_structure"/>
```
```{eval-rst}
.. todo:: Missing description in table ...
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Class
- Description
* - oe_structure
- Define a drag-and-drop area for the user.
* - oe_structure_solo
- Only one snippet can be dropped in this area.
```
You can also populate an existing drop zone with your content.
```xml
<template id="oe_structure_layout_01" inherit_id="..." name="...">
<xpath expr="//*[@id='oe_structure_layout_01']" position="replace">
<div id="oe_structure_layout_01" class="oe_structure oe_structure_solo">
<!-- Content -->
</div>
</xpath>
</template>
```
## Responsive
You can find some hints below to help you make your website responsive.
### Bootstrap
:::{seealso}
- [Bootstrap documentation on responsive breakpoints](https://getbootstrap.com/docs/4.6/layout/overview/#responsive-breakpoints)
- [Bootstrap documentation on display property](https://getbootstrap.com/docs/4.6/utilities/display/)
:::
**Font size**
As of v4.3.0, Bootstrap ships with the option to enable responsive font sizes, allowing text to
scale more naturally across device and viewport sizes. Enable them by changing the
`$enable-responsive-font-sizes` Sass variable to true.
:::{seealso}
[Responsive Font Size GitHub](https://github.com/twbs/rfs/tree/v8.1.0)
:::
### Website Builder
Hide a specific `<section>` on mobile.
```xml
<section class="d-none d-md-block">
<!-- Content -->
</section>
```
Hide a `<col>` on mobile.
```xml
<section>
<div class="container">
<div class="row d-flex align-items-stretch">
<div class="col-lg-4 d-none d-md-block">
<!-- Content -->
</div>
</div>
</div>
</section>
```
@@ -1,619 +0,0 @@
======
Layout
======
In this chapter, you will learn how to:
- Create a custom header.
- Create a custom footer.
- Modify a standard template.
- Add a copyright section.
- Improve your website's responsiveness.
Default
=======
An Odoo page combines cross-page and unique elements. Cross-page elements are the same on every
page, while unique elements are only related to a specific page. By default, a page has two
cross-page elements, the header and the footer, and a unique main element that contains the specific
content of that page.
.. code-block:: xml
<div id="wrapwrap">
<header/>
<main>
<div id="wrap" class="oe_structure">
<!-- Page Content -->
</div>
</main>
<footer/>
</div>
Any Odoo XML file starts with encoding specifications. After that, you must write your code inside
an `<odoo>` tag.
.. code-block:: xml
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
...
</odoo>
.. note::
Using precise file names is important to find information through all modules quickly. File names
should only contain lowercase alphanumerics and underscores.
Always add an empty line at the end of your file. This can be done automatically by configuring
your IDE.
XPath
=====
XPath (XML Path Language) is an expression language that enables you to navigate through elements
and attributes in an XML document easily. XPath is used to extend standard Odoo templates.
A view is coded the following way.
.. code-block:: xml
<template id="..." inherit_id="..." name="...">
<!-- Content -->
</template>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - id
- ID of the modified view
* - inherited_id
- ID of the standard view
* - name
- Human-readable name of the modified view
For each XPath, you modify two attributes: **expression** and **position**.
.. example::
.. code-block:: xml
:caption: ``/website_airproof/views/website_templates.xml``
<template id="layout" inherit_id="website.layout" name="Welcome Message">
<xpath expr="//header" position="before">
<!-- Content -->
</xpath>
</template>
This XPath adds a welcome message right before the page content.
.. warning::
Be careful when replacing default elements' attributes. As your theme extends the default one,
your changes will take priority over any future Odoo update.
.. note::
- You should update your module every time you create a new template or record.
- *XML IDs* of inheriting views should use the same *ID* as the original record. It helps to find
all inheritance at a glance. As final *XML IDs* are prefixed by the module that creates them,
there is no overlap.
Expressions
-----------
XPath uses path expressions to select nodes in an XML document. Selectors are used inside the
expression to target the right element. The most useful ones are listed below.
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Descendent selectors
- Description
* - /
- Selects from the root node.
* - //
- Selects nodes in the document from the current node that matches the selection no matter
where they are.
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute selectors
- Description
* - \*
- Selects any XML tag. `*` can be replaced by a specific tag if the selection needs to be
more precise.
* - \*[@id="id"]
- Selects a specific ID.
* - \*[hasclass("class")]
- Selects a specific class.
* - \*[@name="name"]
- Selects a tag with a specific name.
* - \*[@t-call="t-call"]
- Selects a specific t-call.
Position
--------
The position defines where the code is placed inside the template. The possible values are listed
below:
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Position
- Description
* - replace
- Replaces the targeted node with the XPath content.
* - inside
- Adds the XPath content inside the targeted node.
* - before
- Adds the XPath content before the targeted node.
* - after
- Adds the XPath content after the targeted node.
* - attributes
- Adds the XPath content inside an attribute.
.. example::
This XPath adds a `<div>` before the `<nav>` that is a direct child of the `<header>`.
.. code-block:: xml
<xpath expr="//header/nav" position="before">
<div>Some content before the header</div>
</xpath>
This XPath adds `x_airproof_header` in the class attribute of the header. You also need to define
a `separator` attribute to add a space before the class you are adding.
.. code-block:: xml
<xpath expr="//header" position="attributes">
<attribute name="class" add="x_airproof_header" separator=" "/>
</xpath>
This XPath removes `x_airproof_header` in the class attribute of the header. In this case, you
don't need to use the `separator` attribute.
.. code-block:: xml
<xpath expr="//header" position="attributes">
<attribute name="class" remove="x_airproof_header" />
</xpath>
This XPath removes the first element with a `.breadcrumb` class.
.. code-block:: xml
<xpath expr="//*[hasclass('breadcrumb')]" position="replace"/>
This XPath adds an extra `<li>` element after the last child of the `<ul>` element.
.. code-block:: xml
<xpath expr="//ul" position="inside">
<li>Last element of the list</li>
</xpath>
.. seealso::
You can find more information about XPath in this `cheat sheet <https://devhints.io/xpath>`_.
QWeb
====
QWeb is the primary templating engine used by Odoo. It is an XML templating engine mainly used to
generate HTML fragments and pages.
.. seealso::
:doc:`QWeb templates documentation <../../reference/frontend/qweb>`.
Background
==========
You can define a color or an image as the background of your website.
**Colors**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-color-palettes: map-merge($o-color-palettes,
(
'airproof': (
'o-cc1-bg': 'o-color-5',
'o-cc5-bg': 'o-color-1',
),
)
);
**Image/pattern**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-website-values-palettes: (
(
'body-image': '/website_airproof/static/src/img/background-lines.svg',
'body-image-type': 'image' or 'pattern'
)
);
Header
======
By default, the header contains a responsive navigation menu and the company's logo. You can easily
add new elements or create your own template.
Standard
--------
Enable one of the header default templates.
.. important::
Don't forget that you may need to disable the active header template first.
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-website-values-palettes: (
(
'header-template': 'Contact',
),
);
.. code-block:: xml
:caption: ``/website_airproof/data/presets.xml``
<record id="website.template_header_contact" model="ir.ui.view">
<field name="active" eval="True"/>
</record>
Custom
------
Create your own template and add it to the list.
.. important::
Don't forget that you may need to disable the active header template first.
**Option**
Use the following code to add an option for your new custom header on the Website Builder.
.. code-block:: xml
:caption: ``/website_airproof/data/presets.xml``
<template id="template_header_opt" inherit_id="website.snippet_options" name="Header Template - Option">
<xpath expr="//we-select[@data-variable='header-template']" position="inside">
<we-button title="airproof"
data-customize-website-views="website_airproof.header"
data-customize-website-variable="'airproof'" data-img="/website_airproof/static/src/img/wbuilder/template_header_opt.svg"/>
</xpath>
</template>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - data-customize-website-views
- The template to enable
* - data-customize-website-variable
- The name given to the variable
* - data-img
- The thumbnail of the custom template shown in the templates selection on the Website Builder
Now you have to explicitly define that you want to use your custom template in the Odoo SASS
variables.
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-website-values-palettes: (
(
'header-template': 'airproof',
),
);
**Layout**
.. code-block:: xml
:caption: ``/website_airproof/views/website_templates.xml``
<record id="header" model="ir.ui.view">
<field name="name">Airproof Header</field>
<field name="type">qweb</field>
<field name="key">website_airproof.header</field>
<field name="inherit_id" ref="website.layout"/>
<field name="mode">extension</field>
<field name="arch" type="xml">
<xpath expr="//header//nav" position="replace">
<!-- Static Content -->
<!-- Components -->
<!-- Editable areas -->
</xpath>
</field>
</record>
Components
----------
In your custom header, you can call several sub-templates using the `t-call` directive from QWeb:
Logo
~~~~
.. code-block:: xml
<t t-call="website.placeholder_header_brand">
<t t-set="_link_class" t-valuef="..."/>
</t>
Don't forget to record the logo of your website in the database.
.. code-block:: xml
:caption: ``/website_airproof/data/images.xml``
<record id="website.default_website" model="website">
<field name="logo" type="base64" file="website_airproof/static/src/img/content/logo.png"/>
</record>
Menu
~~~~
.. code-block:: xml
<t t-foreach="website.menu_id.child_id" t-as="submenu">
<t t-call="website.submenu">
<t t-set="item_class" t-valuef="nav-item"/>
<t t-set="link_class" t-valuef="nav-link"/>
</t>
</t>
Sign in
~~~~~~~
.. code-block:: xml
<t t-call="portal.placeholder_user_sign_in">
<t t-set="_item_class" t-valuef="nav-item"/>
<t t-set="_link_class" t-valuef="nav-link"/>
</t>
User dropdown
~~~~~~~~~~~~~
.. code-block:: xml
<t t-call="portal.user_dropdown">
<t t-set="_user_name" t-value="true"/>
<t t-set="_icon" t-value="false"/>
<t t-set="_avatar" t-value="false"/>
<t t-set="_item_class" t-valuef="nav-item dropdown"/>
<t t-set="_link_class" t-valuef="nav-link"/>
<t t-set="_dropdown_menu_class" t-valuef="..."/>
</t>
Language selector
~~~~~~~~~~~~~~~~~
.. code-block:: xml
<t t-call="website.placeholder_header_language_selector">
<t t-set="_div_classes" t-valuef="..."/>
</t>
Call to action
~~~~~~~~~~~~~~
.. code-block:: xml
<t t-call="website.placeholder_header_call_to_action">
<t t-set="_div_classes" t-valuef="..."/>
</t>
Navbar toggler
~~~~~~~~~~~~~~
.. code-block:: xml
<t t-call="website.navbar_toggler">
<t t-set="_toggler_class" t-valuef="..."/>
</t>
.. seealso::
You can add a :ref:`header overlay <header_overlay>` to position your header over the content of
your page. It has to be done on each page individually.
Footer
======
By default, the footer contains a section with some static content. You can easily add new elements
or create your own template.
Standard
--------
Enable one of the default footer templates. Don't forget that you may need to disable the active
footer template first.
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-website-values-palettes: (
(
'header-template': 'Contact',
),
);
.. code-block:: xml
:caption: ``/website_airproof/data/presets.xml``
<record id="website.template_header_contact" model="ir.ui.view">
<field name="active" eval="True"/>
</record>
Custom
------
Create your own template and add it to the list. Don't forget that you may need to disable the
active footer template first.
**Option**
.. code-block:: xml
:caption: ``/website_airproof/data/presets.xml``
<template id="template_header_opt" inherit_id="website.snippet_options" name="Footer Template - Option">
<xpath expr="//we-select[@data-variable='footer-template']" position="inside">
<we-button title="airproof"
data-customize-website-views="website_airproof.footer"
data-customize-website-variable="'airproof'"
data-img="/website_airproof/static/src/img/wbuilder/template_header_opt.svg"/>
</xpath>
</template>
**Declaration**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-website-values-palettes: (
(
'footer-template': 'airproof',
),
);
**Layout**
.. code-block:: xml
:caption: ``/website_airproof/views/website_templates.xml``
<record id="footer" model="ir.ui.view">
<field name="name">Airproof Footer</field>
<field name="type">qweb</field>
<field name="key">website_airproof.footer</field>
<field name="inherit_id" ref="website.layout"/>
<field name="mode">extension</field>
<field name="arch" type="xml">
<xpath expr="//div[@id='footer']" position="replace">
<div id="footer" class="oe_structure oe_structure_solo" t-ignore="true" t-if="not no_footer">
<!-- Content -->
</div>
</xpath>
</field>
</record>
Copyright
=========
There is only one template available at the moment for the copyright bar.
To replace the content or modify its structure, you can add your own code to the following XPath.
.. code-block:: xml
:caption: ``/website_airproof/views/website_templates.xml``
<template id="copyright" inherit_id="website.layout">
<xpath expr="//div[hasclass('o_footer_copyright')]" position="replace">
<div class="o_footer_copyright" data-name="Copyright">
<!-- Content -->
</div>
</xpath>
</template>
Drop zone
=========
Instead of defining the complete layout of a page, you can create building blocks (snippets) and
let the user choose where to drag and drop them, creating the page layout on their own. We call
this *modular design*.
You can define an empty area that the user can fill with snippets.
.. code-block:: xml
<div id="oe_structure_layout_01" class="oe_structure"/>
.. todo:: Missing description in table ...
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Class
- Description
* - oe_structure
- Define a drag-and-drop area for the user.
* - oe_structure_solo
- Only one snippet can be dropped in this area.
You can also populate an existing drop zone with your content.
.. code-block:: xml
<template id="oe_structure_layout_01" inherit_id="..." name="...">
<xpath expr="//*[@id='oe_structure_layout_01']" position="replace">
<div id="oe_structure_layout_01" class="oe_structure oe_structure_solo">
<!-- Content -->
</div>
</xpath>
</template>
Responsive
==========
You can find some hints below to help you make your website responsive.
Bootstrap
---------
.. seealso::
- `Bootstrap documentation on responsive breakpoints
<https://getbootstrap.com/docs/4.6/layout/overview/#responsive-breakpoints>`_
- `Bootstrap documentation on display property
<https://getbootstrap.com/docs/4.6/utilities/display/>`_
**Font size**
As of v4.3.0, Bootstrap ships with the option to enable responsive font sizes, allowing text to
scale more naturally across device and viewport sizes. Enable them by changing the
`$enable-responsive-font-sizes` Sass variable to true.
.. seealso::
`Responsive Font Size GitHub <https://github.com/twbs/rfs/tree/v8.1.0>`_
Website Builder
---------------
Hide a specific `<section>` on mobile.
.. code-block:: xml
<section class="d-none d-md-block">
<!-- Content -->
</section>
Hide a `<col>` on mobile.
.. code-block:: xml
<section>
<div class="container">
<div class="row d-flex align-items-stretch">
<div class="col-lg-4 d-none d-md-block">
<!-- Content -->
</div>
</div>
</div>
</section>
@@ -0,0 +1,215 @@
# Navigation
You can easily modify the navigation with the Website Builder to fit your needs.
In this chapter, you will learn how to:
- Delete and create menu items.
- Create a dropdown menu.
- Create a mega menu.
## Default
Odoo automatically generates some basic menu items depending on the apps you installed. For example,
the Website app adds two items to the main menu. These items are linked to pages, which are also
automatically created.
Delete default menu items.
```{code-block} xml
:caption: '``/website_airproof/data/menu.xml``'
<!-- Contact us -->
<delete model="website.menu" search="[('url','in', ['/', '/contactus']),
('website_id', '=', 1)]"/>
<!-- Shop -->
<delete model="website.menu" search="[('url','in', ['/', '/shop']),
('website_id', '=', 1)]"/>
```
## Menu item
**Declaration**
```{code-block} xml
:caption: '``/website_airproof/data/menu.xml``'
<record id="menu_about_us" model="website.menu">
<field name="name">About us</field>
<field name="url">/about-us</field>
<field name="parent_id" search="[
('url', '=', '/default-main-menu'),
('website_id', '=', 1)]"/>
<field name="website_id">1</field>
<field name="sequence" type="int">10</field>
</record>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Link text
* - url
- Value of the href attribute
* - parent_id
- The menu in which the item will be added.
* - website_id
- The website on which the item will be added.
* - sequence
- Defines the link's position in the top menu.
```
### New window
Open the link's URL in a new tab.
```xml
<record id="..." model="website.menu">
<field name="new_window" eval="True"/>
</record>
```
### External Links
Add a link to an external website.
```xml
<record id="..." model="website.menu">
<field name="url">https://www.odoo.com</field>
</record>
```
### Anchor
Link to a specific section of a page.
```xml
<record id="..." model="website.menu">
<field name="url">/about-us#our-team</field>
</record>
```
## Dropdown menu
**Declaration**
```{code-block} xml
:caption: '``/website_airproof/data/menu.xml``'
<record id="menu_services" model="website.menu">
<field name="name">Services</field>
<field name="website_id">1</field>
<field name="parent_id" search="[
('url', '=', '/default-main-menu'),
('website_id', '=', 1)]"/>
<field name="sequence" type="int">...</field>
</record>
```
Add an item to a dropdown menu.
```xml
<record id="menu_services_item_1" model="website.menu">
<field name="name">Item 1</field>
<field name="url">/dropdown/item-1</field>
<field name="website_id">1</field>
<field name="parent_id" ref="website_airproof.menu_services"/>
<field name="sequence" type="int">...</field>
</record>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - parent_id
- The dropdown in which the item will be added.
```
## Mega menu
A mega menu is a dropdown menu with additional possibilities and not just a list of links. In a
mega menu, you can use any kind of content (text, images, icons, ...).
**Declaration**
```{code-block} xml
:caption: '``/website_airproof/data/menu.xml``'
<record id="menu_mega_menu" model="website.menu">
<field name="name">Mega Menu</field>
<field name="url">/mega-menu</field>
<field name="parent_id" search="[
('url', '=', '/default-main-menu'),
('website_id', '=', 1)]"/>
<field name="website_id">1</field>
<field name="sequence" type="int">..</field>
<field name="is_mega_menu" eval="True"/>
<field name="mega_menu_classes">...</field>
<field name="mega_menu_content" type="html">
<!-- Content -->
</field>
</record>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - is_mega_menu
- Enable the mega menu feature.
* - mega_menu_classes
- Custom classes to be added to the main element
* - mega_menu_content
- The default content of the mega menu
```
### Custom template
Create your own template and add it to the list.
**Layout**
```{code-block} xml
:caption: '``/website_airproof/views/website_templates.xml``'
<template id="s_mega_menu_airproof" name="Airproof" groups="base.group_user">
<section class="s_mega_menu_airproof o_cc o_cc1 pt40">
<!-- Content -->
</section>
</template>
```
**Option**
Use the following code to add an option for your new custom mega menu on the Website Builder.
```{code-block} xml
:caption: '``/website_airproof/data/presets.xml``'
<template id="snippet_options" inherit_id="website.snippet_options" name="Airproof - Mega Menu Options">
<xpath expr="//*[@data-name='mega_menu_template_opt']/*" position="before">
<t t-set="_label">Airproof</t>
<we-button t-att-data-select-label="_label"
data-select-template="website_website_airproof.s_mega_menu_airproof"
data-img="/website_airproof/static/src/img/builder/header_opt.svg"
t-out="_label"/>
</xpath>
</template>
```
@@ -1,212 +0,0 @@
==========
Navigation
==========
You can easily modify the navigation with the Website Builder to fit your needs.
In this chapter, you will learn how to:
- Delete and create menu items.
- Create a dropdown menu.
- Create a mega menu.
Default
=======
Odoo automatically generates some basic menu items depending on the apps you installed. For example,
the Website app adds two items to the main menu. These items are linked to pages, which are also
automatically created.
Delete default menu items.
.. code-block:: xml
:caption: ``/website_airproof/data/menu.xml``
<!-- Contact us -->
<delete model="website.menu" search="[('url','in', ['/', '/contactus']),
('website_id', '=', 1)]"/>
<!-- Shop -->
<delete model="website.menu" search="[('url','in', ['/', '/shop']),
('website_id', '=', 1)]"/>
Menu item
=========
**Declaration**
.. code-block:: xml
:caption: ``/website_airproof/data/menu.xml``
<record id="menu_about_us" model="website.menu">
<field name="name">About us</field>
<field name="url">/about-us</field>
<field name="parent_id" search="[
('url', '=', '/default-main-menu'),
('website_id', '=', 1)]"/>
<field name="website_id">1</field>
<field name="sequence" type="int">10</field>
</record>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Link text
* - url
- Value of the href attribute
* - parent_id
- The menu in which the item will be added.
* - website_id
- The website on which the item will be added.
* - sequence
- Defines the link's position in the top menu.
New window
----------
Open the link's URL in a new tab.
.. code-block:: xml
<record id="..." model="website.menu">
<field name="new_window" eval="True"/>
</record>
External Links
--------------
Add a link to an external website.
.. code-block:: xml
<record id="..." model="website.menu">
<field name="url">https://www.odoo.com</field>
</record>
Anchor
------
Link to a specific section of a page.
.. code-block:: xml
<record id="..." model="website.menu">
<field name="url">/about-us#our-team</field>
</record>
Dropdown menu
=============
**Declaration**
.. code-block:: xml
:caption: ``/website_airproof/data/menu.xml``
<record id="menu_services" model="website.menu">
<field name="name">Services</field>
<field name="website_id">1</field>
<field name="parent_id" search="[
('url', '=', '/default-main-menu'),
('website_id', '=', 1)]"/>
<field name="sequence" type="int">...</field>
</record>
Add an item to a dropdown menu.
.. code-block:: xml
<record id="menu_services_item_1" model="website.menu">
<field name="name">Item 1</field>
<field name="url">/dropdown/item-1</field>
<field name="website_id">1</field>
<field name="parent_id" ref="website_airproof.menu_services"/>
<field name="sequence" type="int">...</field>
</record>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - parent_id
- The dropdown in which the item will be added.
Mega menu
=========
A mega menu is a dropdown menu with additional possibilities and not just a list of links. In a
mega menu, you can use any kind of content (text, images, icons, ...).
**Declaration**
.. code-block:: xml
:caption: ``/website_airproof/data/menu.xml``
<record id="menu_mega_menu" model="website.menu">
<field name="name">Mega Menu</field>
<field name="url">/mega-menu</field>
<field name="parent_id" search="[
('url', '=', '/default-main-menu'),
('website_id', '=', 1)]"/>
<field name="website_id">1</field>
<field name="sequence" type="int">..</field>
<field name="is_mega_menu" eval="True"/>
<field name="mega_menu_classes">...</field>
<field name="mega_menu_content" type="html">
<!-- Content -->
</field>
</record>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - is_mega_menu
- Enable the mega menu feature.
* - mega_menu_classes
- Custom classes to be added to the main element
* - mega_menu_content
- The default content of the mega menu
Custom template
---------------
Create your own template and add it to the list.
**Layout**
.. code-block:: xml
:caption: ``/website_airproof/views/website_templates.xml``
<template id="s_mega_menu_airproof" name="Airproof" groups="base.group_user">
<section class="s_mega_menu_airproof o_cc o_cc1 pt40">
<!-- Content -->
</section>
</template>
**Option**
Use the following code to add an option for your new custom mega menu on the Website Builder.
.. code-block:: xml
:caption: ``/website_airproof/data/presets.xml``
<template id="snippet_options" inherit_id="website.snippet_options" name="Airproof - Mega Menu Options">
<xpath expr="//*[@data-name='mega_menu_template_opt']/*" position="before">
<t t-set="_label">Airproof</t>
<we-button t-att-data-select-label="_label"
data-select-template="website_website_airproof.s_mega_menu_airproof"
data-img="/website_airproof/static/src/img/builder/header_opt.svg"
t-out="_label"/>
</xpath>
</template>
@@ -0,0 +1,323 @@
# Pages
In this chapter, you will learn how to declare static pages.
## Default pages
In Odoo, websites come with a few default static pages (Home, Contact us, 404, ...). They are built
the following way.
```xml
<template id="website.homepage" name="Homepage">
<t t-call="website.layout">
<!-- Variables -->
<t t-set="additional_title" t-value="'Home'" />
<div id="wrap" class="oe_structure oe_empty">
<!-- Content -->
</div>
</t>
</template>
```
Define the meta title.
```xml
<t t-set="additional_title" t-value="'...'"/>
```
Define the meta description.
```xml
<t t-set="meta_description" t-value="'...'"/>
```
Add a CSS class to the page.
```xml
<t t-set="pageName" t-value="'...'"/>
```
Hide the header.
```xml
<t t-set="no_header" t-value="true"/>
```
Hide the footer.
```xml
<t t-set="no_footer" t-value="true"/>
```
If needed, deactivate default pages.
```{code-block} xml
:caption: '``/website_airproof/data/pages/home.xml``'
<record id="website.homepage" model="ir.ui.view">
<field name="active" eval="False"/>
</record>
```
```{code-block} xml
:caption: '``/website_airproof/data/pages/contactus.xml``'
<record id="website.contactus" model="ir.ui.view">
<field name="active" eval="False"/>
</record>
```
Alternatively, replace the default content of these pages using XPath.
```{code-block} xml
:caption: '``/website_airproof/data/pages/404.xml``'
<template id="404" inherit_id="http_routing.404">
<xpath expr="//*[@id='wrap']" position="replace">
<t t-set="additional_title" t-value="'404 - Not found'"/>
<div id="wrap" class="oe_structure">
<!-- Content -->
</div>
</xpath>
</template>
```
:::{seealso}
- [Odoo eLearning: Search Engine Optimization (SEO)](https://www.odoo.com/slides/slide/search-engine-optimization-seo-648)
- {doc}`Documentation on SEO <../../../applications/websites/website/pages/seo>`
:::
## Theme pages
You can add as many pages as you want to your website. Instead of defining a `<template>`, create a
page object.
**Declaration**
```{code-block} xml
:caption: '``/website_airproof/data/pages/about_us.xml``'
<record id="page_about_us" model="website.page">
<field name="name">About us</field>
<field name="is_published" eval="True"/>
<field name="key">website_airproof.page_about_us</field>
<field name="url">/about-us</field>
<field name="type">qweb</field>
<field name="arch" type="xml">
<t t-name="website_airproof.page_about_us">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<!-- Content -->
</div>
</t>
</t>
</field>
</record>
```
```{eval-rst}
.. todo:: Missing description in table ...
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Page name.
* - is_published
- Define if the page is published (visible to visitors).
* - key
- View key (must be unique)
* - url
- URL where the page is reachable.
* - type
- View type
* - arch
- View architecture
```
With `<t t-call="website.layout">` you use the Odoo default page layout with your code.
(header-overlay)=
### Header overlay
Make the header background transparent and stand on top of the page content.
```xml
<field name="header_overlay" eval="True"/>
```
```{image} pages/header-overlay.png
:alt: Header overlay
```
## Media
### Images
You can record images in the database and use them later in your design/code. They will also be
available for the end user through the *media dialog*.
```{image} pages/media-window.png
:alt: Media window
```
The Website Builder supports the following image file formats: JPG, GIF, PNG, and SVG.
**Declaration**
```{code-block} xml
:caption: '``/website_airproof/data/images.xml``'
<record id="img_about_01" model="ir.attachment">
<field name="name">About Image 01</field>
<field name="datas" type="base64" file="website_airproof/static/src/img/content/img_about_01.jpg"/>
<field name="res_model">ir.ui.view</field>
<field name="public" eval="True"/>
</record>
```
```{eval-rst}
.. todo:: Missing description in table ...
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Image name
* - datas
- Path to the image file
* - res_model
- Name of the wizard model
```
Use it as a background image.
```xml
<section style="background-image: url('/web/image/website_airproof.img_about_01');">
```
Use it as a regular image.
```xml
<img src="/web/image/website_airproof.img_about_01" alt=""/>
```
Use as a regular image with a color filter.
```xml
<img src="/web/image/website.s_media_list_default_image_1"
class="img img-fluid mx-auto" alt=""
data-gl-filter="custom"
data-filter-options="{'filterColor': 'rgba(0, 0, 0, 0.5)'}"/>
```
:::{tip}
The image size greatly influences the user experience, search engine optimization, and overall
website performance. So, be sure to size your images correctly.
:::
### Videos
Add videos as background.
```xml
<section class="o_background_video" data-bg-video-src="...">
<!-- Content -->
</section>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - data-bg-video-src
- Video URL.
```
Add videos as content.
```xml
<div class="media_iframe_video" data-oe-expression="...">
<div class="css_editable_mode_display">&nbsp;</div>
<div class="media_iframe_video_size" contenteditable="false">&nbsp;</div>
<iframe src="..."
frameborder="0"
contenteditable="false"
allowfullscreen="allowfullscreen"/>
</div>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - data-oe-expression
- Video URL.
* - src
- Video URL.
```
### Icons
By default, the Font Awesome icons library is included in the Website Builder. You can place icons
anywhere using the CSS Prefix `fa` and the icon's name. Font Awesome is designed to be used with
inline elements. You can use `<i>` tag for brevity, but using a `<span>` is more semantically
correct.
```xml
<span class="fa fa-picture-o"/>
```
:::{seealso}
[Font Awesome v4 icons](https://fontawesome.com/v4/icons/)
:::
Enable the Website Builder style options.
```xml
<span class="fa fa-2x fa-picture-o rounded-circle"/>
```
Increase the icon size (fa-2x, fa-3x, fa-4x, or fa-5x classes).
```xml
<span class="fa fa-2x fa-picture-o"/>
```
```{image} pages/icon-options.png
:alt: Icon options
```
@@ -1,309 +0,0 @@
=====
Pages
=====
In this chapter, you will learn how to declare static pages.
Default pages
=============
In Odoo, websites come with a few default static pages (Home, Contact us, 404, ...). They are built
the following way.
.. code-block:: xml
<template id="website.homepage" name="Homepage">
<t t-call="website.layout">
<!-- Variables -->
<t t-set="additional_title" t-value="'Home'" />
<div id="wrap" class="oe_structure oe_empty">
<!-- Content -->
</div>
</t>
</template>
Define the meta title.
.. code-block:: xml
<t t-set="additional_title" t-value="'...'"/>
Define the meta description.
.. code-block:: xml
<t t-set="meta_description" t-value="'...'"/>
Add a CSS class to the page.
.. code-block:: xml
<t t-set="pageName" t-value="'...'"/>
Hide the header.
.. code-block:: xml
<t t-set="no_header" t-value="true"/>
Hide the footer.
.. code-block:: xml
<t t-set="no_footer" t-value="true"/>
If needed, deactivate default pages.
.. code-block:: xml
:caption: ``/website_airproof/data/pages/home.xml``
<record id="website.homepage" model="ir.ui.view">
<field name="active" eval="False"/>
</record>
.. code-block:: xml
:caption: ``/website_airproof/data/pages/contactus.xml``
<record id="website.contactus" model="ir.ui.view">
<field name="active" eval="False"/>
</record>
Alternatively, replace the default content of these pages using XPath.
.. code-block:: xml
:caption: ``/website_airproof/data/pages/404.xml``
<template id="404" inherit_id="http_routing.404">
<xpath expr="//*[@id='wrap']" position="replace">
<t t-set="additional_title" t-value="'404 - Not found'"/>
<div id="wrap" class="oe_structure">
<!-- Content -->
</div>
</xpath>
</template>
.. seealso::
- `Odoo eLearning: Search Engine Optimization (SEO)
<https://www.odoo.com/slides/slide/search-engine-optimization-seo-648>`_
- :doc:`Documentation on SEO <../../../applications/websites/website/pages/seo>`
Theme pages
===========
You can add as many pages as you want to your website. Instead of defining a `<template>`, create a
page object.
**Declaration**
.. code-block:: xml
:caption: ``/website_airproof/data/pages/about_us.xml``
<record id="page_about_us" model="website.page">
<field name="name">About us</field>
<field name="is_published" eval="True"/>
<field name="key">website_airproof.page_about_us</field>
<field name="url">/about-us</field>
<field name="type">qweb</field>
<field name="arch" type="xml">
<t t-name="website_airproof.page_about_us">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<!-- Content -->
</div>
</t>
</t>
</field>
</record>
.. todo:: Missing description in table ...
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Page name.
* - is_published
- Define if the page is published (visible to visitors).
* - key
- View key (must be unique)
* - url
- URL where the page is reachable.
* - type
- View type
* - arch
- View architecture
With `<t t-call="website.layout">` you use the Odoo default page layout with your code.
.. _header_overlay:
Header overlay
--------------
Make the header background transparent and stand on top of the page content.
.. code-block:: xml
<field name="header_overlay" eval="True"/>
.. image:: pages/header-overlay.png
:alt: Header overlay
Media
=====
Images
------
You can record images in the database and use them later in your design/code. They will also be
available for the end user through the *media dialog*.
.. image:: pages/media-window.png
:alt: Media window
The Website Builder supports the following image file formats: JPG, GIF, PNG, and SVG.
**Declaration**
.. code-block:: xml
:caption: ``/website_airproof/data/images.xml``
<record id="img_about_01" model="ir.attachment">
<field name="name">About Image 01</field>
<field name="datas" type="base64" file="website_airproof/static/src/img/content/img_about_01.jpg"/>
<field name="res_model">ir.ui.view</field>
<field name="public" eval="True"/>
</record>
.. todo:: Missing description in table ...
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Image name
* - datas
- Path to the image file
* - res_model
- Name of the wizard model
Use it as a background image.
.. code-block:: xml
<section style="background-image: url('/web/image/website_airproof.img_about_01');">
Use it as a regular image.
.. code-block:: xml
<img src="/web/image/website_airproof.img_about_01" alt=""/>
Use as a regular image with a color filter.
.. code-block:: xml
<img src="/web/image/website.s_media_list_default_image_1"
class="img img-fluid mx-auto" alt=""
data-gl-filter="custom"
data-filter-options="{'filterColor': 'rgba(0, 0, 0, 0.5)'}"/>
.. tip::
The image size greatly influences the user experience, search engine optimization, and overall
website performance. So, be sure to size your images correctly.
Videos
------
Add videos as background.
.. code-block:: xml
<section class="o_background_video" data-bg-video-src="...">
<!-- Content -->
</section>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - data-bg-video-src
- Video URL.
Add videos as content.
.. code-block:: xml
<div class="media_iframe_video" data-oe-expression="...">
<div class="css_editable_mode_display">&nbsp;</div>
<div class="media_iframe_video_size" contenteditable="false">&nbsp;</div>
<iframe src="..."
frameborder="0"
contenteditable="false"
allowfullscreen="allowfullscreen"/>
</div>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - data-oe-expression
- Video URL.
* - src
- Video URL.
Icons
-----
By default, the Font Awesome icons library is included in the Website Builder. You can place icons
anywhere using the CSS Prefix `fa` and the icon's name. Font Awesome is designed to be used with
inline elements. You can use `<i>` tag for brevity, but using a `<span>` is more semantically
correct.
.. code-block:: xml
<span class="fa fa-picture-o"/>
.. seealso::
`Font Awesome v4 icons <https://fontawesome.com/v4/icons/>`_
Enable the Website Builder style options.
.. code-block:: xml
<span class="fa fa-2x fa-picture-o rounded-circle"/>
Increase the icon size (fa-2x, fa-3x, fa-4x, or fa-5x classes).
.. code-block:: xml
<span class="fa fa-2x fa-picture-o"/>
.. image:: pages/icon-options.png
:alt: Icon options
@@ -1,6 +1,4 @@
=====
Setup
=====
# Setup
In this chapter, you will learn:
@@ -9,53 +7,48 @@ In this chapter, you will learn:
- To export and import an Odoo database in your local environment.
- To have an Odoo instance up and running.
Install
=======
## Install
There are multiple ways to :doc:`install Odoo </administration/on_premise>`, depending on the
intended use case. This documentation assumes you use the :doc:`source install
There are multiple ways to {doc}`install Odoo </administration/on_premise>`, depending on the
intended use case. This documentation assumes you use the {doc}`source install
</administration/on_premise/source>` (running Odoo from the source code), which is best suited
for Odoo designers and developers.
Databases
=========
## Databases
Structure
---------
### Structure
Every Odoo application works similarly; they are built with the same logic. A model contains fields
and relational fields that link to other models. Each model has views representing all its fields,
with backend and frontend views.
Models
~~~~~~
#### Models
The basis of Odoo is models. Models use fields to record the data. Records are stored in a database:
they are therefore linked to a model. In Odoo, you can find the different models in the
backend by enabling the :ref:`developer mode <developer-mode>` and then going to
:menuselection:`Settings --> Technical --> Database Structure: Models`.
backend by enabling the {ref}`developer mode <developer-mode>` and then going to
{menuselection}`Settings --> Technical --> Database Structure: Models`.
.. image:: setup/models-page.png
:alt: Models page
```{image} setup/models-page.png
:alt: Models page
```
Fields
~~~~~~
#### Fields
In a model, we will centralize fields (field names we need to target in our code).
.. seealso::
:doc:`/applications/studio/fields`
:::{seealso}
{doc}`/applications/studio/fields`
:::
Classic fields
**************
##### Classic fields
- Date
- Char
- Selection
- …
Relational fields
*****************
##### Relational fields
Relational fields call a field from another model. They allow you to link models together and make
them interact easily. In other words, when you use a relational field, you link a record with
@@ -71,29 +64,25 @@ on this linked record.
another model. For example, you can put several tags on one product, and several products can use
the same tags (from *many* records, you can select *many*).
Views
~~~~~
#### Views
Views define how records should be displayed to end-users. They are specified in XML, meaning they
can be edited independently from the models they represent. They are flexible and allow deep
customization of the screens they control.
Backend vs. Frontend
********************
##### Backend vs. Frontend
- **Backend views**: Kanban, List, Form, etc.
- **Frontend view**: QWeb
Static vs. Dynamic
******************
##### Static vs. Dynamic
- **Static pages** have stable content, such as the homepage. You can define their URL and set some
properties like published, indexed, etc.
- **Dynamic pages** are dynamically generated, such as the product page. Their URL is dynamic
and is accessible to all by default (this can be changed by configuring access rights).
Standard vs. Inherited
**********************
##### Standard vs. Inherited
- **Standard views** are base views implemented by Odoo. They are directly derived from their model.
You should never change them as they allow updating an Odoo database without overwriting a
@@ -102,44 +91,47 @@ Standard vs. Inherited
there is a duplicate view, there will be two views with the same name in the database, but the
duplicated view will not have an ID like for standard view.
Import an existing database
---------------------------
### Import an existing database
.. note::
You can directly go to the :doc:`theming` chapter if you do not need to import an existing
database.
:::{note}
You can directly go to the {doc}`theming` chapter if you do not need to import an existing
database.
:::
Dump
~~~~
#### Dump
Odoo SaaS
*********
##### Odoo SaaS
Go to `<database_url>/saas_worker/dump`.
Odoo.sh
*******
##### Odoo.sh
#. Connect to Odoo.sh.
#. Select the branch you want to back up.
#. Choose the :guilabel:`BACKUPS` tab.
#. Click the :guilabel:`Create Backup` button.
#. When the process is over, a notification appears. Open it and click the :guilabel:`Go to Backup`
1. Connect to Odoo.sh.
2. Select the branch you want to back up.
3. Choose the {guilabel}`BACKUPS` tab.
4. Click the {guilabel}`Create Backup` button.
5. When the process is over, a notification appears. Open it and click the {guilabel}`Go to Backup`
button.
#. Click the :guilabel:`Download` icon. Select :guilabel:`Testing` under
:guilabel:`Purpose` and :guilabel:`With filestore` under :guilabel:`Filestore`.
.. image:: setup/download-backup.png
:alt: Download backup
6. Click the {guilabel}`Download` icon. Select {guilabel}`Testing` under
{guilabel}`Purpose` and {guilabel}`With filestore` under {guilabel}`Filestore`.
#. You will receive a notification when the dump is ready to be downloaded. Open it and click on
:guilabel:`Download` to get your dump.
```{image} setup/download-backup.png
:alt: Download backup
```
.. image:: setup/database-backup.png
:alt: Database backup
7. You will receive a notification when the dump is ready to be downloaded. Open it and click on
{guilabel}`Download` to get your dump.
Move filestore
~~~~~~~~~~~~~~
```{image} setup/database-backup.png
:alt: Database backup
```
#### Move filestore
Copy all the folders included in the filestore folder and paste them to the following location on
your computer:
@@ -147,61 +139,61 @@ your computer:
- macOS: `/Users/<User>/Library/Application Support/Odoo/filestore/<database_name>`
- Linux: `/home/<User>/.local/share/Odoo/filestore/<database_name>`
.. note::
`/Library` is a hidden folder.
:::{note}
`/Library` is a hidden folder.
:::
Database setup
~~~~~~~~~~~~~~
#### Database setup
Create an empty database.
.. code-block:: xml
createdb <database_name>
```xml
createdb <database_name>
```
Import the SQL file in the database that you just created.
.. code-block:: xml
psql <database_name> < dump.sql
```xml
psql <database_name> < dump.sql
```
Reset the admin user password.
.. code-block:: xml
```xml
psql \c
<database_name>
update res_users set login='admin', password='admin' where id=2;
```
psql \c
<database_name>
update res_users set login='admin', password='admin' where id=2;
## Getting started
Getting started
===============
Running Odoo
------------
### Running Odoo
Once all dependencies are set up, Odoo can be launched by running `odoo-bin`, the command-line
interface of the server. It is located at the root of the Odoo Community directory.
- :ref:`Running Odoo <install/source/running_odoo>`
- `Docker <https://hub.docker.com/_/odoo/>`_
- {ref}`Running Odoo <install/source/running_odoo>`
- [Docker](https://hub.docker.com/_/odoo/)
To configure the server, you can specify command-line arguments or a configuration file. The first
method is presented below.
The :ref:`CLI <reference/cmdline>` offers several functionalities related to Odoo. You can use it to
:ref:`run the server <reference/cmdline/server>`, scaffold an Odoo theme, populate a database, or
The {ref}`CLI <reference/cmdline>` offers several functionalities related to Odoo. You can use it to
{ref}`run the server <reference/cmdline/server>`, scaffold an Odoo theme, populate a database, or
count the number of lines of code.
Shell script
------------
### Shell script
A typical way to :ref:`run the server <reference/cmdline/server>` would be to add all command line arguments to a `.sh` script.
A typical way to {ref}`run the server <reference/cmdline/server>` would be to add all command line arguments to a `.sh` script.
```{eval-rst}
.. example::
.. code-block:: xml
./odoo-bin --addons-path=../enterprise,addons --db-filter=<database> -d <database> --without-demo=all -i website --dev=xml
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
@@ -232,26 +224,29 @@ A typical way to :ref:`run the server <reference/cmdline/server>` would be to ad
* - :option:`--dev <odoo-bin --dev>`
- Comma-separated list of features. For development purposes only. :ref:`More info
<reference/cmdline/dev>`
```
Sign in
-------
### Sign in
After the server has started (the INFO log `odoo.modules.loading: Modules loaded.` is printed), open
http://localhost:8069 in your web browser and log in with the base administrator account.
<http://localhost:8069> in your web browser and log in with the base administrator account.
Type **admin** for the email and **admin** for the password.
.. image:: setup/welcome-homepage.png
:alt: Welcome homepage
```{image} setup/welcome-homepage.png
:alt: Welcome homepage
```
.. tip::
Hit *CTRL+C* to stop the server. Do it twice if needed.
:::{tip}
Hit *CTRL+C* to stop the server. Do it twice if needed.
:::
Developer mode
--------------
### Developer mode
The developer mode, also known as debug mode, is useful for development as it gives access to
additional tools. In the next chapters, it is assumed that you have enabled the developer mode.
.. seealso::
:doc:`/applications/general/developer_mode`
:::{seealso}
{doc}`/applications/general/developer_mode`
:::
@@ -0,0 +1,239 @@
# Shapes
Shapes are handy if you want to add personality to your website.
In this chapter, you will learn how to add standard and custom background and image shapes.
## Background shapes
Background shapes are SVG files that you can add as a decorative background in your different
sections. Each shape has one or several customizable colors, and some of them are animated.
### Standard
A large selection of default background shapes is available.
**Use**
```xml
<section data-oe-shape-data="{'shape':'web_editor/Zigs/06'}">
<div class="o_we_shape o_web_editor_Zigs_06"/>
<div class="container">
<!-- Content -->
</div>
</section>
```
`data-oe-shape-data` is the location of your shape.
Flip the shape horizontally or vertically by using the X or Y axis.
```xml
<section data-oe-shape-data="{'shape':'web_editor/Zigs/06','flip':[x,y]}">
<div class="o_we_shape o_we_flip_x o_we_flip_y o_web_editor_Zigs_06"/>
<div class="container">
<!-- Content -->
</div>
</section>
```
#### Colors mapping
You can also change the default color mapping of your shape.
##### Switch colors mapping
First, put the c\* color (here `4`).
Then, the replacement color (here `3`). These replacement colors also range from 1 to 5:
- `1` = background color of the color preset 1 (o-cc1).
- `2` = background color of the color preset 2 (o-cc2).
- `3` = background color of the color preset 3 (o-cc3).
- `4` = background color of the color preset 4 (o-cc4).
- `5` = background color of the color preset 5 (o-cc5).
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/boostrap_overridden.scss``'
$o-bg-shapes: change-shape-colors-mapping('web_editor', 'Zigs/06', (4: 3, 5: 1));
```
##### Add extra colors mapping
Adding extra color mapping allows you to add a color variant to the template of a shape while
keeping the original.
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/boostrap_overridden.scss``'
$o-bg-shapes: add-extra-shape-colors-mapping('web_editor', 'Zigs/06', 'second', (4: 3, 5: 1));
```
```xml
<section data-oe-shape-data="{'shape':'web_editor/Zigs/06'}">
<div class="o_we_shape o_web_editor_Zigs_06 o_second_extra_shape_mapping"/>
<div class="container">
<!-- Content -->
</div>
</section>
```
### Custom
Sometimes, your design might require creating one or several custom shapes.
Firstly, you need to create an SVG file for your shape.
```{code-block} xml
:caption: '``/website_airproof/static/shapes/hexagons/01.svg``'
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="86" height="100">
<polygon points="0 25, 43 0, 86 25, 86 75, 43 100, 0 75" style="fill: #3AADAA;"/>
</svg>
```
Make sure to use colors from the default Odoo palette for your shape.
```scss
default_palette = {
'1': '#3AADAA',
'2': '#7C6576',
'3': '#F6F6F6',
'4': '#FFFFFF',
'5': '#383E45',
}
```
Declare your shape file.
```{code-block} xml
:caption: '``/website_airproof/data/shapes.xml``'
<record id="shape_hexagon_01" model="ir.attachment">
<field name="name">01.svg</field>
<field name="datas" type="base64" file="website_airproof/static/shapes/hexagons/01.svg"/>
<field name="url">/web_editor/shape/illustration/hexagons/01.svg</field>
<field name="public" eval="True"/>
</record>
```
```{eval-rst}
.. todo:: Missing description in table ...
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Name of the shape
* - datas
- Path to the shape
* - url
- ...
* - public
- Makes the shape available for later editing.
```
Define the styles of your shape.
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-bg-shapes: map-merge($o-bg-shapes,
(
'illustration': map-merge(
map-get($o-bg-shapes, 'illustration') or (),
(
'hexagons/01': ('position': center center, 'size': auto 100%, 'colors': (1), 'repeat-x': true, 'repeat-y': true),
),
),
)
);
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Key
- Description
* - File location
- `hexagons/01` corresponds to the location of your file in the `shapes` folder.
* - position
- Defines the position of your shape.
* - size
- Defines the size of your shape.
* - colors
- Defines the color c* you want it to have (this will override the color you specified in your
SVG).
* - repeat-x
- Defines if the shape is repeated horizontally. This key is optional and only needs to be
defined if set to `true`.
* - repeat-y
- Defines if the shape is repeated vertically. This key is optional and only needs to be
defined if set to `true`.
```
Lastly, add your shape to the list of shapes available on the Website Builder.
```{code-block} xml
:caption: '``/website_airproof/views/snippets/options.xml``'
<template id="snippet_options_background_options" inherit_id="website.snippet_options_background_options" name="Shapes">
<xpath expr="//*[hasclass('o_we_shape_menu')]/*[last()]" position="after">
<we-select-page string="Theme">
<we-button data-shape="illustration/hexagons/01" data-select-label="Hexagon 01"/>
</we-select-page>
</xpath>
</template>
```
Your custom shape can now be used the same way as standard shapes.
## Image shapes
Image shapes are SVG files you can add as a clipping mask on your images. Some shapes have
customizable colors, and some are animated.
### Standard
A large selection of default image shapes is available.
**Use**
```xml
<img src="..."
class="img img-fluid mx-auto"
alt="..."
data-shape="web_editor/solid/blob_2_solid_str"
data-shape-colors="#35979C;;;;"
>
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - data-shape
- Location of the shape
* - data-shape-colors
- Colors applied to the shape
```
:::{warning}
Sometimes, your image shape might not be applied after adding your changes. To fix the issue,
open the Website Builder and save the page to force the shape to load.
:::
@@ -1,233 +0,0 @@
======
Shapes
======
Shapes are handy if you want to add personality to your website.
In this chapter, you will learn how to add standard and custom background and image shapes.
Background shapes
=================
Background shapes are SVG files that you can add as a decorative background in your different
sections. Each shape has one or several customizable colors, and some of them are animated.
Standard
--------
A large selection of default background shapes is available.
**Use**
.. code-block:: xml
<section data-oe-shape-data="{'shape':'web_editor/Zigs/06'}">
<div class="o_we_shape o_web_editor_Zigs_06"/>
<div class="container">
<!-- Content -->
</div>
</section>
`data-oe-shape-data` is the location of your shape.
Flip the shape horizontally or vertically by using the X or Y axis.
.. code-block:: xml
<section data-oe-shape-data="{'shape':'web_editor/Zigs/06','flip':[x,y]}">
<div class="o_we_shape o_we_flip_x o_we_flip_y o_web_editor_Zigs_06"/>
<div class="container">
<!-- Content -->
</div>
</section>
Colors mapping
~~~~~~~~~~~~~~
You can also change the default color mapping of your shape.
Switch colors mapping
*********************
First, put the c* color (here `4`).
Then, the replacement color (here `3`). These replacement colors also range from 1 to 5:
- `1` = background color of the color preset 1 (o-cc1).
- `2` = background color of the color preset 2 (o-cc2).
- `3` = background color of the color preset 3 (o-cc3).
- `4` = background color of the color preset 4 (o-cc4).
- `5` = background color of the color preset 5 (o-cc5).
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/boostrap_overridden.scss``
$o-bg-shapes: change-shape-colors-mapping('web_editor', 'Zigs/06', (4: 3, 5: 1));
Add extra colors mapping
************************
Adding extra color mapping allows you to add a color variant to the template of a shape while
keeping the original.
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/boostrap_overridden.scss``
$o-bg-shapes: add-extra-shape-colors-mapping('web_editor', 'Zigs/06', 'second', (4: 3, 5: 1));
.. code-block:: xml
<section data-oe-shape-data="{'shape':'web_editor/Zigs/06'}">
<div class="o_we_shape o_web_editor_Zigs_06 o_second_extra_shape_mapping"/>
<div class="container">
<!-- Content -->
</div>
</section>
Custom
------
Sometimes, your design might require creating one or several custom shapes.
Firstly, you need to create an SVG file for your shape.
.. code-block:: xml
:caption: ``/website_airproof/static/shapes/hexagons/01.svg``
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="86" height="100">
<polygon points="0 25, 43 0, 86 25, 86 75, 43 100, 0 75" style="fill: #3AADAA;"/>
</svg>
Make sure to use colors from the default Odoo palette for your shape.
.. code-block:: scss
default_palette = {
'1': '#3AADAA',
'2': '#7C6576',
'3': '#F6F6F6',
'4': '#FFFFFF',
'5': '#383E45',
}
Declare your shape file.
.. code-block:: xml
:caption: ``/website_airproof/data/shapes.xml``
<record id="shape_hexagon_01" model="ir.attachment">
<field name="name">01.svg</field>
<field name="datas" type="base64" file="website_airproof/static/shapes/hexagons/01.svg"/>
<field name="url">/web_editor/shape/illustration/hexagons/01.svg</field>
<field name="public" eval="True"/>
</record>
.. todo:: Missing description in table ...
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Name of the shape
* - datas
- Path to the shape
* - url
- ...
* - public
- Makes the shape available for later editing.
Define the styles of your shape.
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-bg-shapes: map-merge($o-bg-shapes,
(
'illustration': map-merge(
map-get($o-bg-shapes, 'illustration') or (),
(
'hexagons/01': ('position': center center, 'size': auto 100%, 'colors': (1), 'repeat-x': true, 'repeat-y': true),
),
),
)
);
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Key
- Description
* - File location
- `hexagons/01` corresponds to the location of your file in the `shapes` folder.
* - position
- Defines the position of your shape.
* - size
- Defines the size of your shape.
* - colors
- Defines the color c* you want it to have (this will override the color you specified in your
SVG).
* - repeat-x
- Defines if the shape is repeated horizontally. This key is optional and only needs to be
defined if set to `true`.
* - repeat-y
- Defines if the shape is repeated vertically. This key is optional and only needs to be
defined if set to `true`.
Lastly, add your shape to the list of shapes available on the Website Builder.
.. code-block:: xml
:caption: ``/website_airproof/views/snippets/options.xml``
<template id="snippet_options_background_options" inherit_id="website.snippet_options_background_options" name="Shapes">
<xpath expr="//*[hasclass('o_we_shape_menu')]/*[last()]" position="after">
<we-select-page string="Theme">
<we-button data-shape="illustration/hexagons/01" data-select-label="Hexagon 01"/>
</we-select-page>
</xpath>
</template>
Your custom shape can now be used the same way as standard shapes.
Image shapes
============
Image shapes are SVG files you can add as a clipping mask on your images. Some shapes have
customizable colors, and some are animated.
Standard
--------
A large selection of default image shapes is available.
**Use**
.. code-block:: xml
<img src="..."
class="img img-fluid mx-auto"
alt="..."
data-shape="web_editor/solid/blob_2_solid_str"
data-shape-colors="#35979C;;;;"
>
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Attribute
- Description
* - data-shape
- Location of the shape
* - data-shape-colors
- Colors applied to the shape
.. warning::
Sometimes, your image shape might not be applied after adding your changes. To fix the issue,
open the Website Builder and save the page to force the shape to load.
@@ -0,0 +1,650 @@
# Theming
After your development environment is fully set up, you can start building the skeleton of your
theme module. In this chapter, you will discover how to:
- Enable/disable the Website Builder's standard options and templates.
- Define the colors and fonts to use for your design.
- Get the most out of Bootstrap variables.
- Add custom styles and JavaScript.
## Theme module
Odoo comes with a default theme that provides minimal structure and layout. When you create a new
theme, you are extending the default theme.
Remember to add the directory containing your module to the `addons-path` command-line argument
when running Odoo in your development environment.
### Technical naming
The first step is to create a new directory.
```xml
website_airproof
```
:::{note}
Prefix it with `website_` and use only lowercase ASCII alphanumeric characters and underscores.
In this documentation, we will use **Airproof** (a fictional project) as an example.
:::
### File structure
Themes are packaged like any Odoo module. Even if you are designing a basic website, you need to
package its theme like a module.
```
website_airproof
├── data
├── i18n
├── lib
├── static
│ ├── description
│ ├── fonts
│ ├── image_shapes // Shapes for images
│ ├── shapes // Shapes for background
│ └── src
│ ├── img
│ │ ├── content // For those used in the pages of your website
│ │ └── wbuilder // For those used in the builder
│ ├── js
│ ├── scss
│ └── snippets // custom snippets
├── views
├── __init__.py
└── __manifest__.py
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Folder
- Description
* - data
- Presets, menus, pages, images, shapes, … (`*.xml`)
* - i18n
- Translations (`*.po`, `*.pot`)
* - lib
- External libraries (`*.js`)
* - static
- Custom assets (`*.jpg`, `*.gif`, `*.png`, `*.svg`, `*.pdf`, `*.scss`, `*.js`)
* - views
- Custom views and templates (`*.xml`)
```
### Initialization
An Odoo module is also a Python package with a {file}`__init__.py` file containing import
instructions for various Python files in the module. This file can remain empty for now.
### Declaration
An Odoo module is declared by its manifest file. This file declares a Python package as an Odoo
module and specifies the module's metadata. It must at least contain the `name` field, which is
always required. It usually contains much more information.
```{code-block} python
:caption: '``/website_airproof/__manifest__.py``'
{
'name': 'Airproof Theme',
'description': '...',
'category': 'Website/Theme',
'version': '15.0.0',
'author': '...',
'license': '...',
'depends': ['website'],
'data': [
# ...
],
'assets': {
# ...
},
}
```
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Human-readable name of the module (required)
* - description
- Extended description of the module, in `reStructuredText
<https://en.wikipedia.org/wiki/ReStructuredText>`_
* - category
- Classification category within Odoo
* - version
- Odoo version this module is addressing
* - author
- Name of the module author
* - license
- Distribution license for the module
* - depends
- Odoo modules must be loaded before this one, either because this module uses features
they create or because it alters resources they define
* - data
- List of XML files
* - assets
- List of SCSS and JS files
```
:::{note}
To create a website theme, you only need to install the Website app. If you need other apps
(Blogs, Events, eCommerce, ...), you can also add them.
:::
## Default options
First, try to construct your theme by using Odoo's default options. This ensures two things:
1. You do not re-invent something which already exists. For example, as Odoo provides an option to
add a border on the footer, you shouldn't recode it yourself. Instead, enable the default option
first, then extend it if needed.
2. The user can still use all of Odoo's features with your theme. For example, if you recode the
border on the footer, you may break the default option or make it useless, giving the user a bad
experience. Also, your recode might not work as well as the default option, as other Odoo
features may rely on it.
:::{tip}
- Use four spaces per indentation level.
- Do not use tabs.
- Never mix spaces and tabs.
:::
:::{seealso}
{doc}`Odoo coding guidelines <../../../contributing/development/coding_guidelines>`
:::
### Odoo variables
Odoo declares many CSS rules, most entirely customizable by overriding the related SCSS variables.
To do so, create a {file}`primary_variables.scss` file and add it to the `_assets_primary_variables`
bundle.
**Declaration**
```{code-block} python
:caption: '``/website_airproof/__manifest__.py``'
'assets': {
'web._assets_primary_variables': [
('prepend', 'website_airproof/static/src/scss/primary_variables.scss'),
],
},
```
By reading the source code, variables related to options are easily noticeable.
```xml
<we-button title="..."
data-name="..."
data-customize-website-views="..."
data-customize-website-variable="'Sidebar'"
data-img="..."/>
```
These variables can be overridden through the `$o-website-value-palettes` map, for example.
#### Global
**Declaration**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-website-values-palettes: (
(
// Templates
// Colors
// Fonts
// Buttons
// ...
),
);
```
:::{tip}
That file must only contain definitions and overrides of SCSS variables and mixins.
:::
:::{seealso}
[Primary variables SCSS](https://github.com/odoo/odoo/blob/34c0c9c1ae00aba391932129d4cefd027a9c6bbd/addons/website/static/src/scss/primary_variables.scss#L1954)
:::
#### Fonts
You can embed any font on your website. The Website Builder automatically makes them available in
the font selector.
**Declaration**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-theme-font-configs: (
<font-name>: (
'family': <css font family list>,
'url' (optional): <related part of Google fonts URL>,
'properties' (optional): (
<font-alias>: (
<website-value-key>: <value>,
...,
),
...,
)
)
```
**Use**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-website-values-palettes: (
(
'font': '<font-name>',
'headings-font': '<font-name>',
'navbar-font': '<font-name>',
'buttons-font': '<font-name>',
),
);
```
##### Google fonts
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-theme-font-configs: (
'Poppins': (
'family': ('Poppins', sans-serif),
'url': 'Poppins:400,500',
'properties' : (
'base': (
'font-size-base': 1rem,
),
),
),
);
```
##### Custom fonts
First, create a specific SCSS file to declare your custom font(s).
```{code-block} python
:caption: '``/website_airproof/__manifest__.py``'
'assets': {
'web.assets_frontend': [
'website_airproof/static/src/scss/font.scss',
],
},
```
Then, use the `@font-face` rule to allow you custom font(s) to be loaded on your website.
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/font.scss``'
@font-face {
font-family: '<font-name>';
...
}
```
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-theme-font-configs: (
'Proxima Nova': (
'family': ('Proxima Nova', sans-serif),
'properties' : (
'base': (
'font-size-base': 1rem,
),
),
),
);
```
:::{tip}
It is recommended to use the .woff format for your fonts.
:::
#### Colors
The Website Builder relies on palettes composed of five named colors. Defining those in your theme
ensures it stays consistent.
```{eval-rst}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Color
- Description
* - o-color-1
- Primary
* - o-color-2
- Secondary
* - o-color-3
- Extra
* - o-color-4
- Whitish
* - o-color-5
- Blackish
```
```{image} theming/theme-colors.png
:alt: Theme colors
:width: 300
```
**Declaration**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-color-palettes: map-merge($o-color-palettes,
(
'airproof': (
'o-color-1': #bedb39,
'o-color-2': #2c3e50,
'o-color-3': #f2f2f2,
'o-color-4': #ffffff,
'o-color-5': #000000,
),
)
);
```
Add the created palette to the list of palettes offered by the Website Builder.
```scss
$o-selected-color-palettes-names: append($o-selected-color-palettes-names, 'airproof');
```
**Use**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-website-values-palettes: (
(
'color-palettes-name': 'airproof',
),
);
```
```{image} theming/theme-colors-airproof.png
:alt: Theme colors Airproof
:width: 800
```
**Color combinations**
Based on the previously defined five color palettes, the Website Builder automatically generates
five color combinations, each defining a color for the background, text, headings, links, primary
buttons, and secondary buttons. These colors can be customized later by the user.
```{image} theming/theme-colors-big.png
:alt: Theme colors
:width: 300
```
The colors used in a color combination are accessible and can be overridden through the BS
`$colors map` using a specific prefix (`o-cc` for `color combination`).
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/primary_variables.scss``'
$o-color-palettes: map-merge($o-color-palettes,
(
'airproof': (
'o-cc*-bg': 'o-color-*',
'o-cc*-text': 'o-color-*',
'o-cc*-headings': 'o-color-*',
'o-cc*-h2': 'o-color-*',
'o-cc*-h3': 'o-color-*',
'o-cc*-h4': 'o-color-*',
'o-cc*-h5': 'o-color-*',
'o-cc*-h6': 'o-color-*',
'o-cc*-link': 'o-color-*',
'o-cc*-btn-primary': 'o-color-*',
'o-cc*-btn-primary-border': 'o-color-*',
'o-cc*-btn-secondary': 'o-color-*',
'o-cc*-btn-secondary-border': 'o-color-*',
),
)
);
```
:::{note}
For each `o-cc*`, replace the `*` with the digit (1 - 5) corresponding to the desired color
combination.
The default text color is `o-color-5`. If the background is too dark, it will automatically
change to the `o-color-4` color.
:::
:::{seealso}
[Color combinations SCSS](https://github.com/odoo/odoo/blob/34c0c9c1ae00aba391932129d4cefd027a9c6bbd/addons/web_editor/static/src/scss/web_editor.common.scss#L711)
:::
:::{admonition} Demo page
The Website Builder automatically generates a page to view the color combinations of the theme
color palette: <http://localhost:8069/website/demo/color-combinations>
:::
### Bootstrap variables
Odoo includes Bootstrap by default. You can use all variables and mixins of the framework.
If Odoo does not provide the variable you are looking for, there could be a Bootstrap variable that
allows it. Indeed all Odoo layouts respect Bootstrap structures and use Bootstrap components or
their extensions. If you customize a Bootstrap variable, you add a generic style for the whole user
website.
Use a dedicated file added to the {file}`_assets_frontend_helpers` bundle to override Bootstrap
values and *not* the {file}`primary_variables.scss` file.
**Declaration**
```{code-block} python
:caption: '``/website_airproof/__manifest__.py``'
'assets': {
'web._assets_frontend_helpers': [
('prepend', 'website_airproof/static/src/scss/bootstrap_overridden.scss'),
],
},
```
**Use**
```{code-block} scss
:caption: '``/website_airproof/static/src/scss/bootstrap_overridden.scss``'
// Typography
$h1-font-size: 4rem !default;
// Navbar
$navbar-nav-link-padding-x: 1rem!default;
// Buttons + Forms
$input-placeholder-color: o-color('o-color-1') !default;
// Cards
$card-border-width: 0 !default;
```
:::{tip}
That file must only contain definitions and overrides of SCSS variables and mixins.
:::
:::{warning}
Don't override Bootstrap variables that depend on Odoo variables. Otherwise, you might break the
possibility for the user to customize them using the Website Builder.
:::
:::{seealso}
[Bootstrap overridden SCSS]({GITHUB_PATH}/addons/website/static/src/scss/bootstrap_overridden.scss)
:::
:::{admonition} Demo page
<http://localhost:8069/website/demo/bootstrap>
:::
### Views
For some options, in addition to the Website Builder variable, you also have to activate a specific
view.
By reading the source code, templates related to options are easily found.
```xml
<we-button title="..."
data-name="..."
data-customize-website-views="website.template_header_default"
data-customize-website-variable="'...'"
data-img="..."/>
```
```xml
<template id="..." inherit_id="..." name="..." active="True"/>
<template id="..." inherit_id="..." name="..." active="False"/>
```
```{eval-rst}
.. example::
**Changing the menu items' horizontal alignment**
.. code-block:: xml
:caption: ``/website_airproof/data/presets.xml``
<record id="website.template_header_default_align_center" model="ir.ui.view">
<field name="active" eval="True"/>
</record>
The same logic can be used for others Odoo apps as well.
**eCommerce - Display products categories**
.. code-block:: xml
<record id="website_sale.products_categories" model="ir.ui.view">
<field name="active" eval="False"/>
</record>
**Portal - Disable the language selector**
.. code-block:: xml
<record id="portal.footer_language_selector" model="ir.ui.view">
<field name="active" eval="False"/>
</record>
```
## Assets
For this part, we will refer to the `assets_frontend` bundle located in the web module. This bundle
specifies the list of assets loaded by the Website Builder, and the goal is to add your SCSS and JS
files to the bundle.
### Styles
The Website Builder together with Bootstrap are great for defining the basic styles of your website.
But to design something unique, you should go a step further. For this, you can easily add any SCSS
file to your theme.
**Declaration**
```{code-block} python
:caption: '``/website_airproof/__manifest__.py``'
'assets': {
'web.assets_frontend': [
'website_airproof/static/src/scss/theme.scss',
],
},
```
Feel free to reuse the variables from your Bootstrap file and the ones used by Odoo in your
{file}`theme.scss` file.
```{eval-rst}
.. example::
.. code-block:: javascript
:caption: ``/website_airproof/static/src/scss/theme.scss``
blockquote {
border-radius: $rounded-pill;
color: o-color('o-color-3');
font-family: o-website-value('headings-font');
}
```
### Interactivity
Odoo supports three different kinds of JavaScript files:
- {ref}`plain JavaScript files <frontend/modules/plain_js>` (no module system),
- {ref}`native JavaScript module <frontend/modules/native_js>`, and
- {ref}`Odoo modules <frontend/modules/odoo_module>` (using a custom module system).
Most new Odoo JavaScript codes should use the native JavaScript module system. It's simpler and
brings the benefit of a better developer experience with better integration with the IDE.
**Declaration**
```{code-block} python
:caption: '``/website_airproof/__manifest__.py``'
'assets': {
'web.assets_frontend': [
'website_airproof/static/src/js/theme.js',
],
},
```
:::{note}
If you want to include files from an external library, you can add them to the {file}`/lib`
folder of your module.
:::
:::{tip}
- Use a linter (JSHint, ...).
- Never add minified JavaScript libraries.
- Add `'use strict';` on top of every Odoo JavaScript module.
- Variables and functions should be *camelcased* (`myVariable`) instead of *snakecased*
(`my_variable`).
- Do not name a variable `event`; use `ev.` instead. This is to avoid bugs on non-Chrome
browsers, as Chrome is magically assigning a global event variable (so if you use the event
variable without declaring it, it will work fine on Chrome but crash on every other browser).
- Use strict comparisons (`===` instead of `==`).
- Use double quotes for all textual strings (such as `"Hello"`) and single quotes for all other
strings, such as a CSS selector `.x_nav_item`.
- Always use `this._super.apply(this, arguments)`.
:::
:::{seealso}
- [Odoo JavaScript coding guidelines](https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines)
- {doc}`Overview of the Odoo JavaScript framework
<../../reference/frontend/javascript_reference>`
- [Odoo Experience Talk: 10 Tips to take your website design to the next level!](https://www.youtube.com/watch?v=vAgE_fPVXUQ&ab_channel=Odoo)
:::
@@ -1,627 +0,0 @@
=======
Theming
=======
After your development environment is fully set up, you can start building the skeleton of your
theme module. In this chapter, you will discover how to:
- Enable/disable the Website Builder's standard options and templates.
- Define the colors and fonts to use for your design.
- Get the most out of Bootstrap variables.
- Add custom styles and JavaScript.
Theme module
============
Odoo comes with a default theme that provides minimal structure and layout. When you create a new
theme, you are extending the default theme.
Remember to add the directory containing your module to the `addons-path` command-line argument
when running Odoo in your development environment.
Technical naming
----------------
The first step is to create a new directory.
.. code-block:: xml
website_airproof
.. note::
Prefix it with `website_` and use only lowercase ASCII alphanumeric characters and underscores.
In this documentation, we will use **Airproof** (a fictional project) as an example.
File structure
--------------
Themes are packaged like any Odoo module. Even if you are designing a basic website, you need to
package its theme like a module.
::
website_airproof
├── data
├── i18n
├── lib
├── static
│ ├── description
│ ├── fonts
│ ├── image_shapes // Shapes for images
│ ├── shapes // Shapes for background
│ └── src
│ ├── img
│ │ ├── content // For those used in the pages of your website
│ │ └── wbuilder // For those used in the builder
│ ├── js
│ ├── scss
│ └── snippets // custom snippets
├── views
├── __init__.py
└── __manifest__.py
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Folder
- Description
* - data
- Presets, menus, pages, images, shapes, … (`*.xml`)
* - i18n
- Translations (`*.po`, `*.pot`)
* - lib
- External libraries (`*.js`)
* - static
- Custom assets (`*.jpg`, `*.gif`, `*.png`, `*.svg`, `*.pdf`, `*.scss`, `*.js`)
* - views
- Custom views and templates (`*.xml`)
Initialization
--------------
An Odoo module is also a Python package with a :file:`__init__.py` file containing import
instructions for various Python files in the module. This file can remain empty for now.
Declaration
-----------
An Odoo module is declared by its manifest file. This file declares a Python package as an Odoo
module and specifies the module's metadata. It must at least contain the `name` field, which is
always required. It usually contains much more information.
.. code-block:: python
:caption: ``/website_airproof/__manifest__.py``
{
'name': 'Airproof Theme',
'description': '...',
'category': 'Website/Theme',
'version': '15.0.0',
'author': '...',
'license': '...',
'depends': ['website'],
'data': [
# ...
],
'assets': {
# ...
},
}
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Field
- Description
* - name
- Human-readable name of the module (required)
* - description
- Extended description of the module, in `reStructuredText
<https://en.wikipedia.org/wiki/ReStructuredText>`_
* - category
- Classification category within Odoo
* - version
- Odoo version this module is addressing
* - author
- Name of the module author
* - license
- Distribution license for the module
* - depends
- Odoo modules must be loaded before this one, either because this module uses features
they create or because it alters resources they define
* - data
- List of XML files
* - assets
- List of SCSS and JS files
.. note::
To create a website theme, you only need to install the Website app. If you need other apps
(Blogs, Events, eCommerce, ...), you can also add them.
Default options
===============
First, try to construct your theme by using Odoo's default options. This ensures two things:
#. You do not re-invent something which already exists. For example, as Odoo provides an option to
add a border on the footer, you shouldn't recode it yourself. Instead, enable the default option
first, then extend it if needed.
#. The user can still use all of Odoo's features with your theme. For example, if you recode the
border on the footer, you may break the default option or make it useless, giving the user a bad
experience. Also, your recode might not work as well as the default option, as other Odoo
features may rely on it.
.. tip::
- Use four spaces per indentation level.
- Do not use tabs.
- Never mix spaces and tabs.
.. seealso::
:doc:`Odoo coding guidelines <../../../contributing/development/coding_guidelines>`
Odoo variables
--------------
Odoo declares many CSS rules, most entirely customizable by overriding the related SCSS variables.
To do so, create a :file:`primary_variables.scss` file and add it to the `_assets_primary_variables`
bundle.
**Declaration**
.. code-block:: python
:caption: ``/website_airproof/__manifest__.py``
'assets': {
'web._assets_primary_variables': [
('prepend', 'website_airproof/static/src/scss/primary_variables.scss'),
],
},
By reading the source code, variables related to options are easily noticeable.
.. code-block:: xml
<we-button title="..."
data-name="..."
data-customize-website-views="..."
data-customize-website-variable="'Sidebar'"
data-img="..."/>
These variables can be overridden through the `$o-website-value-palettes` map, for example.
Global
~~~~~~
**Declaration**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-website-values-palettes: (
(
// Templates
// Colors
// Fonts
// Buttons
// ...
),
);
.. tip::
That file must only contain definitions and overrides of SCSS variables and mixins.
.. seealso::
`Primary variables SCSS
<https://github.com/odoo/odoo/blob/34c0c9c1ae00aba391932129d4cefd027a9c6bbd/addons/website/static/src/scss/primary_variables.scss#L1954>`_
Fonts
~~~~~
You can embed any font on your website. The Website Builder automatically makes them available in
the font selector.
**Declaration**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-theme-font-configs: (
<font-name>: (
'family': <css font family list>,
'url' (optional): <related part of Google fonts URL>,
'properties' (optional): (
<font-alias>: (
<website-value-key>: <value>,
...,
),
...,
)
)
**Use**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-website-values-palettes: (
(
'font': '<font-name>',
'headings-font': '<font-name>',
'navbar-font': '<font-name>',
'buttons-font': '<font-name>',
),
);
Google fonts
************
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-theme-font-configs: (
'Poppins': (
'family': ('Poppins', sans-serif),
'url': 'Poppins:400,500',
'properties' : (
'base': (
'font-size-base': 1rem,
),
),
),
);
Custom fonts
************
First, create a specific SCSS file to declare your custom font(s).
.. code-block:: python
:caption: ``/website_airproof/__manifest__.py``
'assets': {
'web.assets_frontend': [
'website_airproof/static/src/scss/font.scss',
],
},
Then, use the `@font-face` rule to allow you custom font(s) to be loaded on your website.
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/font.scss``
@font-face {
font-family: '<font-name>';
...
}
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-theme-font-configs: (
'Proxima Nova': (
'family': ('Proxima Nova', sans-serif),
'properties' : (
'base': (
'font-size-base': 1rem,
),
),
),
);
.. tip::
It is recommended to use the .woff format for your fonts.
Colors
~~~~~~
The Website Builder relies on palettes composed of five named colors. Defining those in your theme
ensures it stays consistent.
.. list-table::
:header-rows: 1
:stub-columns: 1
:widths: 20 80
* - Color
- Description
* - o-color-1
- Primary
* - o-color-2
- Secondary
* - o-color-3
- Extra
* - o-color-4
- Whitish
* - o-color-5
- Blackish
.. image:: theming/theme-colors.png
:alt: Theme colors
:width: 300
**Declaration**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-color-palettes: map-merge($o-color-palettes,
(
'airproof': (
'o-color-1': #bedb39,
'o-color-2': #2c3e50,
'o-color-3': #f2f2f2,
'o-color-4': #ffffff,
'o-color-5': #000000,
),
)
);
Add the created palette to the list of palettes offered by the Website Builder.
.. code-block:: scss
$o-selected-color-palettes-names: append($o-selected-color-palettes-names, 'airproof');
**Use**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-website-values-palettes: (
(
'color-palettes-name': 'airproof',
),
);
.. image:: theming/theme-colors-airproof.png
:alt: Theme colors Airproof
:width: 800
**Color combinations**
Based on the previously defined five color palettes, the Website Builder automatically generates
five color combinations, each defining a color for the background, text, headings, links, primary
buttons, and secondary buttons. These colors can be customized later by the user.
.. image:: theming/theme-colors-big.png
:alt: Theme colors
:width: 300
The colors used in a color combination are accessible and can be overridden through the BS
`$colors map` using a specific prefix (`o-cc` for `color combination`).
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/primary_variables.scss``
$o-color-palettes: map-merge($o-color-palettes,
(
'airproof': (
'o-cc*-bg': 'o-color-*',
'o-cc*-text': 'o-color-*',
'o-cc*-headings': 'o-color-*',
'o-cc*-h2': 'o-color-*',
'o-cc*-h3': 'o-color-*',
'o-cc*-h4': 'o-color-*',
'o-cc*-h5': 'o-color-*',
'o-cc*-h6': 'o-color-*',
'o-cc*-link': 'o-color-*',
'o-cc*-btn-primary': 'o-color-*',
'o-cc*-btn-primary-border': 'o-color-*',
'o-cc*-btn-secondary': 'o-color-*',
'o-cc*-btn-secondary-border': 'o-color-*',
),
)
);
.. note::
For each `o-cc*`, replace the `*` with the digit (1 - 5) corresponding to the desired color
combination.
The default text color is `o-color-5`. If the background is too dark, it will automatically
change to the `o-color-4` color.
.. seealso::
`Color combinations SCSS
<https://github.com/odoo/odoo/blob/34c0c9c1ae00aba391932129d4cefd027a9c6bbd/addons/web_editor/static/src/scss/web_editor.common.scss#L711>`_
.. admonition:: Demo page
The Website Builder automatically generates a page to view the color combinations of the theme
color palette: http://localhost:8069/website/demo/color-combinations
Bootstrap variables
-------------------
Odoo includes Bootstrap by default. You can use all variables and mixins of the framework.
If Odoo does not provide the variable you are looking for, there could be a Bootstrap variable that
allows it. Indeed all Odoo layouts respect Bootstrap structures and use Bootstrap components or
their extensions. If you customize a Bootstrap variable, you add a generic style for the whole user
website.
Use a dedicated file added to the :file:`_assets_frontend_helpers` bundle to override Bootstrap
values and *not* the :file:`primary_variables.scss` file.
**Declaration**
.. code-block:: python
:caption: ``/website_airproof/__manifest__.py``
'assets': {
'web._assets_frontend_helpers': [
('prepend', 'website_airproof/static/src/scss/bootstrap_overridden.scss'),
],
},
**Use**
.. code-block:: scss
:caption: ``/website_airproof/static/src/scss/bootstrap_overridden.scss``
// Typography
$h1-font-size: 4rem !default;
// Navbar
$navbar-nav-link-padding-x: 1rem!default;
// Buttons + Forms
$input-placeholder-color: o-color('o-color-1') !default;
// Cards
$card-border-width: 0 !default;
.. tip::
That file must only contain definitions and overrides of SCSS variables and mixins.
.. warning::
Don't override Bootstrap variables that depend on Odoo variables. Otherwise, you might break the
possibility for the user to customize them using the Website Builder.
.. seealso::
`Bootstrap overridden SCSS
<{GITHUB_PATH}/addons/website/static/src/scss/bootstrap_overridden.scss>`_
.. admonition:: Demo page
http://localhost:8069/website/demo/bootstrap
Views
-----
For some options, in addition to the Website Builder variable, you also have to activate a specific
view.
By reading the source code, templates related to options are easily found.
.. code-block:: xml
<we-button title="..."
data-name="..."
data-customize-website-views="website.template_header_default"
data-customize-website-variable="'...'"
data-img="..."/>
.. code-block:: xml
<template id="..." inherit_id="..." name="..." active="True"/>
<template id="..." inherit_id="..." name="..." active="False"/>
.. example::
**Changing the menu items' horizontal alignment**
.. code-block:: xml
:caption: ``/website_airproof/data/presets.xml``
<record id="website.template_header_default_align_center" model="ir.ui.view">
<field name="active" eval="True"/>
</record>
The same logic can be used for others Odoo apps as well.
**eCommerce - Display products categories**
.. code-block:: xml
<record id="website_sale.products_categories" model="ir.ui.view">
<field name="active" eval="False"/>
</record>
**Portal - Disable the language selector**
.. code-block:: xml
<record id="portal.footer_language_selector" model="ir.ui.view">
<field name="active" eval="False"/>
</record>
Assets
======
For this part, we will refer to the `assets_frontend` bundle located in the web module. This bundle
specifies the list of assets loaded by the Website Builder, and the goal is to add your SCSS and JS
files to the bundle.
Styles
------
The Website Builder together with Bootstrap are great for defining the basic styles of your website.
But to design something unique, you should go a step further. For this, you can easily add any SCSS
file to your theme.
**Declaration**
.. code-block:: python
:caption: ``/website_airproof/__manifest__.py``
'assets': {
'web.assets_frontend': [
'website_airproof/static/src/scss/theme.scss',
],
},
Feel free to reuse the variables from your Bootstrap file and the ones used by Odoo in your
:file:`theme.scss` file.
.. example::
.. code-block:: javascript
:caption: ``/website_airproof/static/src/scss/theme.scss``
blockquote {
border-radius: $rounded-pill;
color: o-color('o-color-3');
font-family: o-website-value('headings-font');
}
Interactivity
-------------
Odoo supports three different kinds of JavaScript files:
- :ref:`plain JavaScript files <frontend/modules/plain_js>` (no module system),
- :ref:`native JavaScript module <frontend/modules/native_js>`, and
- :ref:`Odoo modules <frontend/modules/odoo_module>` (using a custom module system).
Most new Odoo JavaScript codes should use the native JavaScript module system. It's simpler and
brings the benefit of a better developer experience with better integration with the IDE.
**Declaration**
.. code-block:: python
:caption: ``/website_airproof/__manifest__.py``
'assets': {
'web.assets_frontend': [
'website_airproof/static/src/js/theme.js',
],
},
.. note::
If you want to include files from an external library, you can add them to the :file:`/lib`
folder of your module.
.. tip::
- Use a linter (JSHint, ...).
- Never add minified JavaScript libraries.
- Add `'use strict';` on top of every Odoo JavaScript module.
- Variables and functions should be *camelcased* (`myVariable`) instead of *snakecased*
(`my_variable`).
- Do not name a variable `event`; use `ev.` instead. This is to avoid bugs on non-Chrome
browsers, as Chrome is magically assigning a global event variable (so if you use the event
variable without declaring it, it will work fine on Chrome but crash on every other browser).
- Use strict comparisons (`===` instead of `==`).
- Use double quotes for all textual strings (such as `"Hello"`) and single quotes for all other
strings, such as a CSS selector `.x_nav_item`.
- Always use `this._super.apply(this, arguments)`.
.. seealso::
- `Odoo JavaScript coding guidelines <https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines>`_
- :doc:`Overview of the Odoo JavaScript framework
<../../reference/frontend/javascript_reference>`
- `Odoo Experience Talk: 10 Tips to take your website design to the next level! <https://www.youtube.com/watch?v=vAgE_fPVXUQ&ab_channel=Odoo>`_
@@ -0,0 +1,66 @@
# Translations
With Odoo, you can translate your website into different languages.
In this chapter, you will learn how to:
- Translate the content of a module.
- Import and export translations.
- Integrate translations to a module.
## Frontend
To translate your pages with the Website Builder, go to your website and click on the language
selector to switch to it. If your website was never translated to the target language, click
{guilabel}`Add a language...`, select it in the pop-up window, and click {guilabel}`Add`.
Click {guilabel}`Translate` to start translating. Depending on the language, some text is
automatically translated and highlighted in green, while everything that should be translated
manually is highlighted in yellow.
```{image} translations/translate-button.png
:alt: Translate button
:width: 570
```
## Backend
Translating pages directly from the backend allows you to translate several languages at the same
time. To do so, go to {menuselection}`Settings --> Technical --> User Interface: Views`, search for
the name of the page you want to translate, and click the {guilabel}`Edit Translations` button.
```{image} translations/edit-translations.png
:alt: Edit translations
:width: 718
```
## Export
Once you are done translating, you need to export the translations to integrate them into your
module. To export everything at once, open your database, activate {ref}`developer mode
<developer-mode>`, and go to {menuselection}`Settings --> Translations --> Export Translation`.
Select the {guilabel}`Language` you translated, *PO File* under {guilabel}`File Format`, and
*website_airproof* as the {guilabel}`Apps To Export`.
Download the file and move it to the {file}`i18n` folder. If needed, you can manually edit the
{file}`.po` file afterward.
## PO file
You can translate directly by editing a {file}`.po` file or creating the file yourself. Check out
the {doc}`translating modules documentation <../translations>` to write your translations.
```{code-block} po
:caption: '``/website_coconuts/i18n/fr_BE.po``'
#. module: website_airproof
#: model_terms:ir.ui.view,arch_db:website_airproof.s_custom_snippet
msgid "..."
msgstr "..."
```
## Import
To import your translation files into Odoo, go to {menuselection}`Settings --> Translations -->
Import Translation` and upload them.
@@ -1,69 +0,0 @@
============
Translations
============
With Odoo, you can translate your website into different languages.
In this chapter, you will learn how to:
- Translate the content of a module.
- Import and export translations.
- Integrate translations to a module.
Frontend
========
To translate your pages with the Website Builder, go to your website and click on the language
selector to switch to it. If your website was never translated to the target language, click
:guilabel:`Add a language...`, select it in the pop-up window, and click :guilabel:`Add`.
Click :guilabel:`Translate` to start translating. Depending on the language, some text is
automatically translated and highlighted in green, while everything that should be translated
manually is highlighted in yellow.
.. image:: translations/translate-button.png
:alt: Translate button
:width: 570
Backend
=======
Translating pages directly from the backend allows you to translate several languages at the same
time. To do so, go to :menuselection:`Settings --> Technical --> User Interface: Views`, search for
the name of the page you want to translate, and click the :guilabel:`Edit Translations` button.
.. image:: translations/edit-translations.png
:alt: Edit translations
:width: 718
Export
======
Once you are done translating, you need to export the translations to integrate them into your
module. To export everything at once, open your database, activate :ref:`developer mode
<developer-mode>`, and go to :menuselection:`Settings --> Translations --> Export Translation`.
Select the :guilabel:`Language` you translated, *PO File* under :guilabel:`File Format`, and
*website_airproof* as the :guilabel:`Apps To Export`.
Download the file and move it to the :file:`i18n` folder. If needed, you can manually edit the
:file:`.po` file afterward.
PO file
=======
You can translate directly by editing a :file:`.po` file or creating the file yourself. Check out
the :doc:`translating modules documentation <../translations>` to write your translations.
.. code-block:: po
:caption: ``/website_coconuts/i18n/fr_BE.po``
#. module: website_airproof
#: model_terms:ir.ui.view,arch_db:website_airproof.s_custom_snippet
msgid "..."
msgstr "..."
Import
======
To import your translation files into Odoo, go to :menuselection:`Settings --> Translations -->
Import Translation` and upload them.
+22
View File
@@ -0,0 +1,22 @@
---
hide-page-toc: true
nosearch: true
show-content: true
show-toc: true
---
# Reference
```{toctree}
:maxdepth: 3
reference/backend
reference/frontend
reference/user_interface
reference/standard_modules
reference/cli
reference/upgrades
reference/external_api
reference/extract_api
```
-20
View File
@@ -1,20 +0,0 @@
:nosearch:
:show-content:
:show-toc:
:hide-page-toc:
=========
Reference
=========
.. toctree::
:maxdepth: 3
reference/backend
reference/frontend
reference/user_interface
reference/standard_modules
reference/cli
reference/upgrades
reference/external_api
reference/extract_api
+22
View File
@@ -0,0 +1,22 @@
---
hide-page-toc: true
nosearch: true
---
# Server framework
```{toctree}
:titlesonly: true
backend/orm
backend/data
backend/actions
backend/reports
backend/module
backend/security
backend/performance
backend/testing
backend/http
backend/mixins
```
-20
View File
@@ -1,20 +0,0 @@
:nosearch:
:hide-page-toc:
================
Server framework
================
.. toctree::
:titlesonly:
backend/orm
backend/data
backend/actions
backend/reports
backend/module
backend/security
backend/performance
backend/testing
backend/http
backend/mixins
@@ -0,0 +1,555 @@
# 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
{doc}`views <../user_interface/view_records>`: 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 (list, 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, "list"], [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= `list,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`
```xml
<record model="ir.actions.act_window.view" id="test_action_tree">
<field name="sequence" eval="1"/>
<field name="view_mode">list</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`:
```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)`
```{eval-rst}
.. 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` (default= `new`)
: the available values are :
- `new`: opens the URL in a new window/page
- `self`: opens the URL in the current window/page (replaces the actual content)
- `download`: redirects to a download URL
example:
```
{
"type": "ir.actions.act_url",
"url": "https://odoo.com",
"target": "self",
}
```
This will replace the current content section by the Odoo home page.
(reference-actions-server)=
## Server Actions (`ir.actions.server`)
```{eval-rst}
.. 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 several 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
```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:
```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)=
## Scheduled Actions (`ir.cron`)
Actions triggered automatically on a predefined frequency.
`name`
: Name of the scheduled 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`)
`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 :
```python
model.<method_name>()
```
`nextcall`
: Next planned execution date of this action (date/time format)
`priority`
: Priority of the action when executing multiple actions at the same time
### Advanced use: Batching
When executing a scheduled action, it's recommended to try batching progress in order
to avoid hogging a worker for a long period of time and possibly running into timeout exceptions.
Odoo provides a simple API for scheduled action batching;
```python
self.env['ir.cron']._notify_progress(done=XX:int, remaining=XX:int)
```
This method allows the scheduler to know if progress was made and whether there is
still remaining work that must be done.
By default, if the API is used, the scheduler tries to process 10 batches in one sitting.
If there are still remaining tasks after those 10 batches, a new cron call will be executed as
soon as possible.
### Advanced use: Triggers
For more complex use cases, Odoo provides a more advanced way to trigger
scheduled actions directly from business code.
```python
action_record._trigger(at=XX:date)
```
### Security
To avoid a fair usage of resources among scheduled actions, some security measures ensure the
correct functioning of your scheduled actions.
- If a scheduled action encounters an error or a timeout three consecutive times,
it will skip its current execution and be considered as failed.
- If a scheduled action fails its execution five consecutive times over a period of at least
seven days, it will be deactivated and will notify the DB admin.
@@ -1,483 +0,0 @@
=======
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
:doc:`views <../user_interface/view_records>`: 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 (list, 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, "list"], [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= ``list,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">list</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`` (default= ``new``)
the available values are :
* ``new``: opens the URL in a new window/page
* ``self``: opens the URL in the current window/page (replaces the actual content)
* ``download``: redirects to a download URL
example:
::
{
"type": "ir.actions.act_url",
"url": "https://odoo.com",
"target": "self",
}
This 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 several 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:
Scheduled Actions (``ir.cron``)
===============================
Actions triggered automatically on a predefined frequency.
``name``
Name of the scheduled 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``)
``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)
``priority``
Priority of the action when executing multiple actions at the same time
Advanced use: Batching
----------------------
When executing a scheduled action, it's recommended to try batching progress in order
to avoid hogging a worker for a long period of time and possibly running into timeout exceptions.
Odoo provides a simple API for scheduled action batching;
.. code-block:: python
self.env['ir.cron']._notify_progress(done=XX:int, remaining=XX:int)
This method allows the scheduler to know if progress was made and whether there is
still remaining work that must be done.
By default, if the API is used, the scheduler tries to process 10 batches in one sitting.
If there are still remaining tasks after those 10 batches, a new cron call will be executed as
soon as possible.
Advanced use: Triggers
----------------------
For more complex use cases, Odoo provides a more advanced way to trigger
scheduled actions directly from business code.
.. code-block:: python
action_record._trigger(at=XX:date)
Security
--------
To avoid a fair usage of resources among scheduled actions, some security measures ensure the
correct functioning of your scheduled actions.
- If a scheduled action encounters an error or a timeout three consecutive times,
it will skip its current execution and be considered as failed.
- If a scheduled action fails its execution five consecutive times over a period of at least
seven days, it will be deactivated and will notify the DB admin.
+343
View File
@@ -0,0 +1,343 @@
(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 record 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`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- the root elements of the data file -->
<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.
```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).
```xml
<odoo>
<data noupdate="1">
<record id="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/view_architectures/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.
(reference-data-csvdatafiles)=
## 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 country states
`res.country.state.csv`
```{literalinclude} data/res.country.state.csv
:language: text
```
rendered in a more readable format:
```{eval-rst}
.. csv-table::
:file: data/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
@@ -1,299 +0,0 @@
.. _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 record 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
<?xml version="1.0" encoding="UTF-8"?>
<!-- the root elements of the data file -->
<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 id="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/view_architectures/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.
.. _reference/data/csvdatafiles:
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 country states
``res.country.state.csv``
.. literalinclude:: data/res.country.state.csv
:language: text
rendered in a more readable format:
.. csv-table::
:file: data/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
+100
View File
@@ -0,0 +1,100 @@
(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
```{eval-rst}
.. autodecorator:: odoo.http.route
```
(reference-http-request)=
### Request
The request object is automatically set on {data}`odoo.http.request` at
the start of the request.
```{eval-rst}
.. autoclass:: odoo.http.Request
:members:
:member-order: bysource
```
```{eval-rst}
.. autoclass:: odoo.http.JsonRPCDispatcher
:members:
:member-order: bysource
```
```{eval-rst}
.. autoclass:: odoo.http.HttpDispatcher
:members:
:member-order: bysource
```
### Response
```{eval-rst}
.. 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:
```
@@ -1,89 +0,0 @@
.. _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
-------
.. autodecorator:: 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.Request
:members:
:member-order: bysource
.. autoclass:: odoo.http.JsonRPCDispatcher
:members:
:member-order: bysource
.. autoclass:: odoo.http.HttpDispatcher
:members:
:member-order: bysource
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:
@@ -0,0 +1,185 @@
# 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.
:::{note}
Module `base` is always installed in any Odoo instance.
But you still need to specify it as dependency to make sure your module is updated when `base` is updated.
:::
`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` or `list(str)`, 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 synergetic 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.
If it is a list, it must contain a subset of the dependencies. This module will automatically be
installed as soon as all the dependencies in the subset are installed. The remaining
dependencies will be automatically installed as well. If the list is empty, this module will
always be automatically installed regardless of its dependencies and these will be installed as
well.
`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.
`assets` (`dict`)
: A definition of how all static files are loaded in various assets bundles.
See the {ref}`assets <reference/assets>` page for more details on how to
describe bundles.
`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`)
: Deprecated. Replaced by `auto_install`.
[existing categories]: {GITHUB_PATH}/odoo/addons/base/data/ir_module_category_data.xml
[semantic versioning]: https://semver.org
@@ -1,157 +0,0 @@
================
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.
.. note::
Module `base` is always installed in any Odoo instance.
But you still need to specify it as dependency to make sure your module is updated when `base` is updated.
``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`` or ``list(str)``, 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 synergetic 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.
If it is a list, it must contain a subset of the dependencies. This module will automatically be
installed as soon as all the dependencies in the subset are installed. The remaining
dependencies will be automatically installed as well. If the list is empty, this module will
always be automatically installed regardless of its dependencies and these will be installed as
well.
``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.
``assets`` (``dict``)
A definition of how all static files are loaded in various assets bundles.
See the :ref:`assets <reference/assets>` page for more details on how to
describe bundles.
``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``)
Deprecated. Replaced by ``auto_install``.
.. _semantic versioning: https://semver.org
.. _existing categories: {GITHUB_PATH}/odoo/addons/base/data/ir_module_category_data.xml
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
(reference-orm-changelog)=
# Changelog
## Odoo version 18.0
- Searching by name is now implemented as `_search_display_name` like all other fields.
See [#174967](https://github.com/odoo/odoo/pull/174967).
- New methods to check access rights and rules now combine both access rights
and rules: `check_access`, `has_access` and `_filtered_access`.
See [#179148](https://github.com/odoo/odoo/pull/179148).
- Translations are made available from the `Environment` with [#174844](https://github.com/odoo/odoo/pull/174844).
## Odoo Online version 17.4
- The internal operator `inselect` is removed. The alternative is to use `in`
with a Query or SQL object. [#171371](https://github.com/odoo/odoo/pull/171371).
## Odoo Online version 17.3
- We can now group by date parts numbers in `read_group`, `_read_group` and domains with [#159528](https://github.com/odoo/odoo/pull/159528).
## Odoo Online version 17.2
- The {attr}`group_operator` attribute of {class}`~odoo.fields.Field` is renamed into
{attr}`aggregator` with [#127353](https://github.com/odoo/odoo/pull/127353).
- We can now group/aggregate/order by related no-store field with
[#127353](https://github.com/odoo/odoo/pull/127353).
## Odoo Online version 17.1
- Method {meth}`~odoo.models.Model._flush_search` has been deprecated with
[#144747](https://github.com/odoo/odoo/pull/144747).
The flushing of fields is now done by {meth}`~odoo.api.Environment.execute_query`,
and is based on metadata put in the {class}`~odoo.tools.SQL` object by
{meth}`~odoo.models.BaseModel._search` and other low-level ORM methods that
build such objects. Those methods are also responsible for checking the access
rights on the fields that are used in the SQL object.
## Odoo version 17.0
- Introduce an {class}`~odoo.tools.SQL` wrapper object to make SQL composition
easier and safer with respect to SQL injections. Methods of the ORM now use it
internally. Introduced by [#134677](https://github.com/odoo/odoo/pull/134677).
## Odoo Online version 16.4
- Method {meth}`~odoo.models.Model.name_get` has been deprecated with
[#122085](https://github.com/odoo/odoo/pull/122085).
Read field `display_name` instead.
## Odoo Online version 16.3
- Method {meth}`~odoo.models.Model._read_group` has a new signature with
[#110737](https://github.com/odoo/odoo/pull/110737)
## Odoo Online version 16.2
- Refactor the implementation of searching and reading methods to be able to
combine both in a minimal number of SQL queries. We introduce two new methods
{meth}`~odoo.models.Model.search_fetch` and {meth}`~odoo.models.Model.fetch`
that take advantage of the combination. More details can be found on the pull
request [#112126](https://github.com/odoo/odoo/pull/112126).
## Odoo version 16.0
- Translations for translated fields are stored as JSONB values with
[#97692](https://github.com/odoo/odoo/pull/97692)
and [#101115](https://github.com/odoo/odoo/pull/101115).
Code translations are no longer stored into the database.
They become static and are extracted from the PO files when needed.
- {meth}`~odoo.models.Model.search_count` takes the {attr}`limit` argument into account with [#95589](https://github.com/odoo/odoo/pull/95589).
It limits the number of records to count, improving performance when a partial result is acceptable.
## Odoo Online version 15.4
- New API for flushing to the database and invalidating the cache with
[#87527](https://github.com/odoo/odoo/pull/87527).
New methods have been added to `odoo.models.Model` and `odoo.api.Environment`,
and are less confusing about what is actually done in each case.
See the section {ref}`SQL Execution <reference/orm/sql>`.
## Odoo Online version 15.3
- The argument `args` is renamed to `domain` for {meth}`~odoo.models.Model.search`, {meth}`~odoo.models.Model.search_count`
and {meth}`~odoo.models.Model._search`. [#83687](https://github.com/odoo/odoo/pull/83687)
- {meth}`~odoo.models.Model.filtered_domain` conserves the order of the current recordset. [#83687](https://github.com/odoo/odoo/pull/83687)
- {meth}`~odoo.models.Model.browse` does not accept {class}`str` as `ids`. [#83687](https://github.com/odoo/odoo/pull/83687)
- The methods {meth}`~odoo.models.Model.fields_get_keys` and {meth}`~odoo.models.Model.get_xml_id` on {class}`~odoo.models.Model` are deprecated. [#83687](https://github.com/odoo/odoo/pull/83687)
- The method {meth}`~odoo.models.Model._mapped_cache` is removed. [#83687](https://github.com/odoo/odoo/pull/83687)
- Remove the {attr}`limit` attribute of {class}`~odoo.fields.One2many` and {class}`~odoo.fields.Many2many`. [#83687](https://github.com/odoo/odoo/pull/83687)
## Odoo Online version 15.2
- Specific index types on fields: With [#83274](https://github.com/odoo/odoo/pull/83274) and
[#83015](https://github.com/odoo/odoo/pull/83015), developers can now define what type of
indexes can be used on fields by PostgreSQL. See the {ref}`index property <reference/fields>` of
`odoo.fields.Field`.
- The {attr}`_sequence` attribute of {class}`~odoo.models.Model` is removed. Odoo lets PostgreSQL use the default sequence of the primary key. [#82727](https://github.com/odoo/odoo/pull/82727)
- The method {meth}`~odoo.models.Model._write` does not raise an error for non-existing records. [#82727](https://github.com/odoo/odoo/pull/82727)
- The {attr}`column_format` and {attr}`deprecated` attributes of {class}`~odoo.fields.Field` are removed. [#82727](https://github.com/odoo/odoo/pull/82727)
@@ -1,119 +0,0 @@
.. _reference/orm/changelog:
=========
Changelog
=========
Odoo version 18.0
=================
- Searching by name is now implemented as `_search_display_name` like all other fields.
See `#174967 <https://github.com/odoo/odoo/pull/174967>`_.
- New methods to check access rights and rules now combine both access rights
and rules: `check_access`, `has_access` and `_filtered_access`.
See `#179148 <https://github.com/odoo/odoo/pull/179148>`_.
- Translations are made available from the `Environment` with `#174844 <https://github.com/odoo/odoo/pull/174844>`_.
Odoo Online version 17.4
========================
- The internal operator `inselect` is removed. The alternative is to use `in`
with a Query or SQL object. `#171371 <https://github.com/odoo/odoo/pull/171371>`_.
Odoo Online version 17.3
========================
- We can now group by date parts numbers in `read_group`, `_read_group` and domains with `#159528 <https://github.com/odoo/odoo/pull/159528>`_.
Odoo Online version 17.2
========================
- The :attr:`group_operator` attribute of :class:`~odoo.fields.Field` is renamed into
:attr:`aggregator` with `#127353 <https://github.com/odoo/odoo/pull/127353>`_.
- We can now group/aggregate/order by related no-store field with
`#127353 <https://github.com/odoo/odoo/pull/127353>`_.
Odoo Online version 17.1
========================
- Method :meth:`~odoo.models.Model._flush_search` has been deprecated with
`#144747 <https://github.com/odoo/odoo/pull/144747>`_.
The flushing of fields is now done by :meth:`~odoo.api.Environment.execute_query`,
and is based on metadata put in the :class:`~odoo.tools.SQL` object by
:meth:`~odoo.models.BaseModel._search` and other low-level ORM methods that
build such objects. Those methods are also responsible for checking the access
rights on the fields that are used in the SQL object.
Odoo version 17.0
=================
- Introduce an :class:`~odoo.tools.SQL` wrapper object to make SQL composition
easier and safer with respect to SQL injections. Methods of the ORM now use it
internally. Introduced by `#134677 <https://github.com/odoo/odoo/pull/134677>`_.
Odoo Online version 16.4
========================
- Method :meth:`~odoo.models.Model.name_get` has been deprecated with
`#122085 <https://github.com/odoo/odoo/pull/122085>`_.
Read field `display_name` instead.
Odoo Online version 16.3
========================
- Method :meth:`~odoo.models.Model._read_group` has a new signature with
`#110737 <https://github.com/odoo/odoo/pull/110737>`_
Odoo Online version 16.2
========================
- Refactor the implementation of searching and reading methods to be able to
combine both in a minimal number of SQL queries. We introduce two new methods
:meth:`~odoo.models.Model.search_fetch` and :meth:`~odoo.models.Model.fetch`
that take advantage of the combination. More details can be found on the pull
request `#112126 <https://github.com/odoo/odoo/pull/112126>`_.
Odoo version 16.0
=================
- Translations for translated fields are stored as JSONB values with
`#97692 <https://github.com/odoo/odoo/pull/97692>`_
and `#101115 <https://github.com/odoo/odoo/pull/101115>`_.
Code translations are no longer stored into the database.
They become static and are extracted from the PO files when needed.
- :meth:`~odoo.models.Model.search_count` takes the :attr:`limit` argument into account with `#95589 <https://github.com/odoo/odoo/pull/95589>`_.
It limits the number of records to count, improving performance when a partial result is acceptable.
Odoo Online version 15.4
========================
- New API for flushing to the database and invalidating the cache with
`#87527 <https://github.com/odoo/odoo/pull/87527>`_.
New methods have been added to `odoo.models.Model` and `odoo.api.Environment`,
and are less confusing about what is actually done in each case.
See the section :ref:`SQL Execution <reference/orm/sql>`.
Odoo Online version 15.3
========================
- The argument `args` is renamed to `domain` for :meth:`~odoo.models.Model.search`, :meth:`~odoo.models.Model.search_count`
and :meth:`~odoo.models.Model._search`. `#83687 <https://github.com/odoo/odoo/pull/83687>`_
- :meth:`~odoo.models.Model.filtered_domain` conserves the order of the current recordset. `#83687 <https://github.com/odoo/odoo/pull/83687>`_
- :meth:`~odoo.models.Model.browse` does not accept :class:`str` as `ids`. `#83687 <https://github.com/odoo/odoo/pull/83687>`_
- The methods :meth:`~odoo.models.Model.fields_get_keys` and :meth:`~odoo.models.Model.get_xml_id` on :class:`~odoo.models.Model` are deprecated. `#83687 <https://github.com/odoo/odoo/pull/83687>`_
- The method :meth:`~odoo.models.Model._mapped_cache` is removed. `#83687 <https://github.com/odoo/odoo/pull/83687>`_
- Remove the :attr:`limit` attribute of :class:`~odoo.fields.One2many` and :class:`~odoo.fields.Many2many`. `#83687 <https://github.com/odoo/odoo/pull/83687>`_
Odoo Online version 15.2
========================
- Specific index types on fields: With `#83274 <https://github.com/odoo/odoo/pull/83274>`_ and
`#83015 <https://github.com/odoo/odoo/pull/83015>`_, developers can now define what type of
indexes can be used on fields by PostgreSQL. See the :ref:`index property <reference/fields>` of
`odoo.fields.Field`.
- The :attr:`_sequence` attribute of :class:`~odoo.models.Model` is removed. Odoo lets PostgreSQL use the default sequence of the primary key. `#82727 <https://github.com/odoo/odoo/pull/82727>`_
- The method :meth:`~odoo.models.Model._write` does not raise an error for non-existing records. `#82727 <https://github.com/odoo/odoo/pull/82727>`_
- The :attr:`column_format` and :attr:`deprecated` attributes of :class:`~odoo.fields.Field` are removed. `#82727 <https://github.com/odoo/odoo/pull/82727>`_
@@ -1,15 +1,16 @@
:custom-css: performance.css
---
custom-css: performance.css
---
===========
Performance
===========
# Performance
.. _performance/profiling:
(performance-profiling)=
Profiling
=========
## Profiling
```{eval-rst}
.. currentmodule:: odoo.tools.profiler
```
Profiling is about analysing the execution of a program and measure aggregated data. These data can
be the elapsed time for each function, the executed SQL queries...
@@ -19,19 +20,18 @@ in finding performance issues and identifying which part of the program is respo
Odoo provides an integrated profiling tool that allows recording all executed queries and stack
traces during execution. It can be used to profile either a set of requests of a user session, or a
specific portion of code. Profiling results can be either inspected with the integrated `speedscope
<https://github.com/jlfwong/speedscope>`_ :dfn:`open source app allowing to visualize a flamegraph`
specific portion of code. Profiling results can be either inspected with the integrated [speedscope](https://github.com/jlfwong/speedscope) {dfn}`open source app allowing to visualize a flamegraph`
view or analyzed with custom tools by first saving them in a JSON file or in the database.
.. _performance/profiling/enable:
(performance-profiling-enable)=
Enable the profiler
-------------------
### Enable the profiler
The profiler can either be enabled from the user interface, which is the easiest way to do so but
allows profiling only web requests, or from Python code, which allows profiling any piece of code
including tests.
```{eval-rst}
.. tabs::
.. tab:: Enable from the user interface
@@ -119,24 +119,26 @@ including tests.
for index in range(max_index):
with ExecutionContext(current_index=index): # Identify each call in speedscope results.
do_stuff()
```
.. _performance/profiling/analyse:
(performance-profiling-analyse)=
Analyse the results
-------------------
### Analyse the results
To browse the profiling results, make sure that the :ref:`profiler is enabled globally on the
database <performance/profiling/enable>`, then open the :ref:`developer mode tools
To browse the profiling results, make sure that the {ref}`profiler is enabled globally on the
database <performance/profiling/enable>`, then open the {ref}`developer mode tools
<developer-mode/tools>` and click on the button in the top-right corner of the profiling
section. A list view of the `ir.profile` records grouped by profiling session opens.
.. image:: performance/profiling_web.png
:align: center
```{image} performance/profiling_web.png
:align: center
```
Each record has a clickable link that opens the speedscope results in a new tab.
.. image:: performance/flamegraph_example.png
:align: center
```{image} performance/flamegraph_example.png
:align: center
```
Speedscope falls out of the scope of this documentation but there are a lot of tools to try: search,
highlight of similar frames, zoom on frame, timeline, left heavy, sandwich view...
@@ -144,39 +146,41 @@ highlight of similar frames, zoom on frame, timeline, left heavy, sandwich view.
Depending on the profiling options that were activated, Odoo generates different view modes that you
can access from the top menu.
.. image:: performance/speedscope_modes.png
:align: center
```{image} performance/speedscope_modes.png
:align: center
```
- The :guilabel:`Combined` view shows all the SQL queries and traces merged togethers.
- The :guilabel:`Combined no context` view shows the same result but ignores the saved execution
context <performance/profiling/enable>`.
- The :guilabel:`sql (no gap)` view shows all the SQL queries as if they were executed one after
- The {guilabel}`Combined` view shows all the SQL queries and traces merged togethers.
- The {guilabel}`Combined no context` view shows the same result but ignores the saved execution
context \<performance/profiling/enable>\`.
- The {guilabel}`sql (no gap)` view shows all the SQL queries as if they were executed one after
another, without any Python logic. This is useful for optimizing SQL only.
- The :guilabel:`sql (density)` view shows only all the SQL queries, leaving gap between them. This
- The {guilabel}`sql (density)` view shows only all the SQL queries, leaving gap between them. This
can be useful to spot if eiter SQL or Python code is the problem, and to identify zones in where
many small queries could be batched.
- The :guilabel:`frames` view shows the results of only the :ref:`periodic collector
- The {guilabel}`frames` view shows the results of only the {ref}`periodic collector
<performance/profiling/collectors/periodic>`.
.. important::
Even though the profiler has been designed to be as light as possible, it can still impact
performance, especially when using the :ref:`Sync collector
<performance/profiling/collectors/sync>`. Keep that in mind when analyzing speedscope results.
:::{important}
Even though the profiler has been designed to be as light as possible, it can still impact
performance, especially when using the {ref}`Sync collector
<performance/profiling/collectors/sync>`. Keep that in mind when analyzing speedscope results.
:::
.. _performance/profiling/collectors:
(performance-profiling-collectors)=
Collectors
----------
### Collectors
Whereas the profiler is about the *when* of profiling, the collectors take care of the *what*.
Each collector specializes in collecting profiling data in its own format and manner. They can be
individually enabled from the user interface through their dedicated toggle button in the
:ref:`developer mode tools <developer-mode/tools>`, or from Python code through their key or
{ref}`developer mode tools <developer-mode/tools>`, or from Python code through their key or
class.
There are currently four collectors available in Odoo:
```{eval-rst}
.. list-table::
:header-rows: 1
@@ -200,62 +204,66 @@ There are currently four collectors available in Odoo:
- No
- `traces_sync`
- `SyncCollector`
```
By default, the profiler enables the SQL and the Periodic collectors. Both when it is enabled from
the user interface or Python code.
.. _performance/profiling/collectors/sql:
(performance-profiling-collectors-sql)=
SQL collector
~~~~~~~~~~~~~
#### SQL collector
The SQL collector saves all the SQL queries made to the database in the current thread (for all
cursors), as well as the stack trace. The overhead of the collector is added to the analysed thread
for each query, which means that using it on a lot of small queries may impact execution time and
other profilers.
It is especially useful to debug query counts, or to add information to the :ref:`Periodic collector
It is especially useful to debug query counts, or to add information to the {ref}`Periodic collector
<performance/profiling/collectors/periodic>` in the combined speedscope view.
```{eval-rst}
.. autoclass:: SQLCollector
```
.. _performance/profiling/collectors/periodic:
(performance-profiling-collectors-periodic)=
Periodic collector
~~~~~~~~~~~~~~~~~~
#### Periodic collector
This collector runs in a separate thread and saves the stack trace of the analysed thread at every
interval. The interval (by default 10 ms) can be defined through the :guilabel:`Interval` option in
interval. The interval (by default 10 ms) can be defined through the {guilabel}`Interval` option in
the user interface, or the `interval` parameter in Python code.
.. warning::
If the interval is set at a very low value, profiling long requests will generate memory issues.
If the interval is set at a very high value, information on short function executions will be
lost.
:::{warning}
If the interval is set at a very low value, profiling long requests will generate memory issues.
If the interval is set at a very high value, information on short function executions will be
lost.
:::
It is one of the best way to analyse performance as it should have a very low impact on the
execution time thanks to its separate thread.
```{eval-rst}
.. autoclass:: PeriodicCollector
```
.. _performance/profiling/collectors/qweb:
(performance-profiling-collectors-qweb)=
QWeb collector
~~~~~~~~~~~~~~
#### QWeb collector
This collector saves the Python execution time and queries of all directives. As for the :ref:`SQL
This collector saves the Python execution time and queries of all directives. As for the {ref}`SQL
collector <performance/profiling/collectors/sql>`, the overhead can be important when executing a
lot of small directives. The results are different from other collectors in terms of collected data,
and can be analysed from the `ir.profile` form view using a custom widget.
It is mainly useful for optimizing views.
```{eval-rst}
.. autoclass:: QwebCollector
```
.. _performance/profiling/collectors/sync:
(performance-profiling-collectors-sync)=
Sync collector
~~~~~~~~~~~~~~
#### Sync collector
This collector saves the stack for every function's call and return and runs on the same thread,
which greatly impacts performance.
@@ -263,22 +271,23 @@ which greatly impacts performance.
It can be useful to debug and understand complex flows, and follow their execution in the code. It
is however not recommended for performance analysis because the overhead is high.
```{eval-rst}
.. autoclass:: SyncCollector
```
.. _performance/profiling/pitfalls:
(performance-profiling-pitfalls)=
Performance pitfalls
--------------------
### Performance pitfalls
- Be careful with randomness. Multiple executions may lead to different results. E.g., a garbage
collector being triggered during execution.
- Be careful with blocking calls. In some cases, external `c_call` may take some time before
releasing the GIL, thus leading to unexpected long frames with the :ref:`Periodic collector
releasing the GIL, thus leading to unexpected long frames with the {ref}`Periodic collector
<performance/profiling/collectors/periodic>`. This should be detected by the profiler and give a
warning. It is possible to trigger the profiler manually before such calls if needed.
- Pay attention to the cache. Profiling before that the `view`/`assets`/... are in cache can lead to
different results.
- Be aware of the profiler's overhead. The :ref:`SQL collector
- Be aware of the profiler's overhead. The {ref}`SQL collector
<performance/profiling/collectors/sql>`'s overhead can be important when a lot of small queries
are executed. Profiling is practical to spot a problem but you may want to disable the profiler in
order to measure the real impact of a code change.
@@ -287,18 +296,17 @@ Performance pitfalls
results, which can lead to an HTTP 500 error. In this case, you may need to start the server with
a higher memory limit: `--limit-memory-hard $((8*1024**3))`.
.. _performance/good_practices:
(performance-good-practices)=
Good practices
==============
## Good practices
.. _performance/good_practices/batch:
(performance-good-practices-batch)=
Batch operations
----------------
### Batch operations
When working with recordsets, it is almost always better to batch operations.
```{eval-rst}
.. example::
Don't call a method that runs SQL queries while looping over a recordset because it will do so
for each record of the set.
@@ -327,7 +335,9 @@ When working with recordsets, it is almost always better to batch operations.
.. note::
This example is not optimal nor correct in all cases. It is only a substitute for a
`search_count`. Another solution could be to prefetch and count the inverse `One2many` field.
```
```{eval-rst}
.. example::
Don't create records one after another.
@@ -347,7 +357,9 @@ When working with recordsets, it is almost always better to batch operations.
for name in ['foo', 'bar']:
create_values.append({'name': name})
records = model.create(create_values)
```
```{eval-rst}
.. example::
Fail to prefetch the fields of a recordset while browsing a single record inside a loop.
@@ -376,17 +388,18 @@ When working with recordsets, it is almost always better to batch operations.
for values in values_list:
message = self.browse(values['id']).with_prefetch(self.ids)
```
.. _performance/good_practices/algorithmic_complexity:
(performance-good-practices-algorithmic-complexity)=
Reduce the algorithmic complexity
---------------------------------
### Reduce the algorithmic complexity
Algorithmic complexity is a measure of how long an algorithm would take to complete in regard to the
size `n` of the input. When the complexity is high, the execution time can grow quickly as the input
becomes larger. In some cases, the algorithmic complexity can be reduced by preparing the input's
data correctly.
```{eval-rst}
.. example::
For a given problem, let's consider a naive algorithm crafted with two nested loops for which the
complexity in in O(n²).
@@ -408,7 +421,9 @@ data correctly.
mapped_result = {result['id']: result['foo'] for result in results}
for record in self:
record.foo = mapped_result.get(record.id)
```
```{eval-rst}
.. example::
Choosing the bad data structure to hold the input can lead to quadratic complexity.
@@ -440,19 +455,21 @@ data correctly.
invalid_ids = self.search(domain)
for record in self - invalid_ids:
...
```
.. _performance/good_practices/index:
(performance-good-practices-index)=
Use indexes
-----------
### Use indexes
Database indexes can help fasten search operations, be it from a search in the or through the user
interface.
.. code-block:: python
```python
name = fields.Char(string="Name", index=True)
```
name = fields.Char(string="Name", index=True)
:::{warning}
Be careful not to index every field as indexes consume space and impact on performance when
executing one of `INSERT`, `UPDATE`, and `DELETE`.
:::
.. warning::
Be careful not to index every field as indexes consume space and impact on performance when
executing one of `INSERT`, `UPDATE`, and `DELETE`.
@@ -0,0 +1,363 @@
```{highlight} xml
```
(reference-reports)=
(reference-reports-report)=
# QWeb Reports
Reports are written in HTML/QWeb, like website views in Odoo. You can use
the usual {ref}`QWeb control flow tools <reference/qweb>`. The PDF rendering
itself is performed by [wkhtmltopdf].
Reports are declared using a {ref}`report action <reference/actions/report>`,
and a {ref}`reference/reports/templates` for the action to use.
If useful or necessary, it is possible to specify a
{ref}`reference/reports/paper_formats` for the report report.
(reference-reports-templates)=
## Report template
Report templates will always provide the following variables:
`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
`website`
: the current website object, if any (this item can be present but `None`)
`web_base_url`
: the base url for the webserver
`context_timestamp`
: a function taking {class}`python:datetime.datetime` in UTC[^unzoned] and
converting it to the timezone of the user printing the report
### 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.
By default, the rendering context will also expose the following items:
`docs`
: records for the current report
`doc_ids`
: list of ids for the `docs` records
`doc_model`
: model for the `docs` records
If you wish to access other records/models in the template, you will need
{ref}`a custom report <reference/reports/custom_reports>`, however in that case
you will have to provide the items above if you need them.
### 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`):
```html
<img t-att-src="'/report/barcode/QR/%s' % 'My text in qr code'"/>
```
More parameters can be passed as a query string
```html
<img t-att-src="'/report/barcode/?
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
By default, the reporting system builds rendering values based on the target
model specified through the `model` field.
However, it will first look for a model named
{samp}`report.{module.report_name}` and call that model's
`_get_report_values(doc_ids, data)` in order to prepare the rendering data for
the template.
This can be used to include arbitrary items to use or display while rendering
the template, such as data from additional models:
```python
from odoo import api, models
class ParticularReport(models.AbstractModel):
_name = 'report.module.report_name'
def _get_report_values(self, docids, data=None):
# get the report action back as we will need its data
report = self.env['ir.actions.report']._get_report_from_name('module.report_name')
# get the records selected for this rendering of the report
obj = self.env[report.model].browse(docids)
# return a custom rendering context
return {
'lines': docids.get_lines()
}
```
:::{warning}
When using a custom report, the "default" document-related items
(`doc_ids`, `doc_model` and `docs`) will *not* be included. If you
want them, you will need to include them yourself.
In the example above, the rendering context will contain the "global" values
as well as the `lines` we put in there but nothing else.
:::
(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/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
[^unzoned]: it does not matter what timezone the {class}`python:datetime`
object is actually in (including no timezone), its timezone will
unconditionally be *set* to UTC before being adjusted to the
user's
[wkhtmltopdf]: https://wkhtmltopdf.org
@@ -1,327 +0,0 @@
.. highlight:: xml
.. _reference/reports:
.. _reference/reports/report:
============
QWeb Reports
============
Reports are written in HTML/QWeb, like website views in Odoo. You can use
the usual :ref:`QWeb control flow tools <reference/qweb>`. The PDF rendering
itself is performed by wkhtmltopdf_.
Reports are declared using a :ref:`report action <reference/actions/report>`,
and a :ref:`reference/reports/templates` for the action to use.
If useful or necessary, it is possible to specify a
:ref:`reference/reports/paper_formats` for the report report.
.. _reference/reports/templates:
Report template
===============
Report templates will always provide the following variables:
``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
``website``
the current website object, if any (this item can be present but ``None``)
``web_base_url``
the base url for the webserver
``context_timestamp``
a function taking :class:`python:datetime.datetime` in UTC\ [#unzoned]_ and
converting it to the timezone of the user printing the report
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.
By default, the rendering context will also expose the following items:
``docs``
records for the current report
``doc_ids``
list of ids for the ``docs`` records
``doc_model``
model for the ``docs`` records
If you wish to access other records/models in the template, you will need
:ref:`a custom report <reference/reports/custom_reports>`, however in that case
you will have to provide the items above if you need them.
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/?
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
==============
By default, the reporting system builds rendering values based on the target
model specified through the ``model`` field.
However, it will first look for a model named
:samp:`report.{module.report_name}` and call that model's
``_get_report_values(doc_ids, data)`` in order to prepare the rendering data for
the template.
This can be used to include arbitrary items to use or display while rendering
the template, such as data from additional models:
.. code-block:: python
from odoo import api, models
class ParticularReport(models.AbstractModel):
_name = 'report.module.report_name'
def _get_report_values(self, docids, data=None):
# get the report action back as we will need its data
report = self.env['ir.actions.report']._get_report_from_name('module.report_name')
# get the records selected for this rendering of the report
obj = self.env[report.model].browse(docids)
# return a custom rendering context
return {
'lines': docids.get_lines()
}
.. warning::
When using a custom report, the "default" document-related items
(``doc_ids``, ``doc_model`` and ``docs``) will *not* be included. If you
want them, you will need to include them yourself.
In the example above, the rendering context will contain the "global" values
as well as the ``lines`` we put in there but nothing else.
.. _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/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
.. [#unzoned] it does not matter what timezone the :class:`python:datetime`
object is actually in (including no timezone), its timezone will
unconditionally be *set* to UTC before being adjusted to the
user's
.. _wkhtmltopdf: https://wkhtmltopdf.org
@@ -0,0 +1,573 @@
(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 mechanisms to users.
```{eval-rst}
.. class:: res.groups
.. attribute:: name
serves as user-readable identification for the group (spells out the
role / purpose of the group)
.. attribute:: category_id
The *module category*, serves to associate groups with an Odoo App
(~a set of related business models) and convert them into an exclusive
selection in the user form.
.. todo:: clarify & document special cases & relationship between
groups & categories better
.. attribute:: implied_ids
Other groups to set on the user alongside this one. This is a
convenience pseudo-inheritance relationship: it's possible to
explicitly remove implied groups from a user without removing the
implier.
.. attribute:: comment
Additional notes on the group e.g.
```
(reference-security-acl)=
## Access Rights
*Grants* access to an entire model for a given set of operations. If no access
rights matches an operation on a model for a user (through their group), the
user doesn't have access.
Access rights are additive, a user's accesses are the union of the accesses
they get through all their groups e.g. given a user who is part of group A
granting read and create access and a group B granting update access, the user
will have all three of create, read, and update.
```{eval-rst}
.. class:: ir.model.access
.. attribute:: name
The purpose or role of the group.
.. attribute:: model_id
The model whose access the ACL controls.
.. attribute:: group_id
The :class:`res.groups` to which the accesses are granted, an empty
:attr:`group_id` means the ACL is granted to *every user*
(non-employees e.g. portal or public users).
The :samp:`perm_{method}` attributes grant the corresponding CRUD access
when set, they are all unset by default.
.. attribute:: perm_create
.. attribute:: perm_read
.. attribute:: perm_write
.. attribute:: perm_unlink
```
(reference-security-rules)=
## Record Rules
Record rules are *conditions* which must be satisfied in order for an operation
to be allowed. Record rules are evaluated record-by-record, following access
rights.
Record rules are default-allow: if access rights grant access and no rule
applies to the operation and model for the user, the access is granted.
```{eval-rst}
.. class:: ir.rule
.. attribute:: name
The description of the rule.
.. attribute:: model_id
The model to which the rule applies.
.. attribute:: groups
The :class:`res.groups` to which access is granted (or not). Multiple
groups can be specified. If no group is specified, the rule is *global*
which is treated differently than "group" rules (see below).
.. attribute:: global
Computed on the basis of :attr:`groups`, provides easy access to the
global status (or not) of the rule.
.. attribute:: domain_force
A predicate specified as a :ref:`domain <reference/orm/domains>`, the
rule allows the selected operations if the domain matches the record,
and forbids it otherwise.
The domain is a *python expression* which can use the following
variables:
``time``
Python's :mod:`python:time` module.
``user``
The current user, as a singleton recordset.
``company_id``
The current user's currently selected company as a single company id
(not a recordset).
``company_ids``
All the companies to which the current user has access as a list of
company ids (not a recordset), see
:ref:`howto/company/security` for more details.
The :samp:`perm_{method}` have completely different semantics than for
:class:`ir.model.access`: for rules, they specify which operation the rules
applies *for*. If an operation is not selected, then the rule is not checked
for it, as if the rule did not exist.
All operations are selected by default.
.. attribute:: perm_create
.. attribute:: perm_read
.. attribute:: perm_write
.. attribute:: perm_unlink
```
(reference-security-rules-global)=
### Global rules versus group rules
There is a large difference between global and group rules in how they compose
and combine:
- Global rules *intersect*, if two global rules apply then *both* must be
satisfied for the access to be granted, this means adding global rules always
restricts access further.
- Group rules *unify*, if two group rules apply then *either* can be
satisfied for the access to be granted. This means adding group rules can
expand access, but not beyond the bounds defined by global rules.
- The global and group rulesets *intersect*, which means the first group rule
being added to a given global ruleset will restrict access.
:::{danger}
Creating multiple global rules is risky as it's possible to create
non-overlapping rulesets, which will remove all access.
:::
(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
```{eval-rst}
.. todo::
field access groups apply to the Superuser in fields_get but not in
read/write...
```
(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
<api/external_api/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.
```python
# this method is public and its arguments can not be trusted
def action_done(self):
if self.state == "draft" and self.env.user.has_group('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.
```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 !
```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.
```javascript
QWeb.render('insecure_template', {
info_message: "You have an <strong>important</strong> notification",
})
```
```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.
```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.
```javascript
QWeb.render('secure_template', {
message: "You have an important notification on the product:",
subject: product.name
})
```
```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>
```
```css
.subject {
font-weight: bold;
}
```
#### Creating safe content using {class}`~markupsafe.Markup`
See the [official documentation](https://markupsafe.palletsprojects.com/) for
explanations, but the big advantage of
{class}`~markupsafe.Markup` is that it's a very rich type overrinding
{class}`str` operations to *automatically escape parameters*.
This means that it's easy to create *safe* html snippets by using
{class}`~markupsafe.Markup` on a string literal and "formatting in"
user-provided (and thus potentially unsafe) content:
```pycon
>>> Markup('<em>Hello</em> ') + '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
>>> Markup('<em>Hello</em> %s') % '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
```
though it is a very good thing, note that the effects can be odd at times:
```pycon
>>> Markup('<a>').replace('>', 'x')
Markup('<a>')
>>> Markup('<a>').replace(Markup('>'), 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', '&')
Markup('<a&amp;')
```
:::{tip}
Most of the content-safe APIs actually return a
{class}`~markupsafe.Markup` with all that implies.
:::
The {class}`~markupsafe.escape` method (and its
alias {class}`~odoo.tools.misc.html_escape`) turns a `str` into
a {class}`~markupsafe.Markup` and escapes its content. It will not escape the
content of a {class}`~markupsafe.Markup` object.
```python
def get_name(self, to_html=False):
if to_html:
return Markup("<strong>%s</strong>") % self.name # escape the name
else:
return self.name
>>> record.name = "<R&D>"
>>> escape(record.get_name())
Markup("&lt;R&amp;D&gt;")
>>> escape(record.get_name(True))
Markup("<strong>&lt;R&amp;D&gt;</strong>") # HTML is kept
```
When generating HTML code, it is important to separate the structure (tags) from
the content (text).
```pycon
>>> Markup("<p>") + "Hello <R&D>" + Markup("</p>")
Markup('<p>Hello &lt;R&amp;D&gt;</p>')
>>> Markup("%s <br/> %s") % ("<R&D>", Markup("<p>Hello</p>"))
Markup('&lt;R&amp;D&gt; <br/> <p>Hello</p>')
>>> escape("<R&D>")
Markup('&lt;R&amp;D&gt;')
>>> _("List of Tasks on project %s: %s",
... project.name,
... Markup("<ul>%s</ul>") % Markup().join(Markup("<li>%s</li>") % t.name for t in project.task_ids)
... )
Markup('Liste de tâches pour le projet &lt;R&amp;D&gt;: <ul><li>First &lt;R&amp;D&gt; task</li></ul>')
>>> Markup("<p>Foo %</p>" % bar) # bad, bar is not escaped
>>> Markup("<p>Foo %</p>") % bar # good, bar is escaped if text and kept if markup
>>> link = Markup("<a>%s</a>") % self.name
>>> message = "Click %s" % link # bad, message is text and Markup did nothing
>>> message = escape("Click %s") % link # good, format two markup objects together
>>> Markup(f"<p>Foo {self.bar}</p>") # bad, bar is inserted before escaping
>>> Markup("<p>Foo {bar}</p>").format(bar=self.bar) # good, sorry no fstring
```
When working with translations, it is especially important to separate the HTML
from the text. The translation methods accepts a {class}`~markupsafe.Markup`
parameters and will escape the translation if it gets receives at least one.
```pycon
>>> Markup("<p>%s</p>") % _("Hello <R&D>")
Markup('<p>Bonjour &lt;R&amp;D&gt;</p>')
>>> _("Order %s has been confirmed", Markup("<a>%s</a>") % order.name)
Markup('Order <a>SO42</a> has been confirmed')
>>> _("Message received from %(name)s <%(email)s>",
... name=self.name,
... email=Markup("<a href='mailto:%s'>%s</a>") % (self.email, self.email)
Markup('Message received from Georges &lt;<a href=mailto:george@abitbol.example>george@abitbol.example</a>&gt;')
```
### 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*.
```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
Markup('&lt;R&amp;D&gt;')
# Now you can mix it with other code...
>>> self.website_description = Markup("<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.
```python
# Sanitizing without escaping is BROKEN: data is corrupted!
>>> html_sanitize(data)
Markup('')
# Sanitizing *after* escaping is OK!
>>> html_sanitize(code)
Markup('<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.
```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)
Markup('<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.
```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.
```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:
```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.
[time module]: https://docs.python.org/3/library/time.html
@@ -1,583 +0,0 @@
.. _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 mechanisms to users.
.. class:: res.groups
.. attribute:: name
serves as user-readable identification for the group (spells out the
role / purpose of the group)
.. attribute:: category_id
The *module category*, serves to associate groups with an Odoo App
(~a set of related business models) and convert them into an exclusive
selection in the user form.
.. todo:: clarify & document special cases & relationship between
groups & categories better
.. attribute:: implied_ids
Other groups to set on the user alongside this one. This is a
convenience pseudo-inheritance relationship: it's possible to
explicitly remove implied groups from a user without removing the
implier.
.. attribute:: comment
Additional notes on the group e.g.
.. _reference/security/acl:
Access Rights
=============
*Grants* access to an entire model for a given set of operations. If no access
rights matches an operation on a model for a user (through their group), the
user doesn't have access.
Access rights are additive, a user's accesses are the union of the accesses
they get through all their groups e.g. given a user who is part of group A
granting read and create access and a group B granting update access, the user
will have all three of create, read, and update.
.. class:: ir.model.access
.. attribute:: name
The purpose or role of the group.
.. attribute:: model_id
The model whose access the ACL controls.
.. attribute:: group_id
The :class:`res.groups` to which the accesses are granted, an empty
:attr:`group_id` means the ACL is granted to *every user*
(non-employees e.g. portal or public users).
The :samp:`perm_{method}` attributes grant the corresponding CRUD access
when set, they are all unset by default.
.. attribute:: perm_create
.. attribute:: perm_read
.. attribute:: perm_write
.. attribute:: perm_unlink
.. _reference/security/rules:
Record Rules
============
Record rules are *conditions* which must be satisfied in order for an operation
to be allowed. Record rules are evaluated record-by-record, following access
rights.
Record rules are default-allow: if access rights grant access and no rule
applies to the operation and model for the user, the access is granted.
.. class:: ir.rule
.. attribute:: name
The description of the rule.
.. attribute:: model_id
The model to which the rule applies.
.. attribute:: groups
The :class:`res.groups` to which access is granted (or not). Multiple
groups can be specified. If no group is specified, the rule is *global*
which is treated differently than "group" rules (see below).
.. attribute:: global
Computed on the basis of :attr:`groups`, provides easy access to the
global status (or not) of the rule.
.. attribute:: domain_force
A predicate specified as a :ref:`domain <reference/orm/domains>`, the
rule allows the selected operations if the domain matches the record,
and forbids it otherwise.
The domain is a *python expression* which can use the following
variables:
``time``
Python's :mod:`python:time` module.
``user``
The current user, as a singleton recordset.
``company_id``
The current user's currently selected company as a single company id
(not a recordset).
``company_ids``
All the companies to which the current user has access as a list of
company ids (not a recordset), see
:ref:`howto/company/security` for more details.
The :samp:`perm_{method}` have completely different semantics than for
:class:`ir.model.access`: for rules, they specify which operation the rules
applies *for*. If an operation is not selected, then the rule is not checked
for it, as if the rule did not exist.
All operations are selected by default.
.. attribute:: perm_create
.. attribute:: perm_read
.. attribute:: perm_write
.. attribute:: perm_unlink
.. _reference/security/rules/global:
Global rules versus group rules
-------------------------------
There is a large difference between global and group rules in how they compose
and combine:
* Global rules *intersect*, if two global rules apply then *both* must be
satisfied for the access to be granted, this means adding global rules always
restricts access further.
* Group rules *unify*, if two group rules apply then *either* can be
satisfied for the access to be granted. This means adding group rules can
expand access, but not beyond the bounds defined by global rules.
* The global and group rulesets *intersect*, which means the first group rule
being added to a given global ruleset will restrict access.
.. danger::
Creating multiple global rules is risky as it's possible to create
non-overlapping rulesets, which will remove all access.
.. _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
<api/external_api/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.env.user.has_group('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;
}
Creating safe content using :class:`~markupsafe.Markup`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See the `official documentation <https://markupsafe.palletsprojects.com/>`_ for
explanations, but the big advantage of
:class:`~markupsafe.Markup` is that it's a very rich type overrinding
:class:`str` operations to *automatically escape parameters*.
This means that it's easy to create *safe* html snippets by using
:class:`~markupsafe.Markup` on a string literal and "formatting in"
user-provided (and thus potentially unsafe) content:
.. code-block:: pycon
>>> Markup('<em>Hello</em> ') + '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
>>> Markup('<em>Hello</em> %s') % '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
though it is a very good thing, note that the effects can be odd at times:
.. code-block:: pycon
>>> Markup('<a>').replace('>', 'x')
Markup('<a>')
>>> Markup('<a>').replace(Markup('>'), 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', '&')
Markup('<a&amp;')
.. tip:: Most of the content-safe APIs actually return a
:class:`~markupsafe.Markup` with all that implies.
The :class:`~markupsafe.escape` method (and its
alias :class:`~odoo.tools.misc.html_escape`) turns a `str` into
a :class:`~markupsafe.Markup` and escapes its content. It will not escape the
content of a :class:`~markupsafe.Markup` object.
.. code-block:: python
def get_name(self, to_html=False):
if to_html:
return Markup("<strong>%s</strong>") % self.name # escape the name
else:
return self.name
>>> record.name = "<R&D>"
>>> escape(record.get_name())
Markup("&lt;R&amp;D&gt;")
>>> escape(record.get_name(True))
Markup("<strong>&lt;R&amp;D&gt;</strong>") # HTML is kept
When generating HTML code, it is important to separate the structure (tags) from
the content (text).
.. code-block:: pycon
>>> Markup("<p>") + "Hello <R&D>" + Markup("</p>")
Markup('<p>Hello &lt;R&amp;D&gt;</p>')
>>> Markup("%s <br/> %s") % ("<R&D>", Markup("<p>Hello</p>"))
Markup('&lt;R&amp;D&gt; <br/> <p>Hello</p>')
>>> escape("<R&D>")
Markup('&lt;R&amp;D&gt;')
>>> _("List of Tasks on project %s: %s",
... project.name,
... Markup("<ul>%s</ul>") % Markup().join(Markup("<li>%s</li>") % t.name for t in project.task_ids)
... )
Markup('Liste de tâches pour le projet &lt;R&amp;D&gt;: <ul><li>First &lt;R&amp;D&gt; task</li></ul>')
>>> Markup("<p>Foo %</p>" % bar) # bad, bar is not escaped
>>> Markup("<p>Foo %</p>") % bar # good, bar is escaped if text and kept if markup
>>> link = Markup("<a>%s</a>") % self.name
>>> message = "Click %s" % link # bad, message is text and Markup did nothing
>>> message = escape("Click %s") % link # good, format two markup objects together
>>> Markup(f"<p>Foo {self.bar}</p>") # bad, bar is inserted before escaping
>>> Markup("<p>Foo {bar}</p>").format(bar=self.bar) # good, sorry no fstring
When working with translations, it is especially important to separate the HTML
from the text. The translation methods accepts a :class:`~markupsafe.Markup`
parameters and will escape the translation if it gets receives at least one.
.. code-block:: pycon
>>> Markup("<p>%s</p>") % _("Hello <R&D>")
Markup('<p>Bonjour &lt;R&amp;D&gt;</p>')
>>> _("Order %s has been confirmed", Markup("<a>%s</a>") % order.name)
Markup('Order <a>SO42</a> has been confirmed')
>>> _("Message received from %(name)s <%(email)s>",
... name=self.name,
... email=Markup("<a href='mailto:%s'>%s</a>") % (self.email, self.email)
Markup('Message received from Georges &lt;<a href=mailto:george@abitbol.example>george@abitbol.example</a>&gt;')
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
Markup('&lt;R&amp;D&gt;')
# Now you can mix it with other code...
>>> self.website_description = Markup("<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)
Markup('')
# Sanitizing *after* escaping is OK!
>>> html_sanitize(code)
Markup('<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)
Markup('<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.
File diff suppressed because it is too large Load Diff
@@ -1,992 +0,0 @@
.. _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:
Testing Python code
===================
Odoo provides support for testing modules using `Python's unittest library
<https://docs.python.org/3/library/unittest.html>`_.
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.TransactionCase
:members: browse_ref, ref
.. autoclass:: odoo.tests.SingleTransactionCase
:members: browse_ref, ref
.. autoclass:: odoo.tests.HttpCase
:members: browse_ref, ref, url_open, browser_js
.. autofunction:: odoo.tests.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::
# coding: utf-8
from odoo.tests import HttpCase, tagged
# This test should only be executed after all modules have been installed.
@tagged('-at_install', 'post_install')
class WebsiteVisitorTests(HttpCase):
def test_create_visitor_on_tracked_page(self):
Page = self.env['website.page']
The most common situation is to use
:class:`~odoo.tests.TransactionCase` and test a property of a model
in each method::
class TestModelA(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.Form
:members:
.. autoclass:: odoo.tests.M2MProxy
:members: add, remove, clear
.. autoclass:: odoo.tests.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
.. _developer/reference/testing/selection:
Test selection
--------------
In Odoo, Python tests can be tagged to facilitate the test selection when
running tests.
Subclasses of :class:`odoo.tests.BaseCase` (usually through
:class:`~odoo.tests.TransactionCase` or
:class:`~odoo.tests.HttpCase`) are automatically tagged with
``standard`` and ``at_install`` by default.
Invocation
~~~~~~~~~~
:option:`--test-tags <odoo-bin --test-tags>` can be used to select/filter tests
to run on the command-line. It implies :option:`--test-enable <odoo-bin --test-enable>`,
so it's not necessary to specify :option:`--test-enable <odoo-bin --test-enable>`
when using :option:`--test-tags <odoo-bin --test-tags>`.
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.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.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 explicitly:
.. code-block:: console
$ odoo-bin --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-tags nice,standard
The config switch parameter also accepts the ``+`` and ``-`` prefixes. The
``+`` prefix is implied and therefore, totally 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-tags 'standard,-slow'
When you write a test that does not inherit from the
:class:`~odoo.tests.BaseCase`, this test will not have the default tags,
you have to add them explicitly 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):
...
Besides tags you can also specify specific modules, classes or functions to
test. The full syntax of the format accepted by :option:`--test-tags <odoo-bin --test-tags>`
is:
.. code-block:: text
[-][tag][/module][:class][.method]
So if you want to test the `stock_account` module, you can use:
.. code-block:: console
$ odoo-bin --test-tags /stock_account
If you want to test a specific function with a unique name, it can be specified
directly:
.. code-block:: console
$ odoo-bin --test-tags .test_supplier_invoice_forwarded_by_internal_user_without_supplier
This is equivalent to
.. code-block:: console
$ odoo-bin --test-tags /account:TestAccountIncomingSupplierInvoice.test_supplier_invoice_forwarded_by_internal_user_without_supplier
if the name of the test is unambiguous. Multiple modules, classes and functions
can be specified at once separated by a `,` like with regular tags.
.. _reference/testing/tags:
Special tags
~~~~~~~~~~~~
- ``standard``: All Odoo tests that inherit from
:class:`~odoo.tests.BaseCase` are implicitly 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.
Examples
~~~~~~~~
.. important::
Tests will be executed only in installed modules. If you're starting from
a clean database, you'll need to install the modules with the
:option:`-i <odoo-bin -i>` switch at least once. After that it's no longer
needed, unless you need to upgrade the module, in which case
:option:`-u <odoo-bin -u>` can be used. 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-tags /sale
Run the tests from the sale module but not the ones tagged as slow:
.. code-block:: console
$ odoo-bin --test-tags '/sale,-slow'
Run only the tests from stock or tagged as slow:
.. code-block:: console
$ odoo-bin --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.
.. _reference/testing/qunit:
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:: testing/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.qunit_suite`_. This bundle contains
the main code (assets common + assets backend), some libraries, the QUnit test
runner and the test bundles listed below.
- a bundle named `web.tests_assets`_ includes most of the assets and utils required
by the test suite: custom QUnit asserts, test helpers, lazy loaded assets, etc.
- another asset bundle, `web.qunit_suite_tests`_, contains all the test scripts.
This is typically where the test files are added to the suite.
- 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 browser_js method spawns a Chrome headless instance.
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 system 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_tests" name="my addon tests" inherit_id="web.qunit_suite_tests">
<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.
.. _reference/testing/integration-testing:
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 PhantomJs browser, point it to the proper
url and simulate the click and inputs, according to the scenario.
Writing a test tour
-------------------
Structure
~~~~~~~~~
To write a test tour for `your_module`, start with creating the required files:
.. code-block:: text
your_module
├── ...
├── static
| └── tests
| └── tours
| └── your_tour.js
├── tests
| ├── __init__.py
| └── test_calling_the_tour.py
└── __manifest__.py
You can then:
- update :file:`__manifest__.py` to add :file:`your_tour.js` in the assets.
.. code-block:: python
'assets': {
'web.assets_tests': [
'your_module/static/tests/tours/your_tour.js',
],
},
- update :file:`__init__.py` in the folder :file:`tests` to import :file:`test_calling_the_tour`.
.. seealso::
- :ref:`Assets Bundle <reference/assets_bundle>`
- :ref:`testing/python`
.. _testing/javascript/test:
Javascript
~~~~~~~~~~
#. Setup your tour by registering it.
.. code-block:: javascript
import tour from 'web_tour.tour';
tour.register('rental_product_configurator_tour', {
url: '/web', // Here, you can specify any other starting url
}, [
// Your sequence of steps
]);
#. Add any step you want.
Every step contains at least a trigger. You can either use the `predefined steps
<{GITHUB_PATH}/addons/web_tour/static/src/tour_service/tour_utils.js#L426>`_ or write your own personalized
step.
Here are some example of steps:
.. example::
.. code-block:: javascript
// First step
tour.stepUtils.showAppsMenuItem(),
// Second step
{
trigger: '.o_app[data-menu-xmlid="your_module.maybe_your_module_menu_root"]',
isActive: ['community'], // Optional
run: "click",
}, {
// Third step
},
.. example::
.. code-block:: javascript
{
trigger: '.js_product:has(strong:contains(Chair floor protection)) .js_add',
run: "click",
},
.. example::
.. code-block:: javascript
{
isActive: ["mobile", "enterprise"],
content: "Click on Add a product link",
trigger: 'a:contains("Add a product")',
tooltipPosition: "bottom",
async run(helpers) { //Exactly the same as run: "click"
helpers.click();
}
},
Here are some possible arguments for your personalized steps:
- **trigger**: Required, Selector/element to ``run`` an action on. The tour will
wait until the element exists and is visible before ``run``-ing the
action *on it*.
- **run**: Optional, Action to perform on the *trigger* element. If no ``run``,
no action.
The action can be:
- A function, asynchronous, executed with the trigger's ``Tip`` as
context (``this``) and the action helpers as parameter.
- The name of one of the action helpers, which will be run on the
trigger element:
.. rst-class:: o-definition-list
``check``
Ensures that the **trigger** element is checked. This helper is intended
for `<input[type=checkbox]>` elements only.
``clear``
Clears the value of the **trigger** element. This helper is
intended for `<input>` or `<textarea>` elements only.
``click``
Clicks the **trigger** element, performing all the relevant intermediate
events.
``dblclick``,
Same as ``click`` with two repetitions.
:samp:`drag_and_drop {target}`
Simulates the dragging of the **trigger** element over to the ``target``.
:samp:`edit {content}`
``clear`` the element and then ``fill`` the ``content``.
:samp:`editor {content}`
Focus the **trigger** element (wysiwyg) and then ``press`` the ``content``.
:samp:`fill {content}`
Focus the **trigger** element and then ``press`` the ``content``. This helper is
intended for `<input>` or `<textarea>` elements only.
``hover``
Performs a hover sequence on the **trigger** element.
:samp:`press {content}`
Performs a keyboard event sequence.
:samp:`range {content}`
Focus the **trigger** element and set ``content`` as value. This helper is intended
for `<input[type=range]>` elements only.
:samp:`select {value}`
Performs a selection event sequence on **trigger** element. Select the option by its
``value``. This helper is intended for `<select>` elements only.
:samp:`selectByIndex {index}`
Same as ``select`` but select the option by its ``index``. Note that first option has
index 0.
:samp:`selectByLabel {label}`
Same as ``select`` but select the option by its ``label``.
``uncheck``
Ensures that the **trigger** element is unchecked. This helper is intended
for `<input[type=checkbox]>` elements only.
- **isActive**: Optional,
Activates the step only if all conditions of isActive array are met.
- Browser is in either **desktop** or **mobile** mode.
- The tour concerns either **community** or **enterprise** edition.
- The tour is run in either **auto** (runbot) or **manual** (onboarding) mode.
- **tooltipPosition**: Optional, ``"top"``, ``"right"``, ``"bottom"``, or
``"left"``. Where to position the tooltip relative to the **target**
when running interactive tours.
- **content**: Optional but recommended, the content of the tooltip in
interactive tours, also logged to the console so very useful to
trace and debug automated tours.
- **timeout**: How long to wait until the step can ``run``, in
milliseconds, 10000 (10 seconds).
.. important::
The last step(s) of a tour should always return the client to a
"stable" state (e.g. no ongoing editions) and ensure all
side-effects (network requests) have finished running to avoid race
conditions or errors during teardown.
.. seealso::
- `jQuery documentation about find <https://api.jquery.com/find/>`_
Python
~~~~~~
To start a tour from a python test, make the class inherit from
:class:`~odoo.tests.HTTPCase`, and call `start_tour`:
.. code-block:: python
def test_your_test(self):
# Optional Setup
self.start_tour("/web", "your_tour_name", login="admin")
# Optional verifications
Writing an onboarding tour
--------------------------
Structure
~~~~~~~~~
To write an onboarding tour for `your_module`, start with creating the required files:
.. code-block:: text
your_module
├── ...
├── data
| └── your_tour.xml
├── static/src/js/tours/your_tour.js
└── __manifest__.py
You can then update :file:`__manifest__.py` to add :file:`your_tour.js` in the assets and :file:`your_tour.xml` in the data.
.. code-block:: python
'data': [
'data/your_tour.xml',
],
'assets': {
'web.assets_backend': [
'your_module/static/src/js/tours/your_tour.js',
],
},
Javascript
~~~~~~~~~~
The javascript part is the same as for :ref: `the test tour <testing/javascript/test>`.
XML
~~~
When you have your tour in the javascript registry, you can create a record `web_tour.tour` in the xml, like that:
.. code-block:: xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="your_tour" model="web_tour.tour">
<field name="name">your_tour</field>
<field name="sequence">10</field>
<field name="rainbow_man_message">Congrats, that was a great tour</field>
</record>
</odoo>
- `name`: Required, the name must be the same as the one in the
javascript registry.
- `sequence`: Optional; determines the order to execute the
onboarding tours. Defaults to 1000.
- `url`: Optional; the url where to start the tour. If ``url`` is ``False``,
take the url from the registry. Defaults to "/odoo".
- `rainbow_man_message`: Optional; will show the message in the
rainbow man effect at the completion of the tour. If ``rainbow_man_message`` is ``False``,
there is no rainbow effect. Defaults to ``<b>Good job!</b> You went through all steps of this tour.``
Running onboarding tours
~~~~~~~~~~~~~~~~~~~~~~~~
They can all be started in their sequence order by toggling the :guilabel:`Onboarding` option in the user menu.
You can run specific onboarding tours by going to the :menuselection:`Settings --> Technical --> User Interface --> Tours`
and clicking on :guilabel:`Onboarding` or :guilabel:`Testing`.
- **Onboarding**: will execute the tour in interactive mode. That means the tour will show what to do and
wait for interactions from the user.
- **Testing**: will execute the tour automatically. That means the tour will be executing all the step in
front of the user.
Tour recorder
~~~~~~~~~~~~~
You can also create tours easily with the tour recorder. To do so, click on :guilabel:`Record` on the
onboarding tours view. When started, this tool will record all your interactions in Odoo.
The created tours are flagged in the onboarding tours view as **Custom**. These tours can also
be exported to a javascript file, ready to be put in your module.
Debugging tips
--------------
Observing test tours in a browser
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are three ways with different tradeoffs:
``watch=True``
**************
When running a tour locally via the test suite, the ``watch=True``
parameter can be added to the ``browser_js`` or ``start_tour``
call::
self.start_tour("/web", "your_tour_name", watch=True)
This will automatically open a Chrome window with the tour being
run inside it.
**Advantages**
- always works if the tour has Python setup / surrounding code, or multiple steps
- runs entirely automatically (just select the test which launches the tour)
- transactional (*should* always be runnable multiple times)
**Drawbacks**
- only works locally
- only works if the test / tour can run correctly locally
``debug=True``
**************
When running a tour locally via the test suite, the ``debug=True``
parameter can be added to the ``browser_js`` or ``start_tour``
call::
self.start_tour("/web", "your_tour_name", debug=True)
This will automatically open a fullscreen Chrome window with opened
devtools and a debugger breakpoint set at the start of the tour. The tour
is ran with the debug=assets query parameter. When an error is thrown, the
debugger stops on the exception.
**Advantages**
- Same advantages as mode `watch=True`
- Easier to debug steps
**Drawbacks**
- only works locally
- only works if the test / tour can run correctly locally
Run via browser
***************
Test tours can also be launched via the browser UI by calling
.. code-block:: javascript
odoo.startTour("tour_name");
in the javascript console, or by enabling :ref:`tests mode
<frontend/framework/tests_debug_mode>` by setting ``?debug=tests`` in
the URL.
**Advantages**
- easier to run
- can be used on production or test sites, not just local instances
- allows running in "Onboarding" mode (manual steps)
**Drawbacks**
- harder to use with test tours involving Python setup
- may not work multiple times depending on tour side-effects
.. tip::
It's possible to use this method to observe or interact with tours
which require Python setup:
- add a *python* breakpoint before the relevant tour is started
(``start_tour`` or ``browser_js`` call)
- when the breakpoint is hit, open the instance in your browser
- run the tour
At this point the Python setup will be visible to the browser, and
the tour will be able to run.
You may want to comment the ``start_tour`` or ``browser_js`` call
if you also want the test to continue afterwards, depending on the
tour's side-effects.
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>`
Introspecting / debugging steps
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When trying to fix / debug a tour, the screenshots (on failure) are
not necessarily sufficient. In that case it can be useful to see
what's happening at some or each step.
While this is pretty easy when in an "onboarding" (as they're mostly
driven explicitly by the user) it's more complicated when running
"test" tours, or when running tours through the test suite. In that
case there are two main tricks:
- A step property ``break: true,`` in debug mode (debug=True).
This adds a debugger breakpoint at the start of the step.
You can then add your own wherever you need.
**Advantages**
- very simple
- the tour continues as soon as you resume execution
**Drawbacks**
- page interaction is limited as all javascript is blocked
- A step property ``pause: true,`` in debug mode (debug=True).
The tour will stop at the end of the step. This allows inspecting
and interacting with the page until the developer is ready to
resume by typing **play();** in the browser console.
**Advantages**
- allows interacting with the page
- no useless (for this situation) debugger UI
- A step with a ``run() { debugger; }`` action.
This can be added to an existing step, or can be a new dedicated
step. Once the step's **trigger** is matched, the execution will
stop all javascript execution.
**Advantages**
- simple
- the tour continues as soon as you resume execution
**Drawbacks**
- page interaction is limited as all javascript is blocked
- the debugger is triggered after trying to find targeted
element defined in the step.
Performance Testing
===================
Query counts
------------
One of the ways to test performance is to measure database queries. Manually, this can be tested with the
`--log-sql` CLI parameter. If you want to establish the maximum number of queries for an operation,
you can use the :meth:`~odoo.tests.BaseCase.assertQueryCount` method, integrated in Odoo test classes.
.. code-block:: python
with self.assertQueryCount(11):
do_something()
.. _qunit: https://qunitjs.com/
.. _qunit_config.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_config.js#L49
.. _web.tests_assets: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L594
.. _web.qunit_suite: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L660
.. _web.qunit_suite_tests: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L680
.. _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
File diff suppressed because it is too large Load Diff
@@ -1,53 +1,53 @@
============
External API
============
# External API
Odoo is usually extended internally via modules, but many of its features and
all of its data are also available from the outside for external analysis or
integration with various tools. Part of the :ref:`reference/orm/model` API is
easily available over XML-RPC_ and accessible from a variety of languages.
integration with various tools. Part of the {ref}`reference/orm/model` API is
easily available over [XML-RPC] and accessible from a variety of languages.
.. important::
Starting with PHP8, the XML-RPC extension may not be available by default.
Check out the `manual <https://www.php.net/manual/en/xmlrpc.installation.php>`_
for the installation steps.
:::{important}
Starting with PHP8, the XML-RPC extension may not be available by default.
Check out the [manual](https://www.php.net/manual/en/xmlrpc.installation.php)
for the installation steps.
:::
.. note::
Access to data via the external API is only available on *Custom* Odoo pricing plans. Access to
the external API is not available on *One App Free* or *Standard* plans. For more information
visit the `Odoo pricing page <https://www.odoo.com/pricing-plan>`_ or reach out to your Customer
Success Manager.
:::{note}
Access to data via the external API is only available on *Custom* Odoo pricing plans. Access to
the external API is not available on *One App Free* or *Standard* plans. For more information
visit the [Odoo pricing page](https://www.odoo.com/pricing-plan) or reach out to your Customer
Success Manager.
:::
.. seealso::
- :doc:`Tutorial on web services <../howtos/web_services>`
:::{seealso}
- {doc}`Tutorial on web services <../howtos/web_services>`
:::
Connection
==========
## Connection
Configuration
-------------
### Configuration
If you already have an Odoo server installed, you can just use its parameters.
.. important::
:::{important}
For Odoo Online instances (\<domain>.odoo.com), users are created without a
*local* password (as a person you are logged in via the Odoo Online
authentication system, not by the instance itself). To use XML-RPC on Odoo
Online instances, you will need to set a password on the user account you
want to use:
For Odoo Online instances (<domain>.odoo.com), users are created without a
*local* password (as a person you are logged in via the Odoo Online
authentication system, not by the instance itself). To use XML-RPC on Odoo
Online instances, you will need to set a password on the user account you
want to use:
- Log in your instance with an administrator account.
- Go to {menuselection}`Settings --> Users & Companies --> Users`.
- Click on the user you want to use for XML-RPC access.
- Click on {guilabel}`Action` and select {guilabel}`Change Password`.
- Set a {guilabel}`New Password` value then click {guilabel}`Change Password`.
* Log in your instance with an administrator account.
* Go to :menuselection:`Settings --> Users & Companies --> Users`.
* Click on the user you want to use for XML-RPC access.
* Click on :guilabel:`Action` and select :guilabel:`Change Password`.
* Set a :guilabel:`New Password` value then click :guilabel:`Change Password`.
The *server url* is the instance's domain (e.g.
*https://mycompany.odoo.com*), the *database name* is the name of the
instance (e.g. *mycompany*). The *username* is the configured user's login
as shown by the *Change Password* screen.
The *server url* is the instance's domain (e.g.
*https://mycompany.odoo.com*), the *database name* is the name of the
instance (e.g. *mycompany*). The *username* is the configured user's login
as shown by the *Change Password* screen.
:::
```{eval-rst}
.. tabs::
.. code-tab:: python
@@ -86,13 +86,14 @@ If you already have an Odoo server installed, you can just use its parameters.
username = "admin"
password = <insert password for your admin user (default: admin)>
)
```
.. _api/external_api/keys:
(api-external-api-keys)=
API Keys
~~~~~~~~
#### API Keys
.. versionadded:: 14.0
:::{versionadded} 14.0
:::
Odoo has support for **api keys** and (depending on modules or settings) may
**require** these keys to perform webservice operations.
@@ -103,42 +104,45 @@ as the password as they essentially provide the same access to your user
account (although they can not be used to log-in via the interface).
In order to add a key to your account, simply go to your
:guilabel:`Preferences` (or :guilabel:`My Profile`):
{guilabel}`Preferences` (or {guilabel}`My Profile`):
.. image:: external_api/preferences.png
:align: center
```{image} external_api/preferences.png
:align: center
```
then open the :guilabel:`Account Security` tab, and click
:guilabel:`New API Key`:
then open the {guilabel}`Account Security` tab, and click
{guilabel}`New API Key`:
.. image:: external_api/account-security.png
:align: center
```{image} external_api/account-security.png
:align: center
```
Input a description for the key, **this description should be as clear and
complete as possible**: it is the only way you will have to identify your keys
later and know whether you should remove them or keep them around.
Click :guilabel:`Generate Key`, then copy the key provided. **Store this key
Click {guilabel}`Generate Key`, then copy the key provided. **Store this key
carefully**: it is equivalent to your password, and just like your password
the system will not be able to retrieve or show the key again later on. If you lose
this key, you will have to create a new one (and probably delete the one you
lost).
Once you have keys configured on your account, they will appear above the
:guilabel:`New API Key` button, and you will be able to delete them:
{guilabel}`New API Key` button, and you will be able to delete them:
.. image:: external_api/delete-key.png
:align: center
```{image} external_api/delete-key.png
:align: center
```
**A deleted API key can not be undeleted or re-set**. You will have to generate
a new key and update all the places where you used the old one.
Test database
~~~~~~~~~~~~~
#### Test database
To make exploration simpler, you can also ask https://demo.odoo.com for a test
To make exploration simpler, you can also ask <https://demo.odoo.com> for a test
database:
```{eval-rst}
.. tabs::
.. code-tab:: python
@@ -215,21 +219,22 @@ database:
The examples do not include imports as these imports couldn't be
pasted in the code.
```
Logging in
----------
### Logging in
Odoo requires users of the API to be authenticated before they can query most
data.
The ``xmlrpc/2/common`` endpoint provides meta-calls which don't require
The `xmlrpc/2/common` endpoint provides meta-calls which don't require
authentication, such as the authentication itself or fetching version
information. To verify if the connection information is correct before trying
to authenticate, the simplest call is to ask for the server's version. The
authentication itself is done through the ``authenticate`` function and
returns a user identifier (``uid``) used in authenticated calls instead of
authentication itself is done through the `authenticate` function and
returns a user identifier (`uid`) used in authenticated calls instead of
the login.
```{eval-rst}
.. tabs::
.. code-tab:: python
@@ -263,19 +268,20 @@ the login.
if err := client.Call("version", nil, &common); err != nil {
log.Fatal(err)
}
```
Result:
.. code-block:: json
{
"server_version": "13.0",
"server_version_info": [13, 0, 0, "final", 0],
"server_serie": "13.0",
"protocol_version": 1,
}
```json
{
"server_version": "13.0",
"server_version_info": [13, 0, 0, "final", 0],
"server_serie": "13.0",
"protocol_version": 1,
}
```
```{eval-rst}
.. tabs::
.. code-tab:: python
@@ -303,25 +309,26 @@ Result:
}, &uid); err != nil {
log.Fatal(err)
}
```
.. _api/external_api/calling_methods:
(api-external-api-calling-methods)=
Calling methods
===============
## Calling methods
The second endpoint is ``xmlrpc/2/object``. It is used to call methods of odoo
models via the ``execute_kw`` RPC function.
The second endpoint is `xmlrpc/2/object`. It is used to call methods of odoo
models via the `execute_kw` RPC function.
Each call to ``execute_kw`` takes the following parameters:
Each call to `execute_kw` takes the following parameters:
* the database to use, a string
* the user id (retrieved through ``authenticate``), an integer
* the user's password, a string
* the model name, a string
* the method name, a string
* an array/list of parameters passed by position
* a mapping/dict of parameters to pass by keyword (optional)
- the database to use, a string
- the user id (retrieved through `authenticate`), an integer
- the user's password, a string
- the model name, a string
- the method name, a string
- an array/list of parameters passed by position
- a mapping/dict of parameters to pass by keyword (optional)
```{eval-rst}
.. example::
For instance, to search for records in the ``res.partner`` model, we can call
@@ -380,16 +387,17 @@ Each call to ``execute_kw`` takes the following parameters:
.. code-block:: json
true
```
List records
------------
### List records
Records can be listed and filtered via :meth:`~odoo.models.Model.search`.
Records can be listed and filtered via {meth}`~odoo.models.Model.search`.
:meth:`~odoo.models.Model.search` takes a mandatory
:ref:`domain <reference/orm/domains>` filter (possibly empty), and returns the
{meth}`~odoo.models.Model.search` takes a mandatory
{ref}`domain <reference/orm/domains>` filter (possibly empty), and returns the
database identifiers of all records matching the filter.
```{eval-rst}
.. example::
To list customer companies, for instance:
@@ -435,14 +443,15 @@ database identifiers of all records matching the filter.
.. code-block:: json
[7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74]
```
Pagination
~~~~~~~~~~
#### Pagination
By default a search will return the ids of all records matching the
condition, which may be a huge number. ``offset`` and ``limit`` parameters are
condition, which may be a huge number. `offset` and `limit` parameters are
available to only retrieve a subset of all matched records.
```{eval-rst}
.. example::
.. tabs::
@@ -488,16 +497,17 @@ available to only retrieve a subset of all matched records.
.. code-block:: json
[13, 20, 30, 22, 29]
```
Count records
-------------
### Count records
Rather than retrieve a possibly gigantic list of records and count them,
:meth:`~odoo.models.Model.search_count` can be used to retrieve
{meth}`~odoo.models.Model.search_count` can be used to retrieve
only the number of records matching the query. It takes the same
:ref:`domain <reference/orm/domains>` filter as
:meth:`~odoo.models.Model.search` and no other parameter.
{ref}`domain <reference/orm/domains>` filter as
{meth}`~odoo.models.Model.search` and no other parameter.
```{eval-rst}
.. example::
.. tabs::
@@ -541,21 +551,23 @@ only the number of records matching the query. It takes the same
.. code-block:: json
19
```
.. note::
Calling ``search`` then ``search_count`` (or the other way around) may not
yield coherent results if other users are using the server: stored data
could have changed between the calls.
:::{note}
Calling `search` then `search_count` (or the other way around) may not
yield coherent results if other users are using the server: stored data
could have changed between the calls.
:::
Read records
------------
### Read records
Record data are accessible via the :meth:`~odoo.models.Model.read` method,
Record data are accessible via the {meth}`~odoo.models.Model.read` method,
which takes a list of ids (as returned by
:meth:`~odoo.models.Model.search`), and optionally a list of fields to
{meth}`~odoo.models.Model.search`), and optionally a list of fields to
fetch. By default, it fetches all the fields the current user can read,
which tends to be a huge amount.
```{eval-rst}
.. example::
.. tabs::
@@ -676,22 +688,24 @@ which tends to be a huge amount.
.. code-block:: json
[{"comment": false, "country_id": [21, "Belgium"], "id": 7, "name": "Agrolait"}]
```
.. note::
Even if the ``id`` field is not requested, it is always returned.
:::{note}
Even if the `id` field is not requested, it is always returned.
:::
List record fields
------------------
### List record fields
:meth:`~odoo.models.Model.fields_get` can be used to inspect
{meth}`~odoo.models.Model.fields_get` can be used to inspect
a model's fields and check which ones seem to be of interest.
Because it returns a large amount of meta-information (it is also used by client
programs) it should be filtered before printing, the most interesting items
for a human user are ``string`` (the field's label), ``help`` (a help text if
available) and ``type`` (to know which values to expect, or to send when
for a human user are `string` (the field's label), `help` (a help text if
available) and `type` (to know which values to expect, or to send when
updating a record).
```{eval-rst}
.. example::
.. tabs::
@@ -773,20 +787,21 @@ updating a record).
"help": "",
"string": "# of Purchase Order"
},
```
Search and read
---------------
### Search and read
Because it is a very common task, Odoo provides a
:meth:`~odoo.models.Model.search_read` shortcut which, as its name suggests, is
equivalent to a :meth:`~odoo.models.Model.search` followed by a
:meth:`~odoo.models.Model.read`, but avoids having to perform two requests
{meth}`~odoo.models.Model.search_read` shortcut which, as its name suggests, is
equivalent to a {meth}`~odoo.models.Model.search` followed by a
{meth}`~odoo.models.Model.read`, but avoids having to perform two requests
and keep ids around.
Its arguments are similar to :meth:`~odoo.models.Model.search`'s, but it
can also take a list of ``fields`` (like :meth:`~odoo.models.Model.read`,
Its arguments are similar to {meth}`~odoo.models.Model.search`'s, but it
can also take a list of `fields` (like {meth}`~odoo.models.Model.read`,
if that list is not provided it will fetch all fields of matched records).
```{eval-rst}
.. example::
.. tabs::
@@ -869,17 +884,18 @@ if that list is not provided it will fetch all fields of matched records).
"name": "Camptocamp"
}
]
```
Create records
--------------
### Create records
Records of a model are created using :meth:`~odoo.models.Model.create`. The
Records of a model are created using {meth}`~odoo.models.Model.create`. The
method creates a single record and returns its database identifier.
:meth:`~odoo.models.Model.create` takes a mapping of fields to values, used
{meth}`~odoo.models.Model.create` takes a mapping of fields to values, used
to initialize the record. For any field which has a default value and is not
set through the mapping argument, the default value will be used.
```{eval-rst}
.. example::
.. tabs::
@@ -922,30 +938,32 @@ set through the mapping argument, the default value will be used.
.. code-block:: json
78
```
.. warning::
While most value types are what would expect (integer for
:class:`~odoo.fields.Integer`, string for :class:`~odoo.fields.Char`
or :class:`~odoo.fields.Text`),
:::{warning}
While most value types are what would expect (integer for
{class}`~odoo.fields.Integer`, string for {class}`~odoo.fields.Char`
or {class}`~odoo.fields.Text`),
- :class:`~odoo.fields.Date`, :class:`~odoo.fields.Datetime` and
:class:`~odoo.fields.Binary` fields use string values
- :class:`~odoo.fields.One2many` and :class:`~odoo.fields.Many2many`
use a special command protocol detailed in :meth:`the documentation to
the write method <odoo.models.Model.write>`.
- {class}`~odoo.fields.Date`, {class}`~odoo.fields.Datetime` and
{class}`~odoo.fields.Binary` fields use string values
- {class}`~odoo.fields.One2many` and {class}`~odoo.fields.Many2many`
use a special command protocol detailed in {meth}`the documentation to
the write method <odoo.models.Model.write>`.
:::
Update records
--------------
### Update records
Records can be updated using :meth:`~odoo.models.Model.write`. It takes
Records can be updated using {meth}`~odoo.models.Model.write`. It takes
a list of records to update and a mapping of updated fields to values similar
to :meth:`~odoo.models.Model.create`.
to {meth}`~odoo.models.Model.create`.
Multiple records can be updated simultaneously, but they will all get the same
values for the fields being set. It is not possible to perform
"computed" updates (where the value being set depends on an existing value of
a record).
```{eval-rst}
.. example::
.. tabs::
@@ -1016,13 +1034,14 @@ a record).
.. code-block:: json
[[78, "Newer partner"]]
```
Delete records
--------------
### Delete records
Records can be deleted in bulk by providing their ids to
:meth:`~odoo.models.Model.unlink`.
{meth}`~odoo.models.Model.unlink`.
```{eval-rst}
.. example::
.. tabs::
@@ -1089,41 +1108,51 @@ Records can be deleted in bulk by providing their ids to
.. code-block:: json
[]
```
Inspection and introspection
----------------------------
### Inspection and introspection
While we previously used :meth:`~odoo.models.Model.fields_get` to query a
While we previously used {meth}`~odoo.models.Model.fields_get` to query a
model and have been using an arbitrary model from the start, Odoo stores
most model metadata inside a few meta-models which allow both querying the
system and altering models and fields (with some limitations) on the fly over
XML-RPC.
.. _reference/webservice/inspection/models:
(reference-webservice-inspection-models)=
``ir.model``
~~~~~~~~~~~~
#### `ir.model`
Provides information about Odoo models via its various fields.
``name``
a human-readable description of the model
``model``
the name of each model in the system
``state``
whether the model was generated in Python code (``base``) or by creating
an ``ir.model`` record (``manual``)
``field_id``
list of the model's fields through a :class:`~odoo.fields.One2many` to
:ref:`reference/webservice/inspection/fields`
``view_ids``
:class:`~odoo.fields.One2many` to the :doc:`../reference/user_interface/view_architectures`
defined for the model
``access_ids``
:class:`~odoo.fields.One2many` relation to the
:ref:`reference/security/acl` set on the model
`name`
``ir.model`` can be used to
: a human-readable description of the model
`model`
: the name of each model in the system
`state`
: whether the model was generated in Python code (`base`) or by creating
an `ir.model` record (`manual`)
`field_id`
: list of the model's fields through a {class}`~odoo.fields.One2many` to
{ref}`reference/webservice/inspection/fields`
`view_ids`
: {class}`~odoo.fields.One2many` to the {doc}`../reference/user_interface/view_architectures`
defined for the model
`access_ids`
: {class}`~odoo.fields.One2many` relation to the
{ref}`reference/security/acl` set on the model
`ir.model` can be used to
- Query the system for installed models (as a precondition to operations
on the model or to explore the system's content).
@@ -1131,12 +1160,14 @@ Provides information about Odoo models via its various fields.
associated with it).
- Create new models dynamically over RPC.
.. important::
* Custom model names must start with ``x_``.
* The ``state`` must be provided and set to ``manual``, otherwise the model will
not be loaded.
* It is not possible to add new *methods* to a custom model, only fields.
:::{important}
- Custom model names must start with `x_`.
- The `state` must be provided and set to `manual`, otherwise the model will
not be loaded.
- It is not possible to add new *methods* to a custom model, only fields.
:::
```{eval-rst}
.. example::
A custom model will initially contain only the "built-in" fields available
@@ -1258,42 +1289,59 @@ Provides information about Odoo models via its various fields.
"string": "Id"
}
}
```
.. _reference/webservice/inspection/fields:
(reference-webservice-inspection-fields)=
``ir.model.fields``
~~~~~~~~~~~~~~~~~~~
#### `ir.model.fields`
Provides information about the fields of Odoo models and allows adding
custom fields without using Python code.
``model_id``
:class:`~odoo.fields.Many2one` to
:ref:`reference/webservice/inspection/models` to which the field belongs
``name``
the field's technical name (used in ``read`` or ``write``)
``field_description``
the field's user-readable label (e.g. ``string`` in ``fields_get``)
``ttype``
the :ref:`type <reference/orm/fields>` of field to create
``state``
whether the field was created via Python code (``base``) or via
``ir.model.fields`` (``manual``)
``required``, ``readonly``, ``translate``
enables the corresponding flag on the field
``groups``
:ref:`field-level access control <reference/security/fields>`, a
:class:`~odoo.fields.Many2many` to ``res.groups``
``selection``, ``size``, ``on_delete``, ``relation``, ``relation_field``, ``domain``
type-specific properties and customizations, see :ref:`the fields
documentation <reference/orm/fields>` for details
`model_id`
.. important::
- Like custom models, only new fields created with ``state="manual"`` are activated as actual
fields on the model.
- Computed fields can not be added via ``ir.model.fields``, some field meta-information
(defaults, onchange) can not be set either.
: {class}`~odoo.fields.Many2one` to
{ref}`reference/webservice/inspection/models` to which the field belongs
`name`
: the field's technical name (used in `read` or `write`)
`field_description`
: the field's user-readable label (e.g. `string` in `fields_get`)
`ttype`
: the {ref}`type <reference/orm/fields>` of field to create
`state`
: whether the field was created via Python code (`base`) or via
`ir.model.fields` (`manual`)
`required`, `readonly`, `translate`
: enables the corresponding flag on the field
`groups`
: {ref}`field-level access control <reference/security/fields>`, a
{class}`~odoo.fields.Many2many` to `res.groups`
`selection`, `size`, `on_delete`, `relation`, `relation_field`, `domain`
: type-specific properties and customizations, see {ref}`the fields
documentation <reference/orm/fields>` for details
:::{important}
- Like custom models, only new fields created with `state="manual"` are activated as actual
fields on the model.
- Computed fields can not be added via `ir.model.fields`, some field meta-information
(defaults, onchange) can not be set either.
:::
```{eval-rst}
.. example::
.. tabs::
@@ -1456,7 +1504,9 @@ custom fields without using Python code.
"display_name": "test record"
}
]
```
[base64]: https://en.wikipedia.org/wiki/Base64
[postgresql]: https://www.postgresql.org
[xml-rpc]: https://en.wikipedia.org/wiki/XML-RPC
.. _PostgreSQL: https://www.postgresql.org
.. _XML-RPC: https://en.wikipedia.org/wiki/XML-RPC
.. _base64: https://en.wikipedia.org/wiki/Base64
-574
View File
@@ -1,574 +0,0 @@
===========
Extract API
===========
Odoo provides a service to automate the processing of documents of type **invoices**, **bank statements**,
**expenses** or **resumes**.
The service scans documents using an :abbr:`OCR (Optical Character Recognition)` engine and then
uses :abbr:`AI(Artificial Intelligence)`-based algorithms to extract fields of interest such as the
total, due date, or invoice lines for *invoices*, the initial and final balances, the date for
*bank statements*, the total, date for *expenses*, or the name, email, phone number for *resumes*.
This service is a paid service. Each document processing will cost you one credit.
Credits can be bought on `iap.odoo.com <https://iap.odoo.com/iap/in-app-services/259?sortby=date>`_.
You can either use this service directly in the Accounting, Expense, or Recruitment App or through
the API. The Extract API, which is detailed in the next section, allows you to integrate our
service directly into your own projects.
Overview
========
The extract API uses the JSON-RPC2_ protocol; its endpoint routes are located at
`https://extract.api.odoo.com`.
.. _extract_api/version:
Version
-------
The version of the Extract API is specified in the route.
The latest versions are:
- invoices: 123
- bank statements: 100
- expenses: 132
- applicant: 102
Flow
----
The flow is the same for each document type.
#. | Call :ref:`/parse <extract_api/parse>` to submit your documents (one call for each
document). On success, you receive a `document_token` in the response.
#. | You then have to regularly poll :ref:`/get_result <extract_api/get_result>` to get the
document's parsing status.
| Alternatively, you can provide a `webhook_url` at the time of the call to
:ref:`/parse <extract_api/parse>` and you will be notified (via a POST request) when the
result is ready.
The HTTP POST method should be used for all of them. A python implementation of the full flow for
invoices can be found :download:`here <extract_api/implementation.py>` and a token for integration
testing is provided in the
:ref:`integration testing section <latestextract_api/integration_testing>`.
Parse
=====
Request the processing of a document from the OCR. The route will return a `document_token`,
you can use it to obtain the result of your request.
.. _extract_api/parse:
Routes
------
- /api/extract/invoice/2/parse
- /api/extract/bank_statement/1/parse
- /api/extract/expense/2/parse
- /api/extract/applicant/2/parse
Request
-------
.. rst-class:: o-definition-list
``jsonrpc`` (required)
see JSON-RPC2_
``method`` (required)
see JSON-RPC2_
``id`` (required)
see JSON-RPC2_
``params``
.. rst-class:: o-definition-list
``account_token`` (required)
The token of the account from which credits will be taken. Each successful call costs one
token.
``version`` (required)
The version will determine the format of your requests and the format of the server response.
You should use the :ref:`latest version available <extract_api/version>`.
``documents`` (required)
The document must be provided as a string in the ASCII encoding. The list should contain
only one string. If multiple strings are provided only the first string corresponding to a
pdf will be processed. If no pdf is found, the first string will be processed. This field
is a list only for legacy reasons. The supported extensions are *pdf*, *png*, *jpg* and
*bmp*.
``dbuuid`` (optional)
Unique identifier of the Odoo database.
``webhook_url`` (optional)
A webhook URL can be provided. An empty POST request will be sent to
``webhook_url/document_token`` when the result is ready.
``user_infos`` (optional)
Information concerning the person sending the document to the extract service. It can be
the client or the supplier (depending on the ``perspective``). This information is not
required in order for the service to work but it greatly improves the quality of the result.
.. rst-class:: o-definition-list
``user_company_vat`` (optional)
VAT number of the user.
``user_company_name`` (optional)
Name of the users company.
``user_company_country_code`` (optional)
Country code of the user. Format:
`ISO3166 alpha-2 <https://www.iban.com/country-codes>`_.
``user_lang`` (optional)
The user language. Format: *language_code + _ + locale* (e.g. fr_FR, en_US).
``user_email`` (optional)
The user email.
``purchase_order_regex`` (optional)
Regex for purchase order identification. Will default to Odoo PO format if not provided.
``perspective`` (optional)
.. rst-class:: o-definition-list
Can be ``client`` or ``supplier``. This field is useful for invoices only.
``client`` means that the user information provided are related to the client of the
invoice.
``supplier`` means that it's related to the supplier.
If not provided, client will be used.
.. code-block:: js
{
"jsonrpc": "2.0",
"method": "call",
"params": {
"account_token": string,
"version": int,
"documents": [string],
"dbuuid": string,
"webhook_url": string,
"user_infos": {
"user_company_vat": string,
"user_company_name": string,
"user_company_country_code": string,
"user_lang": string,
"user_email": string,
"purchase_order_regex": string,
"perspective": string,
},
},
"id": string,
}
.. note::
The ``user_infos`` parameter is optional but it greatly improves the quality of the result,
especially for invoices. The more information you can provide, the better.
Response
--------
.. rst-class:: o-definition-list
``jsonrpc``
see JSON-RPC2_
``id``
see JSON-RPC2_
``result``
.. rst-class:: o-definition-list
``status``
The code indicating the status of the request. See the table below.
``status_msg``
A string giving verbose details about the request status.
``document_token``
Only present if the request is successful.
=========================== ==============================================================
status status_msg
=========================== ==============================================================
`success` Success
`error_unsupported_version` Unsupported version
`error_internal` An error occurred
`error_no_credit` You don't have enough credit
`error_unsupported_format` Unsupported file format
`error_maintenance` Server is currently under maintenance, please try again later
=========================== ==============================================================
.. code-block:: js
{
"jsonrpc": "2.0",
"id": string,
"result": {
"status": string,
"status_msg": string,
"document_token": string,
}
}
.. note::
The API does not actually use the JSON-RPC error scheme. Instead the API has its own error
scheme bundled inside a successful JSON-RPC result.
Get results
===========
.. _extract_api/get_result:
Routes
------
- /api/extract/invoice/2/get_result
- /api/extract/bank_statement/1/get_result
- /api/extract/expense/2/get_result
- /api/extract/applicant/2/get_result
Request
-------
.. rst-class:: o-definition-list
``jsonrpc`` (required)
see JSON-RPC2_
``method`` (required)
see JSON-RPC2_
``id`` (required)
see JSON-RPC2_
``params``
.. rst-class:: o-definition-list
``version`` (required)
The version should match the version passed to the :ref:`/parse <extract_api/parse>` request.
``document_token`` (required)
The ``document_token`` for which you want to get the current parsing status.
``account_token`` (required)
The token of the account that was used to submit the document.
.. code-block:: js
{
"jsonrpc": "2.0",
"method": "call",
"params": {
"version": int,
"document_token": int,
"account_token": string,
},
"id": string,
}
Response
--------
When getting the results from the parse, the detected field vary a lot depending on the type of
document. Each response is a list of dictionaries, one for each document. The keys of the dictionary
are the name of the field and the value is the value of the field.
.. rst-class:: o-definition-list
``jsonrpc``
see JSON-RPC2_
``id``
see JSON-RPC2_
``result``
.. rst-class:: o-definition-list
``status``
The code indicating the status of the request. See the table below.
``status_msg``
A string giving verbose details about the request status.
``results``
Only present if the request is successful.
.. rst-class:: o-definition-list
``full_text_annotation``
Contains the unprocessed full result from the OCR for the document
================================ =============================================================
status status_msg
================================ =============================================================
`success` Success
`error_unsupported_version` Unsupported version
`error_internal` An error occurred
`error_maintenance` Server is currently under maintenance, please try again later
`error_document_not_found` The document could not be found
`error_unsupported_size` The document has been rejected because it is too small
`error_no_page_count` Unable to get page count of the PDF file
`error_pdf_conversion_to_images` Couldn't convert the PDF to images
`error_password_protected` The PDF file is protected by a password
`error_too_many_pages` The document contains too many pages
================================ =============================================================
.. code-block:: js
{
"jsonrpc": "2.0",
"id": string,
"result": {
"status": string,
"status_msg": string,
"results": [
{
"full_text_annotation": string,
"feature_1_name": feature_1_result,
"feature_2_name": feature_2_result,
...
},
...
]
}
}
Common fields
~~~~~~~~~~~~~
.. _latestextract_api/get_result/feature_result:
``feature_result``
******************
Each field of interest we want to extract from the document such as the total or the due date are
also called **features**. An exhaustive list of all the extracted features associated to a type of
document can be found in the sections below.
For each feature, we return a list of candidates and we spotlight the candidate our model predicts
to be the best fit for the feature.
.. rst-class:: o-definition-list
``selected_value`` (optional)
The best candidate for this feature.
``selected_values`` (optional)
The best candidates for this feature.
``candidates`` (optional)
List of all the candidates for this feature ordered by decreasing confidence score.
.. code-block:: js
"feature_name": {
"selected_value": candidate_12,
"candidates": [candidate_12, candidate_3, candidate_4, ...]
}
candidate
*********
For each candidate we give its representation and position in the document. Candidates are sorted
by decreasing order of suitability.
.. rst-class:: o-definition-list
``content``
Representation of the candidate.
``coords``
.. rst-class:: o-definition-list
``[center_x, center_y, width, height, rotation_angle]``. The position and dimensions are
relative to the size of the page and are therefore between 0 and 1.
The angle is a clockwise rotation measured in degrees.
``page``
Page of the original document on which the candidate is located (starts at 0).
.. code-block:: js
"candidate": [
{
"content": string|float,
"coords": [float, float, float, float, float],
"page": int
},
...
]
Invoices
~~~~~~~~
Invoices are complex and can have a lot of different fields. The following table gives an exhaustive
list of all the fields we can extract from an invoice.
+-------------------------+------------------------------------------------------------------------+
| Feature name | Specificities |
+=========================+========================================================================+
| ``SWIFT_code`` | ``content`` is a dictionary encoded as a string. |
| | |
| | It contains information about the detected SWIFT code |
| | (or `BIC <https://www.iso9362.org/isobic/overview.html>`_). |
| | |
| | Keys: |
| | |
| | .. rst-class:: o-definition-list |
| | |
| | ``bic`` |
| | detected BIC (string). |
| | ``name`` (optional) |
| | bank name (string). |
| | ``country_code`` |
| | ISO3166 alpha-2 country code of the bank (string). |
| | ``city`` (optional) |
| | city of the bank (string). |
| | ``verified_bic`` |
| | True if the BIC has been found in our DB (bool). |
| | |
| | Name and city are present only if verified_bic is true. |
+-------------------------+------------------------------------------------------------------------+
| ``iban`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``aba`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``VAT_Number`` | ``content`` is a string |
| | |
| | Depending on the value of perspective in the user_infos, this will be |
| | the VAT number of the supplier or the client. If perspective is |
| | client, it'll be the supplier's VAT number. If it's supplier, it's the |
| | client's VAT number. |
+-------------------------+------------------------------------------------------------------------+
| ``qr-bill`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``payment_ref`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``purchase_order`` | ``content`` is a string |
| | |
| | Uses ``selected_values`` instead of ``selected_value`` |
+-------------------------+------------------------------------------------------------------------+
| ``country`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``currency`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``date`` | ``content`` is a string |
| | |
| | Format : *YYYY-MM-DD* |
+-------------------------+------------------------------------------------------------------------+
| ``due_date`` | Same as for ``date`` |
+-------------------------+------------------------------------------------------------------------+
| ``total_tax_amount`` | ``content`` is a float |
+-------------------------+------------------------------------------------------------------------+
| ``invoice_id`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``subtotal`` | ``content`` is a float |
+-------------------------+------------------------------------------------------------------------+
| ``total`` | ``content`` is a float |
+-------------------------+------------------------------------------------------------------------+
| ``supplier`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``client`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``email`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``website`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
``invoice_lines`` feature
*************************
It is returned as a list of dictionaries where each dictionary represents an invoice line.
.. code-block:: js
"invoice_lines": [
{
"description": string,
"quantity": float,
"subtotal": float,
"total": float,
"taxes": list[float],
"total": float,
"unit_price": float
},
...
]
Bank statements
~~~~~~~~~~~~~~~
The following table gives a list of all the fields that are extracted from bank statements.
+-------------------------+------------------------------------------------------------------------+
| Feature name | Specificities |
+=========================+========================================================================+
| ``balance_start`` | ``content`` is a float |
+-------------------------+------------------------------------------------------------------------+
| ``balance_end`` | ``content`` is a float |
+-------------------------+------------------------------------------------------------------------+
| ``date`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
``bank_statement_lines`` feature
********************************
It is returned as a list of dictionaries where each dictionary represents a bank statement line.
.. code-block:: js
"bank_statement_lines": [
{
"amount": float,
"description": string,
"date": string,
},
...
]
Expense
~~~~~~~
The expenses are less complex than invoices. The following table gives an exhaustive list of all the
fields we can extract from an expense report.
+-------------------------+------------------------------------------------------------------------+
| Feature name | Specificities |
+=========================+========================================================================+
| ``description`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``country`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``date`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``total`` | ``content`` is a float |
+-------------------------+------------------------------------------------------------------------+
| ``currency`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
Applicant
~~~~~~~~~
This third type of document is meant for processing resumes. The following table gives an exhaustive
list of all the fields we can extract from a resume.
+-------------------------+------------------------------------------------------------------------+
| Feature name | Specificities |
+=========================+========================================================================+
| ``name`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``email`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``phone`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
| ``mobile`` | ``content`` is a string |
+-------------------------+------------------------------------------------------------------------+
.. _latestextract_api/integration_testing:
Integration Testing
===================
You can test your integration by using *integration_token* as ``account_token`` in the
:ref:`/parse <extract_api/parse>` request.
Using this token put you in test mode and allows you to simulate the entire flow without really
parsing a document and without being billed one credit for each successful **document** parsing.
The only technical differences in test mode is that the document you send is not parsed by the
system and that the response you get from
:ref:`/get_result <extract_api/get_result>` is a hard-coded one.
A python implementation of the full flow for invoices can be found
:download:`here <extract_api/implementation.py>`.
.. _JSON-RPC2: https://www.jsonrpc.org/specification
.. |ss| raw:: html
<strike>
.. |se| raw:: html
</strike>
+25
View File
@@ -0,0 +1,25 @@
---
hide-page-toc: true
nosearch: true
---
# Web framework
```{toctree}
:titlesonly: true
frontend/framework_overview
frontend/assets
frontend/javascript_modules
frontend/owl_components
frontend/registries
frontend/services
frontend/hooks
frontend/patching_code
frontend/error_handling
frontend/javascript_reference
frontend/mobile
frontend/qweb
frontend/odoo_editor
```
-23
View File
@@ -1,23 +0,0 @@
:nosearch:
:hide-page-toc:
=============
Web framework
=============
.. toctree::
:titlesonly:
frontend/framework_overview
frontend/assets
frontend/javascript_modules
frontend/owl_components
frontend/registries
frontend/services
frontend/hooks
frontend/patching_code
frontend/error_handling
frontend/javascript_reference
frontend/mobile
frontend/qweb
frontend/odoo_editor
@@ -1,45 +1,44 @@
(reference-assets)=
.. _reference/assets:
======
Assets
======
# Assets
Managing assets in Odoo is not as straightforward as it is in some other apps.
One of the reasons is that we have a variety of situations where some, but not all
of the assets are required. For example, the needs of the web client, the point of
sale app, the website or even the mobile application are different. Also, some
assets may be large, but are seldom needed: in that case we may want them
to be :ref:`loaded lazily (on demand) <frontend/assets/lazy_loading>`.
to be {ref}`loaded lazily (on demand) <frontend/assets/lazy_loading>`.
Asset types
===========
## Asset types
There are three different asset types: code (`js` files), style (`css` or `scss`
files) and templates (`xml` files).
Code
Odoo supports :ref:`three different kinds of javascript files<frontend/js_modules>`.
All these files are then processed (native JS modules are transformed into odoo
modules), then minified (if not in `debug=assets` :ref:`mode <frontend/framework/assets_debug_mode>`)
and concatenated. The result is then saved as a file attachment. These file
attachments are usually loaded via a `<script>` tag in the `<head>` part of
the page (as a static file).
: Odoo supports {ref}`three different kinds of javascript files<frontend/js_modules>`.
All these files are then processed (native JS modules are transformed into odoo
modules), then minified (if not in `debug=assets` {ref}`mode <frontend/framework/assets_debug_mode>`)
and concatenated. The result is then saved as a file attachment. These file
attachments are usually loaded via a `<script>` tag in the `<head>` part of
the page (as a static file).
Style
Styling can be done with either `css` or `scss <https://sass-lang.com/>`_. Like
the javascript files, these files are processed (`scss` files are converted into
`css`), then minified (again, if not in `debug=assets` :ref:`mode <frontend/framework/assets_debug_mode>`)
and concatenated. The result is then saved as a file attachment. They are
then usually loaded via a `<link>` tag in the `<head>` part of the page (as
a static file).
: Styling can be done with either `css` or [scss](https://sass-lang.com/). Like
the javascript files, these files are processed (`scss` files are converted into
`css`), then minified (again, if not in `debug=assets` {ref}`mode <frontend/framework/assets_debug_mode>`)
and concatenated. The result is then saved as a file attachment. They are
then usually loaded via a `<link>` tag in the `<head>` part of the page (as
a static file).
Template
Templates (static `xml` files) are handled in a different way: they are simply
read from the file system whenever they are needed, and concatenated.
Whenever the browser loads odoo, it calls the `/web/webclient/qweb/` controller
to fetch the :ref:`templates <reference/qweb>`.
: Templates (static `xml` files) are handled in a different way: they are simply
read from the file system whenever they are needed, and concatenated.
Whenever the browser loads odoo, it calls the `/web/webclient/qweb/` controller
to fetch the {ref}`templates <reference/qweb>`.
It is useful to know that in most cases, a browser only performs a request the
first time it loads a page. This is because each of these assets are
@@ -47,38 +46,37 @@ associated with a checksum, which is injected into the page source. The checksum
is then added to the url, which means that it is possible to safely set the cache
headers to a long period.
.. _reference/assets_bundle:
(reference-assets-bundle)=
Bundles
=======
## Bundles
Odoo assets are grouped by *bundles*. Each bundle (a *list of file paths*
of specific types: `xml`, `js`, `css` or `scss`) is listed in the
:ref:`module manifest <reference/module/manifest>`. Files can be declared using
`glob <https://en.wikipedia.org/wiki/Glob_(programming)>`_ syntax, meaning that
{ref}`module manifest <reference/module/manifest>`. Files can be declared using
[glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) syntax, meaning that
you can declare several asset files using a single line.
The bundles are defined in each module's :file:`__manifest__.py`,
The bundles are defined in each module's {file}`__manifest__.py`,
with a dedicated `assets` key which contains a dictionary. The dictionary maps
bundle names (keys) to the list of files they contain (values). It looks
like this:
.. code-block:: python
'assets': {
'web.assets_backend': [
'web/static/src/xml/**/*',
],
'web.assets_common': [
'web/static/lib/bootstrap/**/*',
'web/static/src/js/boot.js',
'web/static/src/js/webclient.js',
'web/static/src/xml/webclient.xml',
],
'web.qunit_suite_tests': [
'web/static/src/js/webclient_tests.js',
],
},
```python
'assets': {
'web.assets_backend': [
'web/static/src/xml/**/*',
],
'web.assets_common': [
'web/static/lib/bootstrap/**/*',
'web/static/src/js/boot.js',
'web/static/src/js/webclient.js',
'web/static/src/xml/webclient.xml',
],
'web.qunit_suite_tests': [
'web/static/src/js/webclient_tests.js',
],
},
```
Here is a list of some important bundles that most odoo developers will need to
know:
@@ -86,21 +84,15 @@ know:
- `web.assets_common`: this bundle contains most assets which are common to the
web client, the website and also the point of sale. This is supposed to contain
lower level building blocks for the odoo framework. Note that it contains the
:file:`boot.js` file, which defines the odoo module system.
{file}`boot.js` file, which defines the odoo module system.
- `web.assets_backend`: this bundle contains the code specific to the web client
(notably the web client/action manager/views/static XML templates)
- `web.assets_frontend`: this bundle is about all that is specific to the public
website: ecommerce, portal, forum, blog, ...
- `web.qunit_suite_tests`: all javascript qunit testing code (tests, helpers, mocks)
- `web.qunit_mobile_suite_tests`: mobile specific qunit testing code
Operations
----------
### Operations
Typically, handling assets is simple: you just need to add some new files
to a frequently used bundle like `assets_common` or `assets_backend`. But there are other operations
@@ -108,27 +100,25 @@ available to cover some more specific use cases.
Note that all directives targeting a certain asset file (i.e. `before`, `after`,
`replace` and `remove`) need that file to be declared beforehand, either
in manifests higher up in the hierarchy or in ``ir.asset`` records with a lower
in manifests higher up in the hierarchy or in `ir.asset` records with a lower
sequence.
`append`
~~~~~~~~
#### `append`
This operation adds one or multiple file(s). Since it is the most common
operation, it can be done by simply using the file name:
.. code-block:: python
'web.assets_common': [
'my_addon/static/src/js/**/*',
],
```python
'web.assets_common': [
'my_addon/static/src/js/**/*',
],
```
By default, adding a simple string to a bundle will append the files matching the
glob pattern at the end of the bundle. Obviously, the pattern may also be directly
a single file path.
`prepend`
~~~~~~~~~
#### `prepend`
Add one or multiple file(s) at the beginning of the bundle.
@@ -136,14 +126,13 @@ Useful when you need to put a certain file before the others in a bundle (for
example with css files). The `prepend` operation is invoked with the following
syntax: `('prepend', <path>)`.
.. code-block:: python
```python
'web.assets_common': [
('prepend', 'my_addon/static/src/css/bootstrap_overridden.scss'),
],
```
'web.assets_common': [
('prepend', 'my_addon/static/src/css/bootstrap_overridden.scss'),
],
`before`
~~~~~~~~
#### `before`
Add one or multiple file(s) before a specific file.
@@ -152,14 +141,13 @@ Prepending a file at the beginning of a bundle might not be precise enough. The
file. It is declared by replacing the normal path with a 3-element tuple
`('before', <target>, <path>)`.
.. code-block:: python
```python
'web.assets_common': [
('before', 'web/static/src/css/bootstrap_overridden.scss', 'my_addon/static/src/css/bootstrap_overridden.scss'),
],
```
'web.assets_common': [
('before', 'web/static/src/css/bootstrap_overridden.scss', 'my_addon/static/src/css/bootstrap_overridden.scss'),
],
`after`
~~~~~~~
#### `after`
Add one or multiple file(s) after a specific file.
@@ -167,14 +155,13 @@ Same as `before`, with the matching file(s) appended right *after* the target fi
It is declared by replacing the normal path with a 3-element tuple
`('after', <target>, <path>)`.
.. code-block:: python
```python
'web.assets_common': [
('after', 'web/static/src/css/list_view.scss', 'my_addon/static/src/css/list_view.scss'),
],
```
'web.assets_common': [
('after', 'web/static/src/css/list_view.scss', 'my_addon/static/src/css/list_view.scss'),
],
`include`
~~~~~~~~~
#### `include`
Use nested bundles.
@@ -183,14 +170,13 @@ the size of your manifest. In Odoo we use sub bundles (prefixed with an undersco
by convention) to batch files used in multiple other bundles. You can then
specify the sub bundle as a pair `('include', <bundle>)` like this:
.. code-block:: python
```python
'web.assets_common': [
('include', 'web._primary_variables'),
],
```
'web.assets_common': [
('include', 'web._primary_variables'),
],
`remove`
~~~~~~~~
#### `remove`
Remove one or multiple file(s).
@@ -198,14 +184,13 @@ In some cases, you may want to remove one or multiple files from a bundle. This
can be done using the `remove` directive by specifying a pair
`('remove', <target>)`:
.. code-block:: python
```python
'web.assets_common': [
('remove', 'web/static/src/js/boot.js'),
],
```
'web.assets_common': [
('remove', 'web/static/src/js/boot.js'),
],
`replace`
~~~~~~~~~
#### `replace`
Replace an asset file with one or multiple file(s).
@@ -213,39 +198,34 @@ Let us say that an asset needs not only to be removed, but you also want to inse
your new version of that asset at the same exact position. This can be done with
the `replace` directive, using a 3-element tuple `('replace', <target>, <path>)`:
.. code-block:: python
```python
'web.assets_common': [
('replace', 'web/static/src/js/boot.js', 'my_addon/static/src/js/boot.js'),
],
```
'web.assets_common': [
('replace', 'web/static/src/js/boot.js', 'my_addon/static/src/js/boot.js'),
],
Loading order
-------------
### Loading order
The order in which assets are loaded is sometimes critical and must be deterministic,
mostly for stylesheets priorities and setup scripts. Assets in Odoo are processed
as follows:
#. When an asset bundle is called (e.g. `t-call-assets="web.assets_common"`), an empty
1. When an asset bundle is called (e.g. `t-call-assets="web.assets_common"`), an empty
list of assets is generated
#. All records of type `ir.asset` matching the bundle are fetched and sorted
2. All records of type `ir.asset` matching the bundle are fetched and sorted
by sequence number. Then all records with a sequence strictly less than 16 are
processed and applied to the current list of assets.
#. All modules declaring assets for said bundle in their manifest apply their
3. All modules declaring assets for said bundle in their manifest apply their
assets operations to this list. This is done following the order of modules dependencies
(e.g. `web` assets is processed before `website`). If a directive tries to add
a file already present in the list, nothing is done for that file. In other word,
only the first occurrence of a file is kept in the list.
#. The remaining `ir.asset` records (those with a sequence greater than or equal
4. The remaining `ir.asset` records (those with a sequence greater than or equal
to 16) are then processed and applied as well.
Assets declared in the manifest may need to be loaded in a particular order, for
example :file:`jquery.js` must be loaded before all other jquery scripts when loading the
lib folder. One solution would be to create an :ref:`ir.asset <frontend/assets/ir_asset>`
example {file}`jquery.js` must be loaded before all other jquery scripts when loading the
lib folder. One solution would be to create an {ref}`ir.asset <frontend/assets/ir_asset>`
record with a lower sequence or a 'prepend' directive, but there is another simpler
way to do so.
@@ -253,35 +233,34 @@ Since the unicity of each file path in the list of assets is guaranteed, you can
mention any specific file before a glob that includes it. That file will thus appear
in the list before all the others included in the glob.
.. code-block:: python
```python
'web.assets_common': [
'my_addon/static/lib/jquery/jquery.js',
'my_addon/static/lib/jquery/**/*',
],
```
'web.assets_common': [
'my_addon/static/lib/jquery/jquery.js',
'my_addon/static/lib/jquery/**/*',
],
:::{note}
A module *b* removing/replacing the assets declared in a module *a* will have
to depend on it. Trying to operate on assets that have yet to be declared will
result in an error.
:::
.. note::
(frontend-assets-lazy-loading)=
A module *b* removing/replacing the assets declared in a module *a* will have
to depend on it. Trying to operate on assets that have yet to be declared will
result in an error.
.. _frontend/assets/lazy_loading:
Lazy loading
============
## Lazy loading
It is sometimes useful to load files and/or asset bundles dynamically, for
example to only load a library once it is needed. To do that, the Odoo framework
provides a few helper functions, located in :file:`@web/core/assets`.
.. code-block:: javascript
await loadAssets({
jsLibs: ["/web/static/lib/stacktracejs/stacktrace.js"],
});
provides a few helper functions, located in {file}`@web/core/assets`.
```javascript
await loadAssets({
jsLibs: ["/web/static/lib/stacktracejs/stacktrace.js"],
});
```
```{eval-rst}
.. js:function:: loadAssets(assets)
:param Object assets: a description of various assets that should be loaded
@@ -304,18 +283,20 @@ provides a few helper functions, located in :file:`@web/core/assets`.
- `string[]`
- a list of urls of css files
```
```{eval-rst}
.. js:function:: useAssets(assets)
:param Object assets: a description of various assets that should be loaded
This hook is useful when components need to load some assets in their
`onWillStart` method. It internally calls `loadAssets`.
```
.. _frontend/assets/ir_asset:
(frontend-assets-ir-asset)=
The asset model (`ir.asset`)
============================
## The asset model (`ir.asset`)
In most cases the assets declared in the manifest will largely suffice. Yet for
more flexibility, the framework also supports dynamic assets declared in the
@@ -325,43 +306,55 @@ This is done by creating `ir.asset` records. Those will be processed as if they
were found in a module manifest, and they give the same expressive power as their
manifest counterparts.
```{eval-rst}
.. autoclass:: odoo.addons.base.models.ir_asset.IrAsset
```
```{eval-rst}
.. rst-class:: o-definition-list
```
`name`
Name of the asset record (for identification purpose).
: Name of the asset record (for identification purpose).
`bundle`
Bundle in which the asset will be applied.
: Bundle in which the asset will be applied.
`directive` (default= `append`)
This field determines how the `path` (and `target` if needed) will be interpreted.
Here is the list of available directives along with their required arguments:
- **append**: `path`
- **prepend**: `path`
- **before**: `target`, `path`
- **after**: `target`, `path`
- **include**: `path` (interpreted as a **bundle name**)
- **remove**: `path` (interpreted as a **target asset** to remove)
- **replace**: `target`, `path`
: This field determines how the `path` (and `target` if needed) will be interpreted.
Here is the list of available directives along with their required arguments:
- **append**: `path`
- **prepend**: `path`
- **before**: `target`, `path`
- **after**: `target`, `path`
- **include**: `path` (interpreted as a **bundle name**)
- **remove**: `path` (interpreted as a **target asset** to remove)
- **replace**: `target`, `path`
`path`
A string defining one of the following:
- a **relative path** to an asset file in the addons file system;
- a **glob pattern** to a set of asset files in the addons file system;
- a **URL** to an attachment or external asset file;
- a **bundle name**, when using the `include` directive.
: A string defining one of the following:
- a **relative path** to an asset file in the addons file system;
- a **glob pattern** to a set of asset files in the addons file system;
- a **URL** to an attachment or external asset file;
- a **bundle name**, when using the `include` directive.
`target`
Target file to specify a position in the bundle. Can only be used with the
directives `replace`, `before` and `after`.
: Target file to specify a position in the bundle. Can only be used with the
directives `replace`, `before` and `after`.
`active` (default= `True`)
Whether the record is active
: Whether the record is active
`sequence` (default= `16`)
Loading order of the asset records (ascending). A sequence lower than 16 means
that the asset will be processed *before* the ones declared in the manifest.
: Loading order of the asset records (ascending). A sequence lower than 16 means
that the asset will be processed *before* the ones declared in the manifest.
@@ -1,6 +1,4 @@
==============
Error handling
==============
# Error handling
In programming, error handling is a complex topic with many pitfalls, and it can
be even more daunting when you're writing code within the constraints of a framework,
@@ -11,24 +9,21 @@ This article paints the broad strokes of how errors are handled by the JavaScrip
framework and Owl, and gives some recommendations on how to interface with these
systems in a way that avoids common problems.
Errors in JavaScript
====================
## Errors in JavaScript
Before we dive into how errors are handled in Odoo as well as how and where to
customize error handling behavior, it's a good idea to make sure we're on the
same page when it comes to what we mean exactly by "error", as well as some of
the peculiarities of error handling in JavaScript.
The `Error` class
-----------------
### The `Error` class
The first thing that may come to mind when we talk about error handling is the
built-in `Error` class, or classes that extend it. In the rest of this article,
when we refer to an object that is an instance of this class, we will
use the term *Error object* in italics.
Anything can be thrown
----------------------
### Anything can be thrown
In JavaScript, you can throw any value. It is customary to throw *Error objects*,
but it is possible to throw any other object, and even primitives. While we don't
@@ -54,19 +49,18 @@ when handling them. Unfortunately, JavaScript does not have syntactic support fo
filtering by error class in the catch clause, but you can relatively easily do
it yourself:
.. code-block:: javascript
try {
doStuff();
} catch (e) {
if (!(e instanceof MyErrorClass)) {
throw e; // caught an error we can't handle, rethrow
}
// handle MyErrorClass
```javascript
try {
doStuff();
} catch (e) {
if (!(e instanceof MyErrorClass)) {
throw e; // caught an error we can't handle, rethrow
}
// handle MyErrorClass
}
```
Promise rejections are errors
-----------------------------
### Promise rejections are errors
During the early days of Promise adoption, Promises were often treated as a way
to store a disjoint union of a result and an "error", and it was pretty common to
@@ -90,8 +84,7 @@ way as thrown errors. Do not create rejected promises in places where you would
not throw an error, and always reject Promises with *Error objects* as their rejection
reason.
`error` events are not errors
-----------------------------
### `error` events are not errors
With the exception of `error` events on the window, `error` events on other objects
such as `<media>`, `<audio>` `<img>`, `<script>` and `<link>` elements, or
@@ -100,18 +93,17 @@ specifically refers only to thrown values and rejected promises. If you need to
handle errors on these elements or want them to be treated as errors, you need to
explicitly add an event listener for said event:
.. code-block:: javascript
```javascript
const scriptEl = document.createElement("script");
scriptEl.src = "https://example.com/third_party_script.js";
return new Promise((resolve, reject) => {
scriptEl.addEventListener("error", reject);
scriptEl.addEventListener("load", resolve);
document.head.append(scriptEl);
});
```
const scriptEl = document.createElement("script");
scriptEl.src = "https://example.com/third_party_script.js";
return new Promise((resolve, reject) => {
scriptEl.addEventListener("error", reject);
scriptEl.addEventListener("load", resolve);
document.head.append(scriptEl);
});
Lifecycle of errors within the Odoo JS framework
================================================
## Lifecycle of errors within the Odoo JS framework
Thrown errors unwind their call stack to find a catch clause that can handle
them. The way an error is handled depends on what code is encountered while
@@ -119,8 +111,7 @@ unwinding the call stack. While there are a virtually infinite number of places
errors could be thrown from, there are only a few possible paths into the JS framework's
error handling code.
Throwing an error at the top-level of a module
----------------------------------------------
### Throwing an error at the top-level of a module
When a JS module is loaded, the code at the top level of that module is executed and
may throw. While the framework might report these errors with a dialog, module loading
@@ -134,8 +125,7 @@ Any error handling and reporting that does happen at this stage is purely with t
objective of helping you, the developer, fix the code that threw the error, and
we provide no mechanism to customize how these errors are handled.
The error service
-----------------
### The error service
When an error is thrown but never caught, the runtime dispatches an event on the
global object (`window`). The type of the event depends on whether the error was
@@ -177,8 +167,7 @@ not able to collect stack trace information about the thrown error, we do not ca
or other random objects. In those cases, the browser logs the stack trace itself,
as it has that information but does not expose it to the JS code.
The `error_handlers` registry
-----------------------------
### The `error_handlers` registry
The `error_handlers` registry is the main way to extend the way that the JS framework
handles "generic" errors. Generic errors, in this context, means errors that can happen
@@ -195,8 +184,7 @@ in many places, but that should be handled uniformly. Some examples:
be displayed the same way regardless of where it happens
- LostConnection: same reasoning again.
Throwing an error in an Owl component
-------------------------------------
### Throwing an error in an Owl component
Registering or modifying Owl components is the main way in which you can extend the
functionality of the web client. As such, most errors that are thrown are in one
@@ -217,8 +205,9 @@ error handlers with the `onError` hook to attempt to handle the error. If the er
is not handled by any of them, Owl destroys the application as it is likely in a
corrupted state.
.. seealso::
`Error handling in the Owl documentation <https://github.com/odoo/owl/blob/master/doc/reference/error_handling.md>`_
:::{seealso}
[Error handling in the Owl documentation](https://github.com/odoo/owl/blob/master/doc/reference/error_handling.md)
:::
Inside Odoo, there are some places where we do not want the entire application to
crash in case of error, and so the framework has a few places where it uses the
@@ -238,27 +227,28 @@ hooks are called by Owl code, most of this information is *generally* not very u
for developers, but knowing where the hook was registered and by which component
is very useful.
When reading errors that mention "OwlError: the following error occurred in <hookName>",
When reading errors that mention "OwlError: the following error occurred in \<hookName>",
make sure to read both parts of the composite stack trace:
.. code-block::
:emphasize-lines: 4,12
```{code-block}
:emphasize-lines: 4,12
Error: The following error occurred in onMounted: "My error"
at wrapError
at onMounted
at MyComponent.setup
at new ComponentNode
at Root.template
at MountFiber._render
at MountFiber.render
at ComponentNode.initiateRender
Error: The following error occurred in onMounted: "My error"
at wrapError
at onMounted
at MyComponent.setup
at new ComponentNode
at Root.template
at MountFiber._render
at MountFiber.render
at ComponentNode.initiateRender
Caused by: Error: My error
at ParentComponent.someMethod
at MountFiber.complete
at Scheduler.processFiber
at Scheduler.processTasks
Caused by: Error: My error
at ParentComponent.someMethod
at MountFiber.complete
at Scheduler.processFiber
at Scheduler.processTasks
```
The first highlighted line tells you which component registered the `onMounted`
hook, while the second highlighted line tells you which function threw the error.
@@ -268,16 +258,14 @@ of information can be useful, as the method could have been called by mistake by
the child (or at a point in the lifecycle where it shouldn't), but it could also
be that the parent's method contains a bug.
Marking errors as handled
-------------------------
### Marking errors as handled
In the previous sections, we talked about two ways to register error handlers: one
is adding them to the `error_handlers` registry, the other is using the `onError`
hook in owl. In both cases, the handler has to decide whether to mark the error as
handled.
`onError`
~~~~~~~~~
#### `onError`
In the case of a handler registered in Owl with `onError`, the error is considered
by Owl as handled unless you rethrow it. Whatever you do in `onError`, the user
@@ -292,18 +280,18 @@ you should do instead is dispatch this error in a separate call stack outside of
Owl. The easiest way to do this is to simply create a rejected Promise with the error
as its rejection reason:
.. code-block:: javascript
import { Component, onError } from "@odoo/owl";
class MyComponent extends Component {
setup() {
onError((error) => {
// implementation of this method is left as an exercise for the reader
this.removeErroringSubcomponent();
Promise.reject(error); // create a rejected Promise without passing it anywhere
});
}
}
```javascript
import { Component, onError } from "@odoo/owl";
class MyComponent extends Component {
setup() {
onError((error) => {
// implementation of this method is left as an exercise for the reader
this.removeErroringSubcomponent();
Promise.reject(error); // create a rejected Promise without passing it anywhere
});
}
}
```
This causes the browser to dispatch an `unhandledrejection` event on the window, which
causes the JS framework's error handling to kick in and deal with the error, in
@@ -311,8 +299,7 @@ most cases by opening a dialog with information about the error. This is the str
that is used internally by the action service and dialog service to stop rendering
broken actions or dialogs while still reporting the error.
Handler in the `error_handlers` registry
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#### Handler in the `error_handlers` registry
Handlers that are added to the `error_handlers` registry can mark an error as being
handled in two ways, with different meanings.
@@ -335,15 +322,12 @@ When not calling `preventDefault`, the error is treated as unexpected, any such
occurrence during a test causes the test to fail, as it's generally indicative
of defective code.
Avoid throwing errors as much as possible
=========================================
## Avoid throwing errors as much as possible
Errors introduce complexity in many ways, here are some reasons why you should
avoid throwing them.
Errors are expensive
--------------------
### Errors are expensive
Because errors need to unwind the callstack and collect information as they do so,
throwing errors is slow. Additionally, JavaScript runtimes are generally optimized
@@ -351,8 +335,7 @@ with the assumption that exceptions are rare, and as such generally compiles the
code with the assumption that it doesn't throw, and fall back to a slower code path
if it ever does.
Throwing errors makes debugging harder
--------------------------------------
### Throwing errors makes debugging harder
JavaScript debuggers, like the one included in the Chrome and Firefox devtools for example,
have a feature that allows you to pause the execution when an exception is thrown. You
@@ -384,18 +367,16 @@ because clicking the play button in the debugger removes focus from the page, it
even make the interesting throw scenario inaccessible without using the keyboard shortcut
for resuming execution which results in poor developer experience.
Throwing breaks the normal flow of the code
-------------------------------------------
### Throwing breaks the normal flow of the code
When throwing an error, code that looks like it should always execute may be skipped, this
can cause many subtle bugs and memory leaks. Here is a simple example:
.. code-block:: javascript
eventTarget.addEventListener("event", handler);
someFunction();
eventTarget.removeEventListener("event", handler);
```javascript
eventTarget.addEventListener("event", handler);
someFunction();
eventTarget.removeEventListener("event", handler);
```
In this block of code, we add an event listener to an event target, then call a function
which may dispatch events on that target. After the function call, we remove the event
@@ -412,21 +393,20 @@ This is a bug.
To account for this, one would need to wrap the call in a `try` block, and the cleanup in a
`finally` block:
.. code-block:: javascript
eventTarget.addEventListener("event", handler);
try {
someFunction();
} finally {
eventTarget.removeEventListener("event", handler);
}
```javascript
eventTarget.addEventListener("event", handler);
try {
someFunction();
} finally {
eventTarget.removeEventListener("event", handler);
}
```
While this now avoids the problems mentioned above, not only does this require more code,
it also requires knowledge that the function may throw. It would be unmanageable to wrap
all code that may throw in a `try/finally` block.
Catching errors
===============
## Catching errors
Sometimes, you need to call into code that is known to throw errors and you want
to handle some of these errors. There are two important things to keep in mind:
@@ -437,33 +417,33 @@ to handle some of these errors. There are two important things to keep in mind:
the one you're trying to catch. Generally, the try block should contain exactly
*one* statement.
.. code-block:: javascript
let someVal;
try {
someVal = someFunction();
// do not start working with someVal here.
} catch (e) {
if (!(e instanceof MyError)) {
throw e;
}
someVal = null;
}
// start working with someVal here
```javascript
let someVal;
try {
someVal = someFunction();
// do not start working with someVal here.
} catch (e) {
if (!(e instanceof MyError)) {
throw e;
}
someVal = null;
}
// start working with someVal here
```
While this is straightforward with try/catch, it's much easier to accidentally wrap
a much larger portion of code in a catch clause when working with `Promise.catch`:
.. code-block:: javascript
someFunction().then((someVal) => {
// work with someVal
}).catch((e) => {
if (!(e instanceof MyError)) {
throw e;
}
return null;
});
```javascript
someFunction().then((someVal) => {
// work with someVal
}).catch((e) => {
if (!(e instanceof MyError)) {
throw e;
}
return null;
});
```
In this example, the catch block is actually catching errors in the entire then
block, which is not what we want. In this particular example, because we properly
@@ -474,25 +454,24 @@ the null isn't going through the codepath that uses `someVal`. To avoid this,
catch clauses should generally be as close as possible to the promise that may throw,
and should always filter on the error type.
Error free control flow
=======================
## Error free control flow
For the reasons outlined above, you should avoid throwing errors for doing routine
things, and in particular, for control flow. If a function is expected to be unable
to complete its work on a regular basis, it should communicate that failure without
throwing an exception. Consider the example code:
.. code-block:: javascript
let someVal;
try {
someVal = someFunction();
} catch (e) {
if (!(e instanceof MyError)) {
throw e;
}
someVal = null;
}
```javascript
let someVal;
try {
someVal = someFunction();
} catch (e) {
if (!(e instanceof MyError)) {
throw e;
}
someVal = null;
}
```
There are many things that are problematic with this code. First, because we want
the variable `someVal` to be accessible after the `try/catch` block, it needs to be
@@ -520,56 +499,53 @@ error.
The following sections outline some alternative approaches you can use instead of
using errors.
Return `null` or `undefined`
----------------------------
### Return `null` or `undefined`
If the function returns a primitive or an object, you can generally use `null` or
`undefined` to signal that it was unable to do its intended job. This suffices in
most cases. The code ends up looking something like this:
.. code-block:: javascript
const someVal = someFunction();
// further
if (someVal !== null) { /* do something */ }
```javascript
const someVal = someFunction();
// further
if (someVal !== null) { /* do something */ }
```
As you can see, this is much simpler.
Return an object or array
-------------------------
### Return an object or array
In some cases, a value of `null` or `undefined` is part of the expected return values.
In those cases, you can instead return a wrapper object or a two-element array that
contains either the return value or the error:
.. code-block:: javascript
const { val: someVal, err } = someFunction();
if (err) {
return;
}
// do something with someVal as it is known to be valid
```javascript
const { val: someVal, err } = someFunction();
if (err) {
return;
}
// do something with someVal as it is known to be valid
```
Or with an array:
.. code-block:: javascript
```javascript
const [err, someVal] = someFunction();
if (err) {
return;
}
// do something with someVal as it is known to be valid
```
const [err, someVal] = someFunction();
if (err) {
return;
}
// do something with someVal as it is known to be valid
:::{note}
When using a two-element array, it is advisable to have the error be the first
element, so that it is harder to ignore by mistake when destructuring. One would
need to explicitly add a placeholder or comma to skip the error, whereas if the
error is the second element, it is easy to simply destructure only the first
element and mistakenly forget to handle the error.
:::
.. note::
When using a two-element array, it is advisable to have the error be the first
element, so that it is harder to ignore by mistake when destructuring. One would
need to explicitly add a placeholder or comma to skip the error, whereas if the
error is the second element, it is easy to simply destructure only the first
element and mistakenly forget to handle the error.
When to throw errors
====================
## When to throw errors
The previous sections give many good reasons to avoid throwing errors, so what are
some examples of cases where throwing an error is the best course of action?
@@ -586,3 +562,4 @@ some examples of cases where throwing an error is the best course of action?
ergonomic and less error prone than having to manually test for errors and forward
them through many levels of calls. This should be very rare in practice, and needs
to be weighed against all the disadvantages mentioned in this article.
@@ -1,14 +1,11 @@
==================
Framework Overview
==================
# Framework Overview
Introduction
============
## Introduction
The Odoo Javascript framework is a set of features/building blocks provided by
the ``web/`` addon to help build odoo applications running in the browser. At
the `web/` addon to help build odoo applications running in the browser. At
the same time, the Odoo Javascript framework is a single page application,
usually known as the *web client* (available at the url ``/web``).
usually known as the *web client* (available at the url `/web`).
The web client started as an application made with a custom class and widget
system, but it is now transitioning to using native javascript classes instead,
@@ -25,80 +22,78 @@ The javascript framework (all or some parts) is also used in other situations,
such as the Odoo website or the point of sale. This reference is mostly focused
on the web client.
.. note::
:::{note}
It is common in the Odoo ecosystem to see the words *frontend* and *backend*
as synonyms for the odoo website (public) and the web client, respectively.
This terminology is not to be confused with the more common use of
browser-code (frontend) and server (backend).
:::
It is common in the Odoo ecosystem to see the words *frontend* and *backend*
as synonyms for the odoo website (public) and the web client, respectively.
This terminology is not to be confused with the more common use of
browser-code (frontend) and server (backend).
:::{note}
In this documentation, the word *component* always refers to new Owl
components, and *widget* refers to old Odoo widgets.
:::
.. note::
:::{note}
All new development should be done in Owl, if possible!
:::
In this documentation, the word *component* always refers to new Owl
components, and *widget* refers to old Odoo widgets.
## Code structure
.. note::
All new development should be done in Owl, if possible!
Code structure
==============
The ``web/static/src`` folder contains all the ``web/`` javascript (and css and
The `web/static/src` folder contains all the `web/` javascript (and css and
templates) codebase. Here is a list of the most important folders:
- ``core/`` most of the low level features
- ``fields/`` all field components
- ``views/`` all javascript views components (``form``, ``list``, ...)
- ``search/`` control panel, search bar, search panel, ...
- ``webclient/`` the web client specific code: navbar, user menu, action service, ...
- `core/` most of the low level features
- `fields/` all field components
- `views/` all javascript views components (`form`, `list`, ...)
- `search/` control panel, search bar, search panel, ...
- `webclient/` the web client specific code: navbar, user menu, action service, ...
The ``web/static/src`` is the root folder. Everything inside can simply be
imported by using the ``@web`` prefix. For example, here is how one can import
the ``memoize`` function located in ``web/static/src/core/utils/functions``:
The `web/static/src` is the root folder. Everything inside can simply be
imported by using the `@web` prefix. For example, here is how one can import
the `memoize` function located in `web/static/src/core/utils/functions`:
.. code-block:: javascript
```javascript
import { memoize } from "@web/core/utils/functions";
```
import { memoize } from "@web/core/utils/functions";
WebClient Architecture
======================
## WebClient Architecture
As mentioned above, the web client is an owl application. Here is a slightly
simplified version of its template:
.. code-block:: xml
<t t-name="web.WebClient">
<body class="o_web_client">
<NavBar/>
<ActionContainer/>
<MainComponentsContainer/>
</body>
</t>
```xml
<t t-name="web.WebClient">
<body class="o_web_client">
<NavBar/>
<ActionContainer/>
<MainComponentsContainer/>
</body>
</t>
```
As we can see, it basically is a wrapper for a navbar, the current action and
some additional components. The ``ActionContainer`` is a higher order component
some additional components. The `ActionContainer` is a higher order component
that will display the current action controller (so, a client action, or a
specific view in the case of actions of type ``act_window``). Managing actions
specific view in the case of actions of type `act_window`). Managing actions
is a huge part of its work: the action service keeps in memory a stack of
all active actions (represented in the breadcrumbs), and coordinates each
change.
Another interesting thing to note is the ``MainComponentsContainer``: it is
Another interesting thing to note is the `MainComponentsContainer`: it is
simply a component that displays all components registered in the
``main_components`` registry. This is how other parts of the system can extend
`main_components` registry. This is how other parts of the system can extend
the web client.
.. _frontend/framework/environment:
(frontend-framework-environment)=
Environment
===========
## Environment
As an Owl application, the Odoo web client defines its own environment (components
can access it using ``this.env``). Here is a description of what Odoo adds to
the shared ``env`` object:
can access it using `this.env`). Here is a description of what Odoo adds to
the shared `env` object:
```{eval-rst}
.. list-table::
:widths: 25 75
:header-rows: 1
@@ -118,108 +113,103 @@ the shared ``env`` object:
- translation function
* - `isSmall`
- boolean. If true, the web client is currently in mobile mode (screen width <= 767px)
```
So, for example, to translate a string in a component (note: templates are
automatically translated, so no specific action is required in that case), one
can do this:
```javascript
const someString = this.env._t('some text');
```
.. code-block:: javascript
:::{note}
Having a reference to the environment is quite powerful, because it provides
access to all services. This is useful in many cases: for example,
user menu items are mostly defined as a string, and a function taking the `env`
as unique argument. This is enough to express all user menu needs.
:::
const someString = this.env._t('some text');
.. note::
Having a reference to the environment is quite powerful, because it provides
access to all services. This is useful in many cases: for example,
user menu items are mostly defined as a string, and a function taking the `env`
as unique argument. This is enough to express all user menu needs.
Building Blocks
===============
## Building Blocks
Most of the web client is built with a few types of abstractions: registries,
services, components and hooks.
Registries
----------
### Registries
:ref:`Registries <frontend/registries>` are basically a simple key/value mapping
{ref}`Registries <frontend/registries>` are basically a simple key/value mapping
that stores some specific kind of objects. They are an important part of the
extensibility of the UI: once some object is registered, the rest of the web
client can use it. For example, the field registry contains all field components
(or widgets) that can be used in views.
.. code-block:: javascript
```javascript
import { Component } from "@odoo/owl";
import { registry } from "./core/registry";
import { Component } from "@odoo/owl";
import { registry } from "./core/registry";
class MyFieldChar extends Component {
// some code
}
class MyFieldChar extends Component {
// some code
}
registry.category("fields").add("my_field_char", MyFieldChar);
```
registry.category("fields").add("my_field_char", MyFieldChar);
Note that we import the main registry from `@web/core/registry` then open the
sub registry `fields`.
Note that we import the main registry from ``@web/core/registry`` then open the
sub registry ``fields``.
### Services
Services
--------
:ref:`Services <frontend/services>` are long lived pieces of code that provide a
feature. They may be imported by components (with ``useService``) or by other
{ref}`Services <frontend/services>` are long lived pieces of code that provide a
feature. They may be imported by components (with `useService`) or by other
services. Also, they can declare a set of dependencies. In that sense, services
are basically a DI (dependency injection) system. For example, the ``notification``
service provides a way to display a notification, or the ``rpc`` service is the
are basically a DI (dependency injection) system. For example, the `notification`
service provides a way to display a notification, or the `rpc` service is the
proper way to perform a request to the Odoo server.
The following example registers a simple service that displays a notification
every 5 second:
.. code-block:: javascript
```javascript
import { registry } from "./core/registry";
import { registry } from "./core/registry";
const serviceRegistry = registry.category("services");
const serviceRegistry = registry.category("services");
const myService = {
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
}
};
const myService = {
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
}
};
serviceRegistry.add("myService", myService);
```
serviceRegistry.add("myService", myService);
### Components and Hooks
Components and Hooks
--------------------
:ref:`Components <frontend/components>` and :ref:`hooks <frontend/hooks>` are ideas coming from the
`Owl component system <https://github.com/odoo/owl/blob/master/doc/readme.md>`_.
{ref}`Components <frontend/components>` and {ref}`hooks <frontend/hooks>` are ideas coming from the
[Owl component system](https://github.com/odoo/owl/blob/master/doc/readme.md).
Odoo components are simply owl components that are part of the web client.
`Hooks <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md>`_ are a
[Hooks](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md) are a
way to factorize code, even if it depends on lifecycle. This is a
composable/functional way to inject a feature in a component. They can be seen
as a kind of mixin.
.. code-block:: javascript
```javascript
function useCurrentTime() {
const state = useState({ now: new Date() });
const update = () => state.now = new Date();
let timer;
onWillStart(() => timer = setInterval(update, 1000));
onWillUnmount(() => clearInterval(timer));
return state;
}
```
function useCurrentTime() {
const state = useState({ now: new Date() });
const update = () => state.now = new Date();
let timer;
onWillStart(() => timer = setInterval(update, 1000));
onWillUnmount(() => clearInterval(timer));
return state;
}
Context
=======
## Context
An important concept in the Odoo javascript is the *context*: it provides a way
for code to give more context to a function call or a rpc, so other parts of the
@@ -232,31 +222,31 @@ There are two different contexts in the Odoo web client: the *user context* and
the *action context* (so, we should be careful when using the word *context*: it
could mean a different thing depending on the situation).
.. note::
The `context` object may be useful in many cases, but one should be careful
not to overuse it! Many problems can be solved in a standard way without
modifying the context.
:::{note}
The `context` object may be useful in many cases, but one should be careful
not to overuse it! Many problems can be solved in a standard way without
modifying the context.
:::
.. _frontend/framework/user_context:
(frontend-framework-user-context)=
User Context
------------
### User Context
The *user context* is a small object containing various informations related to
the current user. It is available through the `user` service:
.. code-block:: javascript
class MyComponent extends Component {
setup() {
const user = useService("user");
console.log(user.context);
}
```javascript
class MyComponent extends Component {
setup() {
const user = useService("user");
console.log(user.context);
}
}
```
It contains the following information:
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -273,26 +263,27 @@ It contains the following information:
* - `tz`
- `string`
- the user current timezone (for example "Europe/Brussels")
```
In practice, the `orm` service automatically adds the user context to each of
its requests. This is why it is usually not necessary to import it directly in
most cases.
.. note::
The first element of the `allowed_company_ids` is the main company of the user.
:::{note}
The first element of the `allowed_company_ids` is the main company of the user.
:::
Action Context
--------------
### Action Context
The :ref:`ir.actions.act_window<reference/actions/window>` and
:ref:`ir.actions.client<reference/actions/client>` support an optional `context` field.
The {ref}`ir.actions.act_window<reference/actions/window>` and
{ref}`ir.actions.client<reference/actions/client>` support an optional `context` field.
This field is a `char` that represents an object. Whenever the corresponding
action is loaded in the web client, this context field will be evaluated as an
object and given to the component that corresponds to the action.
.. code-block:: xml
<field name="context">{'search_default_customer': 1}</field>
```xml
<field name="context">{'search_default_customer': 1}</field>
```
It can be used in many different ways. For example, the views add the
action context to every requests made to the server. Another important use is to
@@ -302,27 +293,26 @@ Sometimes, when we execute new actions manually (so, programmatically, in javasc
it is useful to be able to extend the action context. This can be done with the
`additional_context` argument.
.. code-block:: javascript
```javascript
// in setup
let actionService = useService("action");
// in setup
let actionService = useService("action");
// in some event handler
actionService.doAction("addon_name.something", {
additional_context:{
default_period_id: defaultPeriodId
}
});
// in some event handler
actionService.doAction("addon_name.something", {
additional_context:{
default_period_id: defaultPeriodId
}
});
```
In this example, the action with xml_id `addon_name.something` will be loaded,
and its context will be extended with the `default_period_id` value. This is a
very important usecase that lets developers combine actions together by providing
some information to the next action.
.. _frontend/framework/pyjs:
(frontend-framework-pyjs)=
Python Interpreter
==================
## Python Interpreter
The Odoo framework features a built-in small python interpreter. Its purpose
is to evaluate small python expressions. This is important, because views in
@@ -331,46 +321,54 @@ browser.
Example:
.. code-block:: javascript
```javascript
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateExpr } from "@web/core/py_js/py";
evaluateExpr("1 + 2*{'a': 1}.get('b', 54) + v", { v: 33 }); // returns 142
```
evaluateExpr("1 + 2*{'a': 1}.get('b', 54) + v", { v: 33 }); // returns 142
The ``py`` javascript code exports 5 functions:
The `py` javascript code exports 5 functions:
```{eval-rst}
.. js:function:: tokenize(expr)
:param string expr: the expression to tokenize
:returns: Token[] a list of token
```
```{eval-rst}
.. js:function:: parse(tokens)
:param Token[] tokens: a list of tokens
:returns: AST an abstract syntax tree structure representing the expression
```
```{eval-rst}
.. js:function:: parseExpr(expr)
:param string expr: a string representing a valid python expression
:returns: AST an abstract syntax tree structure representing the expression
```
```{eval-rst}
.. js:function:: evaluate(ast[, context])
:param AST ast: a AST structure that represents an expression
:param Object context: an object that provides an additional evaluation context
:returns: any the resulting value of the expression, with respect to the context
```
```{eval-rst}
.. js:function:: evaluateExpr(expr[, context])
:param string expr: a string representing a valid python expression
:param Object context: an object that provides an additional evaluation context
:returns: any the resulting value of the expression, with respect to the context
```
.. _frontend/framework/domains:
(frontend-framework-domains)=
Domains
=======
## Domains
Broadly speaking, domains in Odoo represent a set of records that matches some
specified conditions. In javascript, they are usually represented either as a
@@ -378,19 +376,19 @@ list of conditions (or of operators: `|`, `&` or `!` in prefix notation), or as
expressions. They don't have to be normalized (the `&` operator is implied if
necessary). For example:
.. code-block:: javascript
```javascript
// list of conditions
[]
[["a", "=", 3]]
[["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]
["&", "&", ["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]
["&", "!", ["a", "=", 1], "|", ["a", "=", 2], ["a", "=", 3]]
// list of conditions
[]
[["a", "=", 3]]
[["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]
["&", "&", ["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]
["&", "!", ["a", "=", 1], "|", ["a", "=", 2], ["a", "=", 3]]
// string expressions
"[('some_file', '>', a)]"
"[('date','>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"
"[('date', '!=', False)]"
// string expressions
"[('some_file', '>', a)]"
"[('date','>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"
"[('date', '!=', False)]"
```
String expressions are more powerful than list expressions: they can contain
python expressions and unevaluated values, that depends on some evaluation context.
@@ -399,19 +397,20 @@ However, manipulating string expressions is more difficult.
Since domains are quite important in the web client, Odoo provides a `Domain`
class:
.. code-block:: javascript
```javascript
new Domain([["a", "=", 3]]).contains({ a: 3 }) // true
new Domain([["a", "=", 3]]).contains({ a: 3 }) // true
const domain = new Domain(["&", "&", ["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]);
domain.contains({ a: 1, b: 2, c: 3 }); // true
domain.contains({ a: -1, b: 2, c: 3 }); // false
const domain = new Domain(["&", "&", ["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]);
domain.contains({ a: 1, b: 2, c: 3 }); // true
domain.contains({ a: -1, b: 2, c: 3 }); // false
// next expression returns ["|", ("a", "=", 1), ("b", "<=", 3)]
Domain.or([[["a", "=", 1]], "[('b', '<=', 3)]"]).toString();
// next expression returns ["|", ("a", "=", 1), ("b", "<=", 3)]
Domain.or([[["a", "=", 1]], "[('b', '<=', 3)]"]).toString();
```
Here is the `Domain` class description:
```{eval-rst}
.. js:class:: Domain([descr])
:param descr: a domain description
@@ -441,24 +440,25 @@ Here is the `Domain` class description:
.. code-block:: javascript
new Domain(`[('a', '>', b)]`).toList({ b:3 }); // [['a', '>', 3]]
```
The `Domain` class also provides 4 useful static methods to combine domains:
.. code-block:: javascript
```javascript
// ["&", ("a", "=", 1), ("uid", "<=", uid)]
Domain.and([[["a", "=", 1]], "[('uid', '<=', uid)]"]).toString();
// ["&", ("a", "=", 1), ("uid", "<=", uid)]
Domain.and([[["a", "=", 1]], "[('uid', '<=', uid)]"]).toString();
// ["|", ("a", "=", 1), ("uid", "<=", uid)]
Domain.or([[["a", "=", 1]], "[('uid', '<=', uid)]"]).toString();
// ["|", ("a", "=", 1), ("uid", "<=", uid)]
Domain.or([[["a", "=", 1]], "[('uid', '<=', uid)]"]).toString();
// ["!", ("a", "=", 1)]
Domain.not([["a", "=", 1]]).toString();
// ["&", ("a", "=", 1), ("uid", "<=", uid)]
Domain.combine([[["a", "=", 1]], "[('uid', '<=', uid)]"], "AND").toString();
// ["!", ("a", "=", 1)]
Domain.not([["a", "=", 1]]).toString();
// ["&", ("a", "=", 1), ("uid", "<=", uid)]
Domain.combine([[["a", "=", 1]], "[('uid', '<=', uid)]"], "AND").toString();
```
```{eval-rst}
.. staticmethod:: Domain.and(domains)
:param domains: a list of domain representations
@@ -466,7 +466,9 @@ The `Domain` class also provides 4 useful static methods to combine domains:
:returns: Domain
Returns a domain representing the intersection of all domains.
```
```{eval-rst}
.. staticmethod:: Domain.or(domains)
:param domains: a list of domain representations
@@ -474,7 +476,9 @@ The `Domain` class also provides 4 useful static methods to combine domains:
:returns: Domain
Returns a domain representing the union of all domains.
```
```{eval-rst}
.. staticmethod:: Domain.not(domain)
:param domain: a domain representation
@@ -482,7 +486,9 @@ The `Domain` class also provides 4 useful static methods to combine domains:
:returns: Domain
Returns a domain representing the negation of the domain argument
```
```{eval-rst}
.. staticmethod:: Domain.combine(domains, operator)
:param domains: a list of domain representations
@@ -494,26 +500,26 @@ The `Domain` class also provides 4 useful static methods to combine domains:
Returns a domain representing either the intersection or the union of all the
domains, depending on the value of the operator argument.
```
.. _frontend/framework/bus:
(frontend-framework-bus)=
Bus
===
## Bus
The web client :ref:`environment <frontend/framework/environment>` object contains an event
The web client {ref}`environment <frontend/framework/environment>` object contains an event
bus, named `bus`. Its purpose is to allow various parts of the system to properly
coordinate themselves, without coupling them. The `env.bus` is an owl
`EventBus <https://github.com/odoo/owl/blob/master/doc/reference/event_bus.md>`_,
[EventBus](https://github.com/odoo/owl/blob/master/doc/reference/event_bus.md),
that should be used for global events of interest.
.. code-block:: javascript
// for example, in some service code:
env.bus.on("WEB_CLIENT_READY", null, doSomething);
```javascript
// for example, in some service code:
env.bus.on("WEB_CLIENT_READY", null, doSomething);
```
Here is a list of the events that can be triggered on this bus:
```{eval-rst}
.. list-table::
:header-rows: 1
@@ -551,21 +557,21 @@ Here is a list of the events that can be triggered on this bus:
- list of functions
- all views with uncommitted changes should clear them, and push a callback in the list
```
Browser Object
==============
## Browser Object
The javascript framework also provides a special object ``browser`` that
provides access to many browser APIs, like ``location``, ``localStorage``
or ``setTimeout``. For example, here is how one could use the
``browser.setTimeout`` function:
The javascript framework also provides a special object `browser` that
provides access to many browser APIs, like `location`, `localStorage`
or `setTimeout`. For example, here is how one could use the
`browser.setTimeout` function:
.. code-block:: javascript
```javascript
import { browser } from "@web/core/browser/browser";
import { browser } from "@web/core/browser/browser";
// somewhere in code
browser.setTimeout(someFunction, 1000);
// somewhere in code
browser.setTimeout(someFunction, 1000);
```
It is mostly interesting for testing purposes: all code using the browser object
can be tested easily by mocking the relevant functions for the duration of the
@@ -573,6 +579,7 @@ test.
It contains the following content:
```{eval-rst}
.. list-table::
* - `addEventListener`
@@ -596,11 +603,11 @@ It contains the following content:
* - `XMLHttpRequest`
-
-
```
.. _frontend/framework/debug_mode:
(frontend-framework-debug-mode)=
Debug mode
==========
## Debug mode
Odoo can sometimes operate in a special mode called the `debug` mode. It is used
for two main purposes:
@@ -609,44 +616,44 @@ for two main purposes:
- provide some additional tools to help developer debug the Odoo interface.
The `debug` mode is described by a string. An empty string means that the `debug`
mode is not active. Otherwise, it is active. If the string contains `assets` or
mode is not active. Otherwise, it is active. If the string contains `assets` or
`tests`, then the corresponding specific sub modes are activated (see below). Both
modes can be active at the same time, for example with the string `assets,tests`.
The `debug` mode current value can be read in the :ref:`environment<frontend/framework/environment>`:
The `debug` mode current value can be read in the {ref}`environment<frontend/framework/environment>`:
`env.debug`.
.. tip::
:::{tip}
To show menus, fields or view elements only in debug mode, you should target
the group `base.group_no_one`:
To show menus, fields or view elements only in debug mode, you should target
the group `base.group_no_one`:
```xml
<field name="fname" groups="base.group_no_one"/>
```
:::
.. code-block:: xml
:::{seealso}
- {ref}`Activate the debug mode <developer-mode>`
:::
<field name="fname" groups="base.group_no_one"/>
(frontend-framework-assets-debug-mode)=
.. seealso::
- :ref:`Activate the debug mode <developer-mode>`
.. _frontend/framework/assets_debug_mode:
Assets mode
-----------
### Assets mode
The `debug=assets` sub mode is useful to debug javascript code: once activated,
the :ref:`assets<reference/assets>` bundles are no longer minified, and source-maps
the {ref}`assets<reference/assets>` bundles are no longer minified, and source-maps
are generated as well. This makes it useful to debug all kind of javascript code.
.. _frontend/framework/tests_debug_mode:
(frontend-framework-tests-debug-mode)=
Tests mode
----------
### Tests mode
There is another sub mode named `tests`: if enabled, the server injects the
bundle `web.assets_tests` in the page. This bundle contains mostly test tours
(tours whose purpose is to test a feature, not to show something interesting to
users). The `tests` mode is then useful to be able to run these tours.
.. seealso::
- `Owl Repository <https://github.com/odoo/owl>`_
:::{seealso}
- [Owl Repository](https://github.com/odoo/owl)
:::
@@ -1,19 +1,18 @@
.. _frontend/hooks:
(frontend-hooks)=
=====
Hooks
=====
# Hooks
`Owl hooks <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md>`_ are a
[Owl hooks](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md) are a
way to factorize code, even if it depends on some component lifecycle. Most hooks
provided by Owl are related to the lifecycle of a component, but some of them (such as
`useComponent <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#usecomponent>`_)
[useComponent](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#usecomponent))
provide a way to build specific hooks.
Using these hooks, it is possible to build many customized hooks that help solve
a specific problem, or make some common tasks easier. The rest of this page
documents the list of hooks provided by the Odoo web framework.
```{eval-rst}
.. list-table::
:widths: 30 70
:header-rows: 1
@@ -32,194 +31,185 @@ documents the list of hooks provided by the Odoo web framework.
- position an element relative to a target
* - :ref:`useSpellCheck <frontend/hooks/useSpellCheck>`
- activate spellcheck on focus for input or textarea
```
.. _frontend/hooks/useassets:
(frontend-hooks-useassets)=
useAssets
=========
## useAssets
Location
--------
### Location
`@web/core/assets`
Description
-----------
### Description
See the section on :ref:`lazy loading assets <frontend/assets/lazy_loading>` for
See the section on {ref}`lazy loading assets <frontend/assets/lazy_loading>` for
more details.
(frontend-hooks-useautofocus)=
.. _frontend/hooks/useAutofocus:
## useAutofocus
useAutofocus
============
Location
--------
### Location
`@web/core/utils/hooks`
Description
-----------
### Description
Focus an element referenced by a t-ref="autofocus" in the current component as
soon as it appears in the DOM and if it was not displayed before.
.. code-block:: javascript
```javascript
import { useAutofocus } from "@web/core/utils/hooks";
import { useAutofocus } from "@web/core/utils/hooks";
class Comp {
setup() {
this.inputRef = useAutofocus();
}
static template = "Comp";
}
```
class Comp {
setup() {
this.inputRef = useAutofocus();
}
static template = "Comp";
}
```xml
<t t-name="Comp">
<input t-ref="autofocus" type="text"/>
</t>
```
.. code-block:: xml
<t t-name="Comp">
<input t-ref="autofocus" type="text"/>
</t>
API
---
### API
```{eval-rst}
.. js:function:: useAutofocus()
:returns: the element reference.
```
.. _frontend/hooks/usebus:
(frontend-hooks-usebus)=
useBus
======
## useBus
Location
--------
### Location
`@web/core/utils/hooks`
Description
-----------
### Description
Add and clear an event listener to a bus. This hook ensures that
the listener is properly cleared when the component is unmounted.
.. code-block:: javascript
```javascript
import { useBus } from "@web/core/utils/hooks";
import { useBus } from "@web/core/utils/hooks";
class MyComponent {
setup() {
useBus(this.env.bus, "some-event", event => {
console.log(event);
});
}
}
```
class MyComponent {
setup() {
useBus(this.env.bus, "some-event", event => {
console.log(event);
});
}
}
API
---
### API
```{eval-rst}
.. js:function:: useBus(bus, eventName, callback)
:param EventBus bus: the target event bus
:param string eventName: the name of the event that we want to listen to
:param function callback: listener callback
```
.. _frontend/hooks/usepager:
(frontend-hooks-usepager)=
usePager
========
## usePager
Location
--------
### Location
`@web/search/pager_hook`
Description
-----------
### Description
Display the :ref:`Pager <frontend/pager>` of the control panel of a view. This hooks correctly sets `env.config` to provide the props to the pager.
Display the {ref}`Pager <frontend/pager>` of the control panel of a view. This hooks correctly sets `env.config` to provide the props to the pager.
.. code-block:: javascript
```javascript
import { usePager } from "@web/search/pager_hook";
import { usePager } from "@web/search/pager_hook";
class CustomView {
setup() {
const state = owl.hooks.useState({
offset: 0,
limit: 80,
total: 50,
});
usePager(() => {
return {
offset: this.state.offset,
limit: this.state.limit,
total: this.state.total,
onUpdate: (newState) => {
Object.assign(this.state, newState);
},
};
});
}
}
```
class CustomView {
setup() {
const state = owl.hooks.useState({
offset: 0,
limit: 80,
total: 50,
});
usePager(() => {
return {
offset: this.state.offset,
limit: this.state.limit,
total: this.state.total,
onUpdate: (newState) => {
Object.assign(this.state, newState);
},
};
});
}
}
API
---
### API
```{eval-rst}
.. js:function:: usePager(getPagerProps)
:param function getPagerProps: function that returns the pager props.
```
.. _frontend/hooks/useposition:
(frontend-hooks-useposition)=
usePosition
===========
## usePosition
Location
--------
### Location
`@web/core/position_hook`
Description
-----------
### Description
Helps positioning an HTMLElement (the `popper`) relatively to another
HTMLElement (the `reference`). This hook ensures the positioning is updated when
the window is resized/scrolled.
.. code-block:: javascript
```javascript
import { usePosition } from "@web/core/position_hook";
import { Component, xml } from "@odoo/owl";
import { usePosition } from "@web/core/position_hook";
import { Component, xml } from "@odoo/owl";
class MyPopover extends Component {
static template = xml`
<div t-ref="popper">
I am positioned through a wonderful hook!
</div>
`;
class MyPopover extends Component {
static template = xml`
<div t-ref="popper">
I am positioned through a wonderful hook!
</div>
`;
setup() {
// Here, the reference is the target props, which is an HTMLElement
usePosition(this.props.target);
}
}
```
setup() {
// Here, the reference is the target props, which is an HTMLElement
usePosition(this.props.target);
}
}
:::{important}
You should indicate your `popper` element using a [t-ref directive](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useref).
:::
.. important::
You should indicate your `popper` element using a `t-ref directive <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useref>`_.
API
---
### API
```{eval-rst}
.. js:function:: usePosition(reference[, options])
:param reference: the target HTMLElement to be positioned from
:type reference: HTMLElement or ()=>HTMLElement
:param Options options: the positioning options (see table below)
```
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -257,7 +247,9 @@ API
Can be used i.e. for dynamic styling regarding the current position.
The `PositioningSolution` is an object having the following type:
`{ direction: Direction, variant: Variant, top: number, left: number }`.
```
```{eval-rst}
.. example::
.. code-block:: javascript
@@ -290,19 +282,17 @@ API
);
}
}
```
.. _frontend/hooks/useSpellCheck:
(frontend-hooks-usespellcheck)=
useSpellCheck
=============
## useSpellCheck
Location
--------
### Location
`@web/core/utils/hooks`
Description
-----------
### Description
Activate the spellcheck state to an input or textarea on focus by a `t-ref="spellcheck"` in
the current component. This state is then removed on blur, as well as the red outline, which
@@ -312,6 +302,7 @@ The hook can also be used on any HTML element with the `contenteditable` attribu
spellcheck completely on elements that might be enabled by the hook, set explicitly the
`spellcheck` attribute as `false` on the element.
```{eval-rst}
.. example::
In the following example, the spellcheck will be enabled on the first input, the textarea and
@@ -340,14 +331,17 @@ spellcheck completely on elements that might be enabled by the hook, set explici
<div contenteditable="true"/>
</div>
</t>
```
API
---
### API
```{eval-rst}
.. js:function:: useSpellCheck([options])
:param Options options: the spellcheck options (see table below)
```
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -359,3 +353,5 @@ API
- string
- this is a `useRef reference <{OWL_PATH}/doc/reference/hooks.md#useref>`_ for the element that will be
spellcheck enabled.
```
@@ -1,16 +1,14 @@
.. _frontend/js_modules:
(frontend-js-modules)=
==================
Javascript Modules
==================
# Javascript Modules
Odoo supports three different kinds of javascript files:
- :ref:`plain javascript files <frontend/modules/plain_js>` (no module system),
- :ref:`native javascript module <frontend/modules/native_js>`.
- :ref:`Odoo modules <frontend/modules/odoo_module>` (using a custom module system),
- {ref}`plain javascript files <frontend/modules/plain_js>` (no module system),
- {ref}`native javascript module <frontend/modules/native_js>`.
- {ref}`Odoo modules <frontend/modules/odoo_module>` (using a custom module system),
As described in the :ref:`assets management page <reference/assets>`,
As described in the {ref}`assets management page <reference/assets>`,
all javascript files are bundled together and served to the browser.
Note that native javascript files are processed by the Odoo server and transformed into Odoo custom modules.
@@ -20,21 +18,20 @@ specific low level purposes. All new javascript files should be created in the
native javascript module system. The custom module system is only useful for old,
not yet converted files.
.. _frontend/modules/plain_js:
(frontend-modules-plain-js)=
Plain Javascript files
======================
## Plain Javascript files
Plain javascript files can contain arbitrary content. It is advised to use the
*iife* :dfn:`immediately invoked function execution` style when writing such a file:
*iife* {dfn}`immediately invoked function execution` style when writing such a file:
.. code-block:: javascript
(function () {
// some code here
let a = 1;
console.log(a);
})();
```javascript
(function () {
// some code here
let a = 1;
console.log(a);
})();
```
The advantages of such files is that we avoid leaking local variables to the
global scope.
@@ -43,57 +40,57 @@ Clearly, plain javascript files do not offer the benefits of a module system, so
one needs to be careful about the order in the bundle (since the browser will
execute them precisely in that order).
.. note::
In Odoo, all external libraries are loaded as plain javascript files.
:::{note}
In Odoo, all external libraries are loaded as plain javascript files.
:::
.. _frontend/modules/native_js:
(frontend-modules-native-js)=
Native Javascript Modules
=========================
## Native Javascript Modules
Odoo javascript code uses the native javascript module system. This is simpler, and
brings the benefits of a better developer experience with a better integration with the IDE.
Let us consider the following module, located in :file:`web/static/src/file_a.js`:
Let us consider the following module, located in {file}`web/static/src/file_a.js`:
.. code-block:: javascript
```javascript
import { someFunction } from "./file_b";
import { someFunction } from "./file_b";
export function otherFunction(val) {
return someFunction(val + 3);
}
export function otherFunction(val) {
return someFunction(val + 3);
}
```
There is a very important point to know: by default Odoo transpiles files under
`/static/src` and `/static/tests` into :ref:`Odoo modules <frontend/modules/odoo_module>`.
`/static/src` and `/static/tests` into {ref}`Odoo modules <frontend/modules/odoo_module>`.
This file will then be transpiled into an Odoo module that looks like this:
.. code-block:: javascript
```javascript
odoo.define('@web/file_a', ['@web/file_b'], function (require) {
'use strict';
let __exports = {};
odoo.define('@web/file_a', ['@web/file_b'], function (require) {
'use strict';
let __exports = {};
const { someFunction } = require("@web/file_b");
const { someFunction } = require("@web/file_b");
__exports.otherFunction = function otherFunction(val) {
return someFunction(val + 3);
};
__exports.otherFunction = function otherFunction(val) {
return someFunction(val + 3);
};
return __exports;
)};
return __exports;
)};
```
So, as you can see, the transformation is basically adding `odoo.define` on top
and updating the import/export statements. This is an opt-out system, it's possible
to tell the transpiler to ignore the file.
.. code-block:: javascript
/** @odoo-module ignore **/
(function () {
const sum = (a, b) => a + b;
console.log(sum(1, 2));
)();
```javascript
/** @odoo-module ignore **/
(function () {
const sum = (a, b) => a + b;
console.log(sum(1, 2));
)();
```
Note the comment in the first line: it describes that this file should be ignored.
@@ -101,51 +98,50 @@ In other folders, files aren't transpiled by default, it is opt-in. Odoo will lo
first line of a JS file and check if it contains a comment with *@odoo-module* and without
the tag *ignore*. If so, it will automatically be converted to an Odoo module.
.. code-block:: javascript
/** @odoo-module **/
export function sum(a, b) {
return a + b;
}
```javascript
/** @odoo-module **/
export function sum(a, b) {
return a + b;
}
```
Another important point is that the transpiled module has an official name:
*@web/file_a*. This is the actual name of the module. Every relative imports
will be converted as well. Every file located in an Odoo addon
:file:`some_addon/static/src/path/to/file.js` will be assigned a name prefixed by the
{file}`some_addon/static/src/path/to/file.js` will be assigned a name prefixed by the
addon name like this: *@some_addon/path/to/file*.
Relative imports work, but only if the modules are in the same Odoo addon. So, imagine that we have
the following file structure:
::
```
addons/
web/
static/
src/
file_a.js
file_b.js
stock/
static/
src/
file_c.js
```
addons/
web/
static/
src/
file_a.js
file_b.js
stock/
static/
src/
file_c.js
The file {file}`file_b` can import {file}`file_a` like this:
The file :file:`file_b` can import :file:`file_a` like this:
```javascript
import {something} from `./file_a`;
```
.. code-block:: javascript
But {file}`file_c` need to use the full name:
import {something} from `./file_a`;
```javascript
import {something} from `@web/file_a`;
```
But :file:`file_c` need to use the full name:
### Aliased modules
.. code-block:: javascript
import {something} from `@web/file_a`;
Aliased modules
---------------
Because :ref:`Odoo modules <frontend/modules/odoo_module>` follow a different module naming pattern, a system exists to allow a smooth
Because {ref}`Odoo modules <frontend/modules/odoo_module>` follow a different module naming pattern, a system exists to allow a smooth
transition towards the new system. Currently, if a file is converted to a module (and therefore
follow the new naming convention), other files not yet converted to ES6-like syntax in the project
won't be able to require the module. Aliases are here to map old names with new ones by creating a
@@ -153,175 +149,172 @@ small proxy function. The module can then be called by its new *and* old name.
To add such alias, the comment tag on top of the file should look like this:
.. code-block:: javascript
```javascript
/** @odoo-module alias=web.someName**/
import { someFunction } from './file_b';
/** @odoo-module alias=web.someName**/
import { someFunction } from './file_b';
export default function otherFunction(val) {
return someFunction(val + 3);
}
export default function otherFunction(val) {
return someFunction(val + 3);
}
```
Then, the translated module will also create an alias with the requested name:
.. code-block:: javascript
```javascript
odoo.define(`web.someName`, ['@web/file_a'], function(require) {
return require('@web/file_a')[Symbol.for("default")];
});
```
odoo.define(`web.someName`, ['@web/file_a'], function(require) {
return require('@web/file_a')[Symbol.for("default")];
});
The default behaviour of aliases is to re-export the ``default`` value of the
The default behaviour of aliases is to re-export the `default` value of the
module they alias. This is because "classic" modules generally export a single
value which would be used directly, roughly matching the semantics of default
exports.
However it is also possible to delegate more directly, and follow the exact
behaviour of the aliased module:
.. code-block:: javascript
```javascript
/** @odoo-module alias=web.someName default=0**/
import { someFunction } from './file_b';
/** @odoo-module alias=web.someName default=0**/
import { someFunction } from './file_b';
export function otherFunction(val) {
return someFunction(val + 3);
}
export function otherFunction(val) {
return someFunction(val + 3);
}
```
In that case, this will define an alias with exactly the values exported by the
original module:
.. code-block:: javascript
```javascript
odoo.define(`web.someName`, ["@web/file_a"], function(require) {
return require('@web/file_a');
});
```
odoo.define(`web.someName`, ["@web/file_a"], function(require) {
return require('@web/file_a');
});
:::{note}
Only one alias can be defined using this method. If you were to need another one to have, for
example, three names to call the same module, you would have to add a proxy manually.
This is not good practice and should be avoided unless there is no other options.
:::
.. note::
Only one alias can be defined using this method. If you were to need another one to have, for
example, three names to call the same module, you would have to add a proxy manually.
This is not good practice and should be avoided unless there is no other options.
Limitations
-----------
### Limitations
For performance reasons, Odoo does not use a full javascript
parser to transform native modules. There are, therefore, a number of limitations including but not
limited to:
- an `import` or `export` keyword cannot be preceded by a non-space character,
- a multiline comment or string cannot have a line starting by `import` or `export`
.. code-block:: javascript
```javascript
// supported
import X from "xxx";
export X;
export default X;
import X from "xxx";
// supported
import X from "xxx";
export X;
export default X;
import X from "xxx";
/*
* import X ...
*/
/*
* import X ...
*/
/*
* export X
*/
/*
* export X
*/
// not supported
// not supported
var a= 1;import X from "xxx";
/*
import X ...
*/
var a= 1;import X from "xxx";
/*
import X ...
*/
```
- when you export an object, it can't contain a comment
.. code-block:: javascript
```javascript
// supported
export {
a as b,
c,
d,
}
// supported
export {
a as b,
c,
d,
}
export {
a
} from "./file_a"
export {
a
} from "./file_a"
// not supported
export {
a as b, // this is a comment
c,
d,
}
// not supported
export {
a as b, // this is a comment
c,
d,
}
export {
a /* this is a comment */
} from "./file_a"
export {
a /* this is a comment */
} from "./file_a"
```
- Odoo needs a way to determine if a module is described by a path (like :file:`./views/form_view`)
- Odoo needs a way to determine if a module is described by a path (like {file}`./views/form_view`)
or a name (like `web.FormView`). It has to use a heuristic to do just that: if there is a `/` in
the name, it is considered a path. This means that Odoo does not really support module names with
the name, it is considered a path. This means that Odoo does not really support module names with
a `/` anymore.
As "classic" modules are not deprecated and there is currently no plan to remove them, you can and should keep using
them if you encounter issues with, or are constrained by the limitations of, native modules. Both styles can coexist
within the same Odoo addon.
(frontend-modules-odoo-module)=
.. _frontend/modules/odoo_module:
Odoo Module System
==================
## Odoo Module System
Odoo has defined a small module system (located in the file
:file:`addons/web/static/src/js/boot.js`, which needs to be loaded first). The Odoo
{file}`addons/web/static/src/js/boot.js`, which needs to be loaded first). The Odoo
module system, inspired by AMD, works by defining the function `define`
on the global odoo object. We then define each javascript module by calling that
function. In the Odoo framework, a module is a piece of code that will be executed
as soon as possible. It has a name and potentially some dependencies. When its
dependencies are loaded, a module will then be loaded as well. The value of the
function. In the Odoo framework, a module is a piece of code that will be executed
as soon as possible. It has a name and potentially some dependencies. When its
dependencies are loaded, a module will then be loaded as well. The value of the
module is then the return value of the function defining the module.
As an example, it may look like this:
.. code-block:: javascript
```javascript
// in file a.js
odoo.define('module.A', [], function (require) {
"use strict";
// in file a.js
odoo.define('module.A', [], function (require) {
"use strict";
var A = ...;
var A = ...;
return A;
});
return A;
});
// in file b.js
odoo.define('module.B', ['module.A'], function (require) {
"use strict";
// in file b.js
odoo.define('module.B', ['module.A'], function (require) {
"use strict";
var A = require('module.A');
var A = require('module.A');
var B = ...; // something that involves A
return B;
});
var B = ...; // something that involves A
return B;
});
```
If some dependencies are missing/non ready, then the module will simply not be
loaded. There will be a warning in the console after a few seconds.
loaded. There will be a warning in the console after a few seconds.
Note that circular dependencies are not supported. It makes sense, but it means that one
needs to be careful.
Defining a module
-----------------
### Defining a module
The `odoo.define` method is given three arguments:
- `moduleName`: the name of the javascript module. It should be a unique string.
- `moduleName`: the name of the javascript module. It should be a unique string.
The convention is to have the name of the odoo addon followed by a specific
description. For example, `web.Widget` describes a module defined in the `web`
addon, which exports a `Widget` class (because the first letter is capitalized)
@@ -330,35 +323,36 @@ The `odoo.define` method is given three arguments:
console.
- `dependencies`: It should be a list of strings, each corresponding to a
javascript module. This describes the dependencies that are required to
javascript module. This describes the dependencies that are required to
be loaded before the module is executed.
- finally, the last argument is a function which defines the module. Its return
value is the value of the module, which may be passed to other modules requiring
it.
.. code-block:: javascript
```javascript
odoo.define('module.Something', ['web.ajax'], function (require) {
"use strict";
odoo.define('module.Something', ['web.ajax'], function (require) {
"use strict";
var ajax = require('web.ajax');
var ajax = require('web.ajax');
// some code here
return something;
});
// some code here
return something;
});
```
If an error happens, it will be logged (in debug mode) in the console:
* `Missing dependencies`:
- `Missing dependencies`:
These modules do not appear in the page. It is possible that the JavaScript
file is not in the page or that the module name is wrong
* `Failed modules`:
- `Failed modules`:
A javascript error is detected
* `Rejected modules`:
- `Rejected modules`:
The module returns a rejected Promise. It (and its dependent modules) is not
loaded.
* `Rejected linked modules`:
- `Rejected linked modules`:
Modules who depend on a rejected module
* `Non loaded modules`:
- `Non loaded modules`:
Modules who depend on a missing or a failed module
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,8 @@
.. _reference/mobile:
(reference-mobile)=
=================
Mobile JavaScript
=================
# Mobile JavaScript
Introduction
============
## Introduction
In Odoo 10.0 we released a mobile app which allows you to access all **Odoo apps**
(even your customized modules).
@@ -18,15 +15,16 @@ Vibration, Notification and Toast through Odoo Web (via JavaScript). For this, y
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
:::{warning}
These features work with **Odoo Enterprise 10.0+** only
:::
How does it work?
=================
## How does it work?
Internal workings of the mobile application:
.. image:: mobile/mobile_working.jpg
```{image} mobile/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
@@ -44,87 +42,94 @@ 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?
==============
## 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:: mobile/odoo_mobile_api.png
```{image} mobile/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
-------
### Methods
.. note:: Each of the methods returns a JQuery Deferred object which returns
a data JSON dictionary
:::{note}
Each of the methods returns a JQuery Deferred object which returns
a data JSON dictionary
:::
Show Toast in device
~~~~~~~~~~~~~~~~~~~~
#### Show Toast in device
```{eval-rst}
.. 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
```javascript
mobile.methods.showToast({'message': 'Message sent'});
```
mobile.methods.showToast({'message': 'Message sent'});
```{image} mobile/toast.png
```
.. image:: mobile/toast.png
Vibrating device
~~~~~~~~~~~~~~~~
#### Vibrating device
```{eval-rst}
.. 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
```javascript
mobile.methods.vibrate({'duration': 100});
```
mobile.methods.vibrate({'duration': 100});
Show snackbar with action
~~~~~~~~~~~~~~~~~~~~~~~~~
#### Show snackbar with action
```{eval-rst}
.. 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
```javascript
mobile.methods.showSnackBar({'message': 'Message is deleted', 'btn_text': 'Undo'}).then(function(result){
if(result){
// Do undo operation
}else{
// Snack Bar dismissed
}
});
```
mobile.methods.showSnackBar({'message': 'Message is deleted', 'btn_text': 'Undo'}).then(function(result){
if(result){
// Do undo operation
}else{
// Snack Bar dismissed
}
});
```{image} mobile/snackbar.png
```
.. image:: mobile/snackbar.png
Showing notification
~~~~~~~~~~~~~~~~~~~~
#### Showing notification
```{eval-rst}
.. 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
@@ -133,77 +138,83 @@ 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
```javascript
mobile.showNotification({'title': 'Simple Notification', 'message': 'This is a test for a simple notification'})
```
mobile.showNotification({'title': 'Simple Notification', 'message': 'This is a test for a simple notification'})
```{image} mobile/mobile_notification.png
```
.. image:: mobile/mobile_notification.png
Create contact in device
~~~~~~~~~~~~~~~~~~~~~~~~
#### Create contact in device
```{eval-rst}
.. 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
```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>>'
}
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);
```
mobile.methods.addContact(contact);
```{image} mobile/mobile_contact_create.png
```
.. image:: mobile/mobile_contact_create.png
Scanning barcodes
~~~~~~~~~~~~~~~~~
#### Scanning barcodes
```{eval-rst}
.. 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
- 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
```javascript
mobile.methods.scanBarcode().then(function(code){
if(code){
// Perform operation with the scanned code
}
});
```
mobile.methods.scanBarcode().then(function(code){
if(code){
// Perform operation with the scanned code
}
});
Switching account in device
~~~~~~~~~~~~~~~~~~~~~~~~~~~
#### Switching account in device
```{eval-rst}
.. js:function:: switchAccount
```
Use switchAccount to switch from one account to another on the device.
.. code-block:: javascript
```javascript
mobile.methods.switchAccount();
```
mobile.methods.switchAccount();
```{image} mobile/mobile_switch_account.png
```
.. image:: mobile/mobile_switch_account.png
@@ -1,26 +1,22 @@
===========
Odoo Editor
===========
# Odoo Editor
Odoo Editor is Odoo's own rich text editor. Its sources can be found in the
`odoo-editor directory
<{GITHUB_PATH}/addons/web_editor/static/src/js/editor/odoo-editor>`_.
[odoo-editor directory]({GITHUB_PATH}/addons/web_editor/static/src/js/editor/odoo-editor).
Powerbox
========
## Powerbox
The Powerbox is a piece of user interface that contains
:ref:`commands <reference/frontend/odoo_editor/powerbox/command>` organized
into :ref:`categories <reference/frontend/odoo_editor/powerbox/category>`. It
{ref}`commands <reference/frontend/odoo_editor/powerbox/command>` organized
into {ref}`categories <reference/frontend/odoo_editor/powerbox/category>`. It
appears when typing `/` in the editor. The commands can be filtered when the
user inputs text, and navigated with the arrow keys.
.. image:: odoo_editor/powerbox.png
:align: center
:alt: The Powerbox opened after typing "/".
```{image} odoo_editor/powerbox.png
:align: center
:alt: The Powerbox opened after typing "/".
```
Modifying the Powerbox
----------------------
### Modifying the Powerbox
Only one Powerbox should be instantiated at the time, and that job is done by
the editor itself. Its Powerbox instance can be found in its `powerbox` instance
@@ -28,10 +24,12 @@ variable.
To change the Powerbox's contents and options, change the options passed to the
editor before it gets instantiated.
.. important::
Never instantiate the Powerbox yourself. Always use the current editor's own
instance instead.
:::{important}
Never instantiate the Powerbox yourself. Always use the current editor's own
instance instead.
:::
```{eval-rst}
.. example::
Say we want to add a new command `Document` to the Powerbox, just for the
`mass_mailing` module. We want to add it to a new category called
@@ -75,19 +73,22 @@ editor before it gets instantiated.
To avoid out-of-control escalations, don't use random numbers for your
priorities: look at which other priorities already exist and choose your
value accordingly (like you would do for a `z-index`).
```
Opening a custom Powerbox
-------------------------
### Opening a custom Powerbox
It is possible to open the Powerbox with a custom set of categories and
commands, bypassing all pre-existing ones. To do that, call the `open` method of
the Powerbox and pass it your custom commands and categories.
.. image:: odoo_editor/powerbox-custom.png
:align: center
:alt: The Powerbox opened with custom categories and commands when pasting an
image URL.
```{image} odoo_editor/powerbox-custom.png
:align: center
:alt: |-
: The Powerbox opened with custom categories and commands when pasting an
: image URL.
```
```{eval-rst}
.. example::
We need the current instance of the Powerbox, which can be found in the
current editor. In the `Wysiwyg class
@@ -112,33 +113,33 @@ the Powerbox and pass it your custom commands and categories.
priority: 300,
}]
);
```
Filtering commands
------------------
### Filtering commands
There are three ways to filter commands:
#. Via the `powerboxFilters`
:ref:`Powerbox option <reference/frontend/odoo_editor/powerbox/options>`.
#. Via a given
:ref:`command <reference/frontend/odoo_editor/powerbox/command>`'s
1. Via the `powerboxFilters`
{ref}`Powerbox option <reference/frontend/odoo_editor/powerbox/options>`.
2. Via a given
{ref}`command <reference/frontend/odoo_editor/powerbox/command>`'s
`isDisabled` entry.
#. The user can filter commands by simply typing text after opening the
3. The user can filter commands by simply typing text after opening the
Powerbox. It will fuzzy-match that text with the names of the categories and
commands.
.. image:: odoo_editor/powerbox-filtered.png
:align: center
:alt: The Powerbox with its commands filtered using the word "head".
```{image} odoo_editor/powerbox-filtered.png
:align: center
:alt: The Powerbox with its commands filtered using the word "head".
```
Reference
---------
### Reference
.. _reference/frontend/odoo_editor/powerbox/category:
(reference-frontend-odoo-editor-powerbox-category)=
Category
~~~~~~~~
#### Category
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -154,17 +155,19 @@ Category
- used to order the category: a category with a higher priority is
displayed higher into the Powerbox (categories with the same priority
are ordered alphabetically)
```
.. note::
If several categories exist with the same name, they will be grouped into
one. Its priority will be that defined in the version of the category that
was declared last.
:::{note}
If several categories exist with the same name, they will be grouped into
one. Its priority will be that defined in the version of the category that
was declared last.
:::
.. _reference/frontend/odoo_editor/powerbox/command:
(reference-frontend-odoo-editor-powerbox-command)=
Command
~~~~~~~
#### Command
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -196,19 +199,21 @@ Command
- `function` (`() => void`)
- a function used to disable the command under certain conditions (when it
returns `true`, the command will be disabled)
```
.. note::
If the command points to a category that doesn't exist yet, that category
will be created and appended at the end of the Powerbox.
:::{note}
If the command points to a category that doesn't exist yet, that category
will be created and appended at the end of the Powerbox.
:::
.. _reference/frontend/odoo_editor/powerbox/options:
(reference-frontend-odoo-editor-powerbox-options)=
Options
~~~~~~~
#### Options
The following options can be passed to OdooEditor, that will then be passed to
the instance of the Powerbox:
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -229,3 +234,5 @@ the instance of the Powerbox:
- `function` (`() => DOMRect`)
- a function that returns the `DOMRect` of an ancestor of the editor (can
be useful when the editor is in an iframe)
```
@@ -1,52 +1,50 @@
.. _frontend/components:
(frontend-components)=
==============
Owl components
==============
# Owl components
The Odoo Javascript framework uses a custom component framework called Owl. It
is a declarative component system, loosely inspired by Vue and React. Components
are defined using :doc:`QWeb templates <qweb>`, enriched with some Owl
are defined using {doc}`QWeb templates <qweb>`, enriched with some Owl
specific directives. The official
`Owl documentation <https://github.com/odoo/owl/blob/master/doc/readme.md>`_
[Owl documentation](https://github.com/odoo/owl/blob/master/doc/readme.md)
contains a complete reference and a tutorial.
.. important::
:::{important}
Although the code can be found in the `web` module, it is maintained from a
separate GitHub repository. Any modification to Owl should therefore be made
through a pull request on <https://github.com/odoo/owl>.
:::
Although the code can be found in the `web` module, it is maintained from a
separate GitHub repository. Any modification to Owl should therefore be made
through a pull request on https://github.com/odoo/owl.
:::{note}
Currently, all Odoo versions (starting in version 14) share the same Owl version.
:::
.. note::
Currently, all Odoo versions (starting in version 14) share the same Owl version.
## Using Owl components
Using Owl components
====================
The `Owl documentation`_ already documents in detail the Owl framework, so this
The [Owl documentation] already documents in detail the Owl framework, so this
page will only provide Odoo specific information. But first, let us see how we
can make a simple component in Odoo.
.. code-block:: javascript
```javascript
import { Component, xml, useState } from "@odoo/owl";
import { Component, xml, useState } from "@odoo/owl";
class MyComponent extends Component {
static template = xml`
<div t-on-click="increment">
<t t-esc="state.value">
</div>
`;
class MyComponent extends Component {
static template = xml`
<div t-on-click="increment">
<t t-esc="state.value">
</div>
`;
setup() {
this.state = useState({ value: 1 });
}
increment() {
this.state.value++;
}
setup() {
this.state = useState({ value: 1 });
}
increment() {
this.state.value++;
}
}
```
This example shows that Owl is available as a library in the global namespace as
`owl`: it can simply be used like most libraries in Odoo. Note that we
defined here the template as a static property, but without using the `static`
@@ -66,43 +64,42 @@ loading the javascript/css files, and loading the templates into Owl.
Here is how the component above should be defined:
.. code-block:: javascript
```javascript
import { Component, useState } from "@odoo/owl";
import { Component, useState } from "@odoo/owl";
class MyComponent extends Component {
static template = 'myaddon.MyComponent';
class MyComponent extends Component {
static template = 'myaddon.MyComponent';
...
}
...
}
```
And the template is now located in the corresponding xml file:
.. code-block:: xml
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="myaddon.MyComponent">
<div t-on-click="increment">
<t t-esc="state.value"/>
</div>
</t>
<t t-name="myaddon.MyComponent">
<div t-on-click="increment">
<t t-esc="state.value"/>
</div>
</t>
</templates>
```
</templates>
:::{note}
Template names should follow the convention `addon_name.ComponentName`.
:::
.. note::
:::{seealso}
- [Owl Repository](https://github.com/odoo/owl)
:::
Template names should follow the convention `addon_name.ComponentName`.
(frontend-owl-best-practices)=
.. seealso::
- `Owl Repository <https://github.com/odoo/owl>`_
.. _frontend/owl/best_practices:
Best practices
==============
## Best practices
First of all, components are classes, so they have a constructor. But constructors
are special methods in javascript that are not overridable in any way. Since this
@@ -110,33 +107,33 @@ is an occasionally useful pattern in Odoo, we need to make sure that no componen
in Odoo directly uses the constructor method. Instead, components should use the
`setup` method:
.. code-block:: javascript
// correct:
class MyComponent extends Component {
setup() {
// initialize component here
}
```javascript
// correct:
class MyComponent extends Component {
setup() {
// initialize component here
}
}
// incorrect. Do not do that!
class IncorrectComponent extends Component {
constructor(parent, props) {
// initialize component here
}
// incorrect. Do not do that!
class IncorrectComponent extends Component {
constructor(parent, props) {
// initialize component here
}
}
```
Another good practice is to use a consistent convention for template names:
`addon_name.ComponentName`. This prevents name collision between odoo addons.
Reference List
==============
## Reference List
The Odoo web client is built with `Owl <https://github.com/odoo/owl>`_ components.
The Odoo web client is built with [Owl](https://github.com/odoo/owl) components.
To make it easier, the Odoo javascript framework provides a suite of generic
components that can be reused in some common situations, such as dropdowns,
checkboxes or datepickers. This page explains how to use these generic components.
```{eval-rst}
.. list-table::
:widths: 30 70
:header-rows: 1
@@ -159,30 +156,28 @@ checkboxes or datepickers. This page explains how to use these generic component
- a dropdown component to choose between different options
* - :ref:`TagsList <frontend/tags_list>`
- a list of tags displayed in rounded pills
```
.. _frontend/owl/actionswiper:
(frontend-owl-actionswiper)=
ActionSwiper
------------
### ActionSwiper
Location
~~~~~~~~
#### Location
`@web/core/action_swiper/action_swiper`
Description
~~~~~~~~~~~
#### Description
This is a component that can perform actions when an element is swiped
horizontally. The swiper is wrapping a target element to add actions to it.
The action is executed once the user has released the swiper passed
a portion of its width.
.. code-block:: xml
<ActionSwiper onLeftSwipe="Object" onRightSwipe="Object">
<SomeElement/>
</ActionSwiper>
```xml
<ActionSwiper onLeftSwipe="Object" onRightSwipe="Object">
<SomeElement/>
</ActionSwiper>
```
The simplest way to use the component is to use it around your target element directly
in an xml template as shown above. But sometimes, you may want to extend an existing element
@@ -194,40 +189,43 @@ element might be swipable, its animation and the minimum portion to swipe to per
You can use the component to interact easily with records, messages, items in lists and much more.
.. image:: owl_components/actionswiper.png
:width: 400 px
:alt: Example of ActionSwiper usage
:align: center
```{image} owl_components/actionswiper.png
:align: center
:alt: Example of ActionSwiper usage
:width: 400 px
```
The following example creates a basic ActionSwiper component.
Here, the swipe is enabled in both directions.
.. code-block:: xml
```xml
<ActionSwiper
onRightSwipe="
{
action: '() => Delete item',
icon: 'fa-delete',
bgColor: 'bg-danger',
}"
onLeftSwipe="
{
action: '() => Star item',
icon: 'fa-star',
bgColor: 'bg-warning',
}"
>
<div>
Swipable item
</div>
</ActionSwiper>
```
<ActionSwiper
onRightSwipe="
{
action: '() => Delete item',
icon: 'fa-delete',
bgColor: 'bg-danger',
}"
onLeftSwipe="
{
action: '() => Star item',
icon: 'fa-star',
bgColor: 'bg-warning',
}"
>
<div>
Swipable item
</div>
</ActionSwiper>
:::{note}
Actions are permuted when using right-to-left (RTL) languages.
:::
.. note:: Actions are permuted when using right-to-left (RTL) languages.
Props
~~~~~
#### Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -250,72 +248,68 @@ Props
* - `swipeDistanceRatio`
- `Number`
- optional minimum width ratio that must be swiped to perform the action
```
You can use both `onLeftSwipe` and `onRightSwipe` props at the same time.
The `Object`'s used for the left/right swipe must contain:
- `action`, which is the callable `Function` serving as a callback.
Once the swipe has been completed in the given direction, that action
is performed.
- `icon` is the icon class to use, usually to represent the action.
It must be a `string`.
- `bgColor` is the background color, given to decorate the action.
can be one of the following `bootstrap contextual color
<https://getbootstrap.com/docs/3.3/components/#available-variations>`_ (`danger`,
`info`, `secondary`, `success` or `warning`).
> - `action`, which is the callable `Function` serving as a callback.
> Once the swipe has been completed in the given direction, that action
> is performed.
> - `icon` is the icon class to use, usually to represent the action.
> It must be a `string`.
> - `bgColor` is the background color, given to decorate the action.
> can be one of the following [bootstrap contextual color](https://getbootstrap.com/docs/3.3/components/#available-variations) (`danger`,
> `info`, `secondary`, `success` or `warning`).
>
> Those values must be given to define the behavior and the visual aspect
> of the swiper.
Those values must be given to define the behavior and the visual aspect
of the swiper.
Example: Extending existing components
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#### Example: Extending existing components
In the following example, you can use `xpath`'s to wrap an existing element
in the ActionSwiper component. Here, a swiper has been added to mark
a message as read in mail.
.. code-block:: xml
```xml
<xpath expr="//*[hasclass('o_Message')]" position="after">
<ActionSwiper
onRightSwipe="messaging.device.isMobile and messageView.message.isNeedaction ?
{
action: () => messageView.message.markAsRead(),
icon: 'fa-check-circle',
bgColor: 'bg-success',
} : undefined"
/>
</xpath>
<xpath expr="//ActionSwiper" position="inside">
<xpath expr="//*[hasclass('o_Message')]" position="move"/>
</xpath>
```
<xpath expr="//*[hasclass('o_Message')]" position="after">
<ActionSwiper
onRightSwipe="messaging.device.isMobile and messageView.message.isNeedaction ?
{
action: () => messageView.message.markAsRead(),
icon: 'fa-check-circle',
bgColor: 'bg-success',
} : undefined"
/>
</xpath>
<xpath expr="//ActionSwiper" position="inside">
<xpath expr="//*[hasclass('o_Message')]" position="move"/>
</xpath>
(frontend-owl-checkbox)=
.. _frontend/owl/checkbox:
### CheckBox
CheckBox
--------
Location
~~~~~~~~
#### Location
`@web/core/checkbox/checkbox`
Description
~~~~~~~~~~~
#### Description
This is a simple checkbox component with a label next to it. The checkbox is
linked to the label: the checkbox is toggled whenever the label is clicked.
.. code-block:: xml
```xml
<CheckBox value="boolean" disabled="boolean" t-on-change="onValueChange">
Some Text
</CheckBox>
```
<CheckBox value="boolean" disabled="boolean" t-on-change="onValueChange">
Some Text
</CheckBox>
Props
~~~~~
#### Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -329,28 +323,26 @@ Props
* - `disabled`
- `boolean`
- if true, the checkbox is disabled, otherwise it is enabled
```
.. _frontend/owl/colorlist:
(frontend-owl-colorlist)=
ColorList
---------
### ColorList
Location
~~~~~~~~
#### Location
`@web/core/colorlist/colorlist`
Description
~~~~~~~~~~~
#### Description
The ColorList let you choose a color from a predefined list. By default, the component displays the current
selected color, and is not expandable until the `canToggle` props is present. Different props can change its
behavior, to always expand the list, or make it act as a toggler once it is clicked, to display the list of
available colors until a choice is selected.
Props
~~~~~
#### Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -376,9 +368,11 @@ Props
* - `selectedColor`
- `number`
- optional. The color `id` that is selected
```
Color `id`'s are the following:
```{eval-rst}
.. list-table::
:header-rows: 1
@@ -408,19 +402,17 @@ Color `id`'s are the following:
- `Green`
* - `11`
- `Purple`
```
.. _frontend/owl/dropdown:
(frontend-owl-dropdown)=
Dropdown
--------
### Dropdown
Location
~~~~~~~~
#### Location
`@web/core/dropdown/dropdown` and `@web/core/dropdown/dropdown_item`
Description
~~~~~~~~~~~
#### Description
The Dropdown lets you show a menu with a list of items when a toggle is
clicked on. They can be combined with DropdownItems to invoke callbacks
@@ -442,7 +434,7 @@ provide is as follow:
- Direct siblings dropdowns: when one is open, toggle others on hover
To properly use a `<Dropdown/>` component, you need to populate two
`OWL slots <https://github.com/odoo/owl/blob/master/doc/reference/slots.md>`_ :
[OWL slots](https://github.com/odoo/owl/blob/master/doc/reference/slots.md) :
- `default` slot: it contains the *toggle* elements of your dropdown. By default, click events will
be attached to this element to open and close the dropdown.
@@ -450,24 +442,24 @@ To properly use a `<Dropdown/>` component, you need to populate two
Although it is not mandatory, you can put some `DropdownItem` inside this slot, the dropdown will
automatically close when these items are selected.
.. code-block:: xml
```xml
<Dropdown>
<!-- The content of the "default" slot is the component's toggle -->
<button class="my-btn" type="button">
Click me to toggle the dropdown menu!
</button>
<Dropdown>
<!-- The content of the "default" slot is the component's toggle -->
<button class="my-btn" type="button">
Click me to toggle the dropdown menu!
</button>
<!-- The "content" slot is rendered inside the menu that pops up next to the toggle -->
<t t-set-slot="content">
<DropdownItem onSelected="selectItem1">Menu Item 1</DropdownItem>
<DropdownItem onSelected="selectItem2">Menu Item 2</DropdownItem>
</t>
</Dropdown>
```
<!-- The "content" slot is rendered inside the menu that pops up next to the toggle -->
<t t-set-slot="content">
<DropdownItem onSelected="selectItem1">Menu Item 1</DropdownItem>
<DropdownItem onSelected="selectItem2">Menu Item 2</DropdownItem>
</t>
</Dropdown>
Dropdown Props
~~~~~~~~~~~~~~
#### Dropdown Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -511,10 +503,11 @@ Dropdown Props
* - `menuRef`
- `Function`
- Optional, allows to get a ref of the dropdown's menu, (expects a function returned from `useChildRef`)
```
DropdownItem Props
~~~~~~~~~~~~~~~~~~
#### DropdownItem Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -535,21 +528,22 @@ DropdownItem Props
* - `attrs`
- `Object`
- Optional object representing attributes that are added to the root element. `<DropdownItem attrs="{ title: 'A tooltip', 'data-hotkey': 'shift+a' }">`. (If `href` is set, the element will automatically become an `a` element).
```
.. important::
When writing custom css for you components, do not forget that the menu elements are not next to the toggle
but inside the overlay container, at the bottom of the document. Thus, use the `menuClass` and `class` props to more
easily write your selectors. (This DOM magic let us avoid lots of z-index issues.)
:::{important}
When writing custom css for you components, do not forget that the menu elements are not next to the toggle
but inside the overlay container, at the bottom of the document. Thus, use the `menuClass` and `class` props to more
easily write your selectors. (This DOM magic let us avoid lots of z-index issues.)
:::
Nested Dropdown
~~~~~~~~~~~~~~~
#### Nested Dropdown
Dropdown can be nested, to do this simply put new Dropdown components inside other dropdown's content slot. When the parent
dropdown is open, child dropdowns will open automatically on hover.
By default, selecting a DropdownItem will close the whole Dropdown tree.
```{eval-rst}
.. example::
This example shows how one could make a nested File dropdown menu, with submenus for the New sub elements.
@@ -603,9 +597,9 @@ By default, selecting a DropdownItem will close the whole Dropdown tree.
</t>
</Dropdown>
</t>
```
Controlled Dropdown
~~~~~~~~~~~~~~~~~~~
#### Controlled Dropdown
If needed, you can also open or close the dropdown using code. To do this you must use the `useDropdownState` hook along
with the `state` prop. `useDropdownState` returns an object that has an `open` and a `close` method (as well as an `isOpen` getter).
@@ -614,6 +608,7 @@ close your dropdown.
You can also set `manual` to `true` if you don't want the default click handlers to be added on the toggle.
```{eval-rst}
.. example::
The following example shows a dropdown that opens automatically when mounted and only has a 50% chance
@@ -653,9 +648,9 @@ You can also set `manual` to `true` if you don't want the default click handlers
}
}
}
```
DropdownGroup
~~~~~~~~~~~~~
#### DropdownGroup
**Location:** `@web/core/dropdown/dropdown_group`
@@ -666,6 +661,7 @@ the need for a click.
To do this, either surround all the Dropdowns with a single DropdownGroup or surround them with
DropdownGroups with the same `group` key.
```{eval-rst}
.. example::
In the example bellow, all dropdown in the snippet bellow will share the same group:
@@ -693,19 +689,17 @@ DropdownGroups with the same `group` key.
<DropdownGroup group="'my-group'">
<Dropdown>...</Dropdown>
</DropdownGroup>
```
.. _frontend/owl/notebook:
(frontend-owl-notebook)=
Notebook
--------
### Notebook
Location
~~~~~~~~
#### Location
`@web/core/notebook/notebook`
Description
~~~~~~~~~~~
#### Description
The Notebook is made to display multiple pages in a tabbed interface. Tabs can be located
at the top of the element to display horizontally, or at the left for a vertical layout.
@@ -717,9 +711,9 @@ A page can be disabled with the `isDisabled` attribute, set directly on the slot
in the page declaration, if the Notebook is used with the `pages` given as props. Once disabled,
the corresponding tab is greyed out and set as inactive as well.
Props
~~~~~
#### Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -748,7 +742,9 @@ Props
* - `pages`
- `array`
- optional. Contain the list of `page`'s to populate from a template.
```
```{eval-rst}
.. example::
The first approach is to set the pages in the slots of the component.
@@ -815,32 +811,31 @@ Props
:alt: Examples with vertical and horizontal layout
:align: center
```
.. _frontend/pager:
(frontend-pager)=
Pager
-----
### Pager
Location
~~~~~~~~
#### Location
`@web/core/pager/pager`
Description
~~~~~~~~~~~
#### Description
The Pager is a small component to handle pagination. A page is defined by an `offset` and a `limit` (the size of the page). It displays the current page and the `total` number of elements, for instance, "9-12 / 20". In the previous example, `offset` is 8, `limit` is 4 and `total` is 20. It has two buttons ("Previous" and "Next") to navigate between pages.
.. note::
The pager can be used anywhere but its main use is in the control panel. See the :ref:`usePager <frontend/hooks/usepager>` hook in order to manipulate the pager of the control panel.
:::{note}
The pager can be used anywhere but its main use is in the control panel. See the {ref}`usePager <frontend/hooks/usepager>` hook in order to manipulate the pager of the control panel.
:::
.. code-block:: xml
```xml
<Pager offset="0" limit="80" total="50" onUpdate="doSomething" />
```
<Pager offset="0" limit="80" total="50" onUpdate="doSomething" />
Props
~~~~~
#### Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -866,30 +861,29 @@ Props
* - `withAccessKey`
- `boolean`
- Binds access key `p` on the previous page button and `n` on the next page one (`true` by default).
```
.. _frontend/select_menu:
(frontend-select-menu)=
SelectMenu
----------
### SelectMenu
Location
~~~~~~~~
#### Location
`@web/core/select_menu/select_menu`
Description
~~~~~~~~~~~
#### Description
This component can be used when you want to do more than using the native `select` element. You can define your own option template, allowing to search
between your options, or group them in subsections.
.. note::
Prefer the native HTML `select` element, as it provides by default accessibility features, and has a better user interface on mobile devices.
This component is designed to be used for more complex use cases, to overcome limitations of the native element.
:::{note}
Prefer the native HTML `select` element, as it provides by default accessibility features, and has a better user interface on mobile devices.
This component is designed to be used for more complex use cases, to overcome limitations of the native element.
:::
Props
~~~~~
#### Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -927,17 +921,19 @@ Props
* - `onSelect`
- `function`
- optional. Callback executed when an option is chosen.
```
The shape of a `choice` is the following:
- `value` is actual value of the choice. It is usually a technical string, but can be from `any` type.
- `label` is the displayed text associated with the option. This one is usually a more friendly and translated `string`.
> - `value` is actual value of the choice. It is usually a technical string, but can be from `any` type.
> - `label` is the displayed text associated with the option. This one is usually a more friendly and translated `string`.
The shape of a `group` is the following:
- `choices` is the list of `choice`'s to display for this group.
- `label` is the displayed text associated with the group. This is a `string` displayed at the top of the group.
> - `choices` is the list of `choice`'s to display for this group.
> - `label` is the displayed text associated with the group. This is a `string` displayed at the top of the group.
```{eval-rst}
.. example::
In the following example, the SelectMenu will display four choices. One of them is displayed on top of the options,
@@ -1042,27 +1038,25 @@ The shape of a `group` is the following:
:width: 400 px
:alt: Example of SelectMenu's bottom area customization
:align: center
```
.. _frontend/tags_list:
(frontend-tags-list)=
TagsList
--------
### TagsList
Location
~~~~~~~~
#### Location
`@web/core/tags_list/tags_list`
Description
~~~~~~~~~~~
#### Description
This component can display a list of tags in rounded pills. Those tags can either simply list a few values, or can be editable, allowing the removal of items.
It can be possible to limit the number of displayed items using the `itemsVisible` props. If the list is longer than this limit, the number of additional items is
shown in a circle next to the last tag.
Props
~~~~~
#### Props
```{eval-rst}
.. list-table::
:widths: 20 20 60
:header-rows: 1
@@ -1082,17 +1076,19 @@ Props
* - `tags`
- `array`
- list of `tag`'s elements given to the component.
```
The shape of a `tag` is the following:
- `colorIndex` is an optional color id.
- `icon` is an optional icon displayed just before the displayed text.
- `id` is a unique identifier for the tag.
- `img` is an optional image displayed in a circle, just before the displayed text.
- `onClick` is an optional callback that can be given to the element. This allows the parent element to handle any functionality depending on the tag clicked.
- `onDelete` is an optional callback that can be given to the element. This makes the removal of the item from the list of tags possible, and must be handled by the parent element.
- `text` is the displayed `string` associated with the tag.
> - `colorIndex` is an optional color id.
> - `icon` is an optional icon displayed just before the displayed text.
> - `id` is a unique identifier for the tag.
> - `img` is an optional image displayed in a circle, just before the displayed text.
> - `onClick` is an optional callback that can be given to the element. This allows the parent element to handle any functionality depending on the tag clicked.
> - `onDelete` is an optional callback that can be given to the element. This makes the removal of the item from the list of tags possible, and must be handled by the parent element.
> - `text` is the displayed `string` associated with the tag.
```{eval-rst}
.. example::
In the next example, a TagsList component is used to display multiple tags.
@@ -1132,3 +1128,5 @@ The shape of a `tag` is the following:
:width: 350 px
:alt: Examples of TagsList using different props and attributes
:align: center
```
@@ -0,0 +1,253 @@
# Patching code
Sometimes, we need to customize the way the UI works. Many common needs are
covered by some supported API. For example, all registries are good extension
points: the field registry allows adding/removing specialized field components,
or the main component registry allows adding components that should be displayed
all the time.
However, there are situations for which it is not sufficient. In those cases, we
may need to modify an object or a class in place. To achieve that, Odoo
provides the utility function `patch`. It is mostly useful to override/update
the behavior of some other component/piece of code that one does not control.
## Description
The patch function is located in `@web/core/utils/patch`:
```{eval-rst}
.. js:function:: patch(objToPatch, extension)
:param object objToPatch: the object that should be patched
:param object extension: an object mapping each key to an extension
:returns: a function to remove the patch
The `patch` function modifies in place the `objToPatch` object (or class) and
applies all key/value described in the `extension` object. An unpatch
function is returned, so it can be used to remove the patch later if necessary.
Most patch operations provide access to the parent value by using the
native `super` keyword (see below in the examples).
```
## Patching a simple object
Here is a simple example of how an object can be patched:
```javascript
import { patch } from "@web/core/utils/patch";
const object = {
field: "a field",
fn() {
// do something
},
};
patch(object, {
fn() {
// do things
},
});
```
When patching functions, we usually want to be able to access the `parent`
function. To do so, we can simply use the native `super` keyword:
```javascript
patch(object, {
fn() {
super.fn(...arguments);
// do other things
},
});
```
:::{warning}
`super` can only be used in a method not a function. This means that the
following constructs are invalid for javascript.
```javascript
const obj = {
a: function () {
// Throws: "Uncaught SyntaxError: 'super' keyword unexpected here"
super.a();
},
b: () => {
// Throws: "Uncaught SyntaxError: 'super' keyword unexpected here"
super.b();
},
};
```
:::
Getters and setters are supported too:
```javascript
patch(object, {
get number() {
return super.number / 2;
},
set number(value) {
super.number = value;
},
});
```
(frontend-patching-class)=
## Patching a javascript class
The `patch` function is designed to work with anything: object or ES6 class.
However, since javascript classes work with the prototypal inheritance, when
one wishes to patch a standard method from a class, then we actually need to patch
the `prototype`:
```javascript
class MyClass {
static myStaticFn() {...}
myPrototypeFn() {...}
}
// this will patch static properties!!!
patch(MyClass, {
myStaticFn() {...},
});
// this is probably the usual case: patching a class method
patch(MyClass.prototype, {
myPrototypeFn() {...},
});
```
Also, Javascript handles the constructor in a special native way which makes it
impossible to be patched. The only workaround is to call a method in the original
constructor and patch that method instead:
```javascript
class MyClass {
constructor() {
this.setup();
}
setup() {
this.number = 1;
}
}
patch(MyClass.prototype, {
setup() {
super.setup(...arguments);
this.doubleNumber = this.number * 2;
},
});
```
:::{warning}
It is impossible to patch directly the `constructor` of a class!
:::
## Patching a component
Components are defined by javascript classes, so all the information above still
holds. For these reasons, Owl components should use the `setup` method, so they
can easily be patched as well (see the section on {ref}`best practices<frontend/owl/best_practices>`).
```javascript
patch(MyComponent.prototype, {
setup() {
useMyHook();
},
});
```
## Removing a patch
The `patch` function returns its counterpart. This is mostly useful for
testing purposes, when we patch something at the beginning of a test, and
unpatch it at the end.
```javascript
const unpatch = patch(object, { ... });
// test stuff here
unpatch();
```
## Applying the same patch to multiple objects
It could happen that one wants to apply the same patch to multiple objects but
because of the way the `super` keyword works, the `extension` can only be used
for patching once and cannot be copied/cloned ([check the documentation of the keyword](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super#description)).
A function returning the object used to patch can be used to make it unique.
```javascript
const obj1 = {
method() {
doSomething();
},
};
const obj2 = {
method() {
doThings();
},
};
function createExtensionObj() {
return {
method() {
super.method();
doCommonThings();
},
};
}
patch(obj1, createExtensionObj());
patch(obj2, createExtensionObj());
```
:::{warning}
If an `extension` is based on another then the two extensions should
be applied separately. Do not copy/clone an extension.
```javascript
const object = {
method1() {
doSomething();
},
method2() {
doAnotherThing();
},
};
const ext1 = {
method1() {
super.method1();
doThings();
},
};
const invalid_ext2 = {
...ext1, // this will not work: super will not refer to the correct object in methods coming from ext1
method2() {
super.method2();
doOtherThings();
},
};
patch(object, invalid_ext2);
object.method1(); // throws: Uncaught TypeError: (intermediate value).method1 is not a function
const valid_ext2 = {
method2() {
super.method2();
doOtherThings();
},
};
patch(object, ext1); // first patch base extension
patch(object, valid_ext2); // then the new one
object.method1(); // works as expected
```
:::
@@ -1,260 +0,0 @@
=============
Patching code
=============
Sometimes, we need to customize the way the UI works. Many common needs are
covered by some supported API. For example, all registries are good extension
points: the field registry allows adding/removing specialized field components,
or the main component registry allows adding components that should be displayed
all the time.
However, there are situations for which it is not sufficient. In those cases, we
may need to modify an object or a class in place. To achieve that, Odoo
provides the utility function `patch`. It is mostly useful to override/update
the behavior of some other component/piece of code that one does not control.
Description
===========
The patch function is located in `@web/core/utils/patch`:
.. js:function:: patch(objToPatch, extension)
:param object objToPatch: the object that should be patched
:param object extension: an object mapping each key to an extension
:returns: a function to remove the patch
The `patch` function modifies in place the `objToPatch` object (or class) and
applies all key/value described in the `extension` object. An unpatch
function is returned, so it can be used to remove the patch later if necessary.
Most patch operations provide access to the parent value by using the
native `super` keyword (see below in the examples).
Patching a simple object
========================
Here is a simple example of how an object can be patched:
.. code-block:: javascript
import { patch } from "@web/core/utils/patch";
const object = {
field: "a field",
fn() {
// do something
},
};
patch(object, {
fn() {
// do things
},
});
When patching functions, we usually want to be able to access the ``parent``
function. To do so, we can simply use the native ``super`` keyword:
.. code-block:: javascript
patch(object, {
fn() {
super.fn(...arguments);
// do other things
},
});
.. warning::
``super`` can only be used in a method not a function. This means that the
following constructs are invalid for javascript.
.. code-block:: javascript
const obj = {
a: function () {
// Throws: "Uncaught SyntaxError: 'super' keyword unexpected here"
super.a();
},
b: () => {
// Throws: "Uncaught SyntaxError: 'super' keyword unexpected here"
super.b();
},
};
Getters and setters are supported too:
.. code-block:: javascript
patch(object, {
get number() {
return super.number / 2;
},
set number(value) {
super.number = value;
},
});
.. _frontend/patching_class:
Patching a javascript class
===========================
The ``patch`` function is designed to work with anything: object or ES6 class.
However, since javascript classes work with the prototypal inheritance, when
one wishes to patch a standard method from a class, then we actually need to patch
the `prototype`:
.. code-block:: javascript
class MyClass {
static myStaticFn() {...}
myPrototypeFn() {...}
}
// this will patch static properties!!!
patch(MyClass, {
myStaticFn() {...},
});
// this is probably the usual case: patching a class method
patch(MyClass.prototype, {
myPrototypeFn() {...},
});
Also, Javascript handles the constructor in a special native way which makes it
impossible to be patched. The only workaround is to call a method in the original
constructor and patch that method instead:
.. code-block:: javascript
class MyClass {
constructor() {
this.setup();
}
setup() {
this.number = 1;
}
}
patch(MyClass.prototype, {
setup() {
super.setup(...arguments);
this.doubleNumber = this.number * 2;
},
});
.. warning::
It is impossible to patch directly the `constructor` of a class!
Patching a component
====================
Components are defined by javascript classes, so all the information above still
holds. For these reasons, Owl components should use the `setup` method, so they
can easily be patched as well (see the section on :ref:`best practices<frontend/owl/best_practices>`).
.. code-block:: javascript
patch(MyComponent.prototype, {
setup() {
useMyHook();
},
});
Removing a patch
================
The `patch` function returns its counterpart. This is mostly useful for
testing purposes, when we patch something at the beginning of a test, and
unpatch it at the end.
.. code-block:: javascript
const unpatch = patch(object, { ... });
// test stuff here
unpatch();
Applying the same patch to multiple objects
===========================================
It could happen that one wants to apply the same patch to multiple objects but
because of the way the `super` keyword works, the `extension` can only be used
for patching once and cannot be copied/cloned (`check the documentation of the keyword <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super#description>`_).
A function returning the object used to patch can be used to make it unique.
.. code-block:: javascript
const obj1 = {
method() {
doSomething();
},
};
const obj2 = {
method() {
doThings();
},
};
function createExtensionObj() {
return {
method() {
super.method();
doCommonThings();
},
};
}
patch(obj1, createExtensionObj());
patch(obj2, createExtensionObj());
.. warning::
If an `extension` is based on another then the two extensions should
be applied separately. Do not copy/clone an extension.
.. code-block:: javascript
const object = {
method1() {
doSomething();
},
method2() {
doAnotherThing();
},
};
const ext1 = {
method1() {
super.method1();
doThings();
},
};
const invalid_ext2 = {
...ext1, // this will not work: super will not refer to the correct object in methods coming from ext1
method2() {
super.method2();
doOtherThings();
},
};
patch(object, invalid_ext2);
object.method1(); // throws: Uncaught TypeError: (intermediate value).method1 is not a function
const valid_ext2 = {
method2() {
super.method2();
doOtherThings();
},
};
patch(object, ext1); // first patch base extension
patch(object, valid_ext2); // then the new one
object.method1(); // works as expected
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,6 @@
.. _frontend/registries:
(frontend-registries)=
==========
Registries
==========
# Registries
Registries are (ordered) key/value maps. They are the main web client extension
points: many features provided by the Odoo javascript framework simply look up
@@ -10,32 +8,32 @@ into a registry whenever it needs a definition for some object (such as fields,
views, client actions or services). Customizing the web client is then simply
done by adding specific values in the correct registry.
.. code-block:: javascript
```javascript
import { Registry } from "@web/core/registry";
import { Registry } from "@web/core/registry";
const myRegistry = new Registry();
const myRegistry = new Registry();
myRegistry.add("hello", "odoo");
myRegistry.add("hello", "odoo");
console.log(myRegistry.get("hello"));
console.log(myRegistry.get("hello"));
```
A useful feature of registries is that they maintain a set of sub registries,
obtained by the `category` method. If the sub registry does not exist yet, it
is created on the fly. All registries used by the web client are obtained
in such a way from one root registry, exported in `@web/core/registry`.
.. code-block:: javascript
```javascript
import { registry } from "@web/core/registry";
import { registry } from "@web/core/registry";
const fieldRegistry = registry.category("fields");
const serviceRegistry = registry.category("services");
const viewRegistry = registry.category("views");
```
const fieldRegistry = registry.category("fields");
const serviceRegistry = registry.category("services");
const viewRegistry = registry.category("views");
Registry API
============
## Registry API
```{eval-rst}
.. js:class:: Registry()
Creates a new registry. Note that a registry is an event bus, so one can
@@ -96,10 +94,11 @@ Registry API
Returns the sub registry associated with the `subcategory`. If it does not
exist yet, the sub registry is created on the fly.
```
Reference List
==============
## Reference List
```{eval-rst}
.. list-table::
:widths: 30 70
:header-rows: 1
@@ -120,24 +119,24 @@ Reference List
- components displayed in the systray zone in the navbar
* - :ref:`user_menuitems <frontend/registries/usermenu>`
- menu items displayed in the user menu (top right of navbar)
```
.. _frontend/registries/effects:
(frontend-registries-effects)=
Effect registry
---------------
### Effect registry
The `effects` registry contains the implementations of all available effects.
See the section on the :ref:`effect service <frontend/services/effect_registry>`
See the section on the {ref}`effect service <frontend/services/effect_registry>`
for more details.
.. _frontend/registries/formatters:
(frontend-registries-formatters)=
Formatter registry
------------------
### Formatter registry
The `formatters` registry contains functions to format values. Each formatter
has the following API:
```{eval-rst}
.. js:function:: format(value[, options])
:param value: a value of a specific type, or `false` if no value is given
@@ -146,46 +145,46 @@ has the following API:
:returns: string
Formats a value and returns a string
```
.. seealso::
- :ref:`Parsers registry <frontend/registries/parsers>`
:::{seealso}
- {ref}`Parsers registry <frontend/registries/parsers>`
:::
.. _frontend/registries/main_components:
(frontend-registries-main-components)=
Main components registry
------------------------
### Main components registry
The main component registry (`main_components`) is useful for adding top level
components in the web client. The webclient has a `MainComponentsContainer` as
components in the web client. The webclient has a `MainComponentsContainer` as
direct child. This component is basically a live representation of the ordered
list of components registered in the main components registry.
API
.. code-block:: text
interface {
Component: Owl Component class
props?: any
}
: ```text
interface {
Component: Owl Component class
props?: any
}
```
For example, the `LoadingIndicator` component can be added in the registry like
this:
.. code-block:: javascript
```javascript
registry.category("main_components").add("LoadingIndicator", {
Component: LoadingIndicator,
});
```
registry.category("main_components").add("LoadingIndicator", {
Component: LoadingIndicator,
});
(frontend-registries-parsers)=
.. _frontend/registries/parsers:
Parser registry
---------------
### Parser registry
The `parsers` registry contains functions to parse values. Each parser
has the following API:
```{eval-rst}
.. js:function:: parse(value[, options])
:noindex:
@@ -196,36 +195,36 @@ has the following API:
Parses a string and returns a value. If the string does not represent a valid
value, parsers can fail and throw errors.
```
.. seealso::
- :ref:`Formatters registry <frontend/registries/formatters>`
:::{seealso}
- {ref}`Formatters registry <frontend/registries/formatters>`
:::
.. _frontend/registries/services:
(frontend-registries-services)=
Service registry
----------------
### Service registry
The service registry (category: `services`) contains all
:ref:`services <frontend/services>` that should be activated by the Odoo
{ref}`services <frontend/services>` that should be activated by the Odoo
framework.
.. code-block:: javascript
```javascript
import { registry } from "@web/core/registry";
import { registry } from "@web/core/registry";
const myService = {
dependencies: [...],
start(env, deps) {
// some code here
}
};
const myService = {
dependencies: [...],
start(env, deps) {
// some code here
}
};
registry.category("services").add("myService", myService);
```
registry.category("services").add("myService", myService);
(frontend-registries-systray)=
.. _frontend/registries/systray:
Systray registry
----------------
### Systray registry
The systray is the zone on the right of the navbar that contains various small
components, that usually display some sort of information (like the number of
@@ -237,68 +236,67 @@ with the following three keys:
- `Component`: the component class that represents the item. Its root element
should be a `<li>` tag, otherwise it might not be styled properly.
- `props (optional)`: props that should be given to the component
- `isDisplayed (optional)`: a function that takes the :ref:`env <frontend/framework/environment>`
- `isDisplayed (optional)`: a function that takes the {ref}`env <frontend/framework/environment>`
and returns a boolean. If true, the systray item is displayed. Otherwise it is
removed.
For example:
.. code-block:: javascript
```javascript
import { registry } from "@web/core/registry";
import { registry } from "@web/core/registry";
class MySystrayItem extends Component {
// some component ...
}
registry.category("systray").add("myAddon.myItem", {
Component: MySystrayItem,
});
class MySystrayItem extends Component {
// some component ...
}
registry.category("systray").add("myAddon.myItem", {
Component: MySystrayItem,
});
```
The systray registry is an ordered registry (with the `sequence` number):
.. code-block:: javascript
const item = {
Component: MySystrayItem
};
registry.category("systray").add("myaddon.some_description", item, { sequence: 43 });
```javascript
const item = {
Component: MySystrayItem
};
registry.category("systray").add("myaddon.some_description", item, { sequence: 43 });
```
The sequence number defaults to 50. If given, this number will be used
to order the items. The lowest sequence is on the right and the highest sequence
is on the left in the systray menu.
.. _frontend/registries/usermenu:
(frontend-registries-usermenu)=
Usermenu registry
-----------------
### Usermenu registry
The user menu registry (category: `user_menuitems`) contains all menu items that
are shown when opening the user menu (the navbar element with the user name, on
the top right).
User menu items are defined by a function taking the :ref:`env <frontend/framework/environment>`
User menu items are defined by a function taking the {ref}`env <frontend/framework/environment>`
and returning a plain object, containing the following information:
* `description` : the menu item text,
* `href` : (optional) if given (and truthy), the item text is put in a `a` tag with given attribute href,
* `callback` : callback to call when the item is selected,
* `hide`: (optional) indicates if the item should be hidden (default: `false`),
* `sequence`: (optional) determines the rank of the item among the other dropwdown items (default: 100).
- `description` : the menu item text,
- `href` : (optional) if given (and truthy), the item text is put in a `a` tag with given attribute href,
- `callback` : callback to call when the item is selected,
- `hide`: (optional) indicates if the item should be hidden (default: `false`),
- `sequence`: (optional) determines the rank of the item among the other dropwdown items (default: 100).
The user menu calls all the functions defining items every time it is opened.
Example:
.. code-block:: javascript
```javascript
import { registry } from "@web/core/registry";
import { registry } from "@web/core/registry";
registry.category("user_menuitems").add("my item", (env) => {
return {
description: env._t("Technical Settings"),
callback: () => { env.services.action_manager.doAction(3); },
hide: (Math.random() < 0.5),
};
});
```
registry.category("user_menuitems").add("my item", (env) => {
return {
description: env._t("Technical Settings"),
callback: () => { env.services.action_manager.doAction(3); },
hide: (Math.random() < 0.5),
};
});
@@ -1,56 +1,55 @@
(frontend-services)=
.. _frontend/services:
========
Services
========
# Services
Services are long lived pieces of code that provide a feature. They may be
imported by components (with ``useService``) or by other services. Also, they
imported by components (with `useService`) or by other services. Also, they
can declare a set of dependencies. In that sense, services are basically a
DI :dfn:`dependency injection` system. For example, the ``notification`` service
provides a way to display a notification, or the ``rpc`` service is the proper
DI {dfn}`dependency injection` system. For example, the `notification` service
provides a way to display a notification, or the `rpc` service is the proper
way to perform a request to the Odoo server.
The following example registers a simple service that displays a notification
every 5 seconds:
.. code-block:: javascript
```javascript
import { registry } from "@web/core/registry";
import { registry } from "@web/core/registry";
const myService = {
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
}
};
const myService = {
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
}
};
registry.category("services").add("myService", myService);
registry.category("services").add("myService", myService);
```
At startup, the web client starts all services present in the `services`
registry. Note that the name used in the registry is the name of the service.
.. note::
:::{note}
Most code that is not a component should be *packaged* in a service, in
particular if it performs some side effect. This is very useful for testing
purposes: tests can choose which services are active, so there are less chance
for unwanted side effects interfering with the code being tested.
:::
Most code that is not a component should be *packaged* in a service, in
particular if it performs some side effect. This is very useful for testing
purposes: tests can choose which services are active, so there are less chance
for unwanted side effects interfering with the code being tested.
Defining a service
==================
## Defining a service
A service needs to implement the following interface:
```{eval-rst}
.. js:data:: dependencies
Optional list of strings. It is the list of all dependencies (other services)
that this service needs
```
```{eval-rst}
.. js:function:: start(env, deps)
:param Environment env: the application environment
@@ -64,7 +63,9 @@ A service needs to implement the following interface:
Some services do not export any value. They may just do their work without a
need to be directly called by other code. In that case, their value will be
set to ``null`` in ``env.services``.
```
```{eval-rst}
.. js:data:: async
Optional value. If given, it should be `true` or a list of strings.
@@ -81,35 +82,35 @@ A service needs to implement the following interface:
that all asynchronous calls coming from components should be left pending if
the component is destroyed.
```
Using a service
===============
## Using a service
A service that depends on other services and has properly declared its
``dependencies`` simply receives a reference to the corresponding services
in the second argument of the ``start`` method.
`dependencies` simply receives a reference to the corresponding services
in the second argument of the `start` method.
The ``useService`` hook is the proper way to use a service in a component. It
The `useService` hook is the proper way to use a service in a component. It
simply returns a reference to the service value, that can then be used by the
component later. For example:
.. code-block:: javascript
```javascript
import { useService } from "@web/core/utils/hooks";
import { useService } from "@web/core/utils/hooks";
class MyComponent extends Component {
setup() {
const rpc = useService("rpc");
class MyComponent extends Component {
setup() {
const rpc = useService("rpc");
onWillStart(async () => {
this.someValue = await rpc(...);
});
}
}
```
onWillStart(async () => {
this.someValue = await rpc(...);
});
}
}
Reference List
==============
## Reference List
```{eval-rst}
.. list-table::
:widths: 25 75
:header-rows: 1
@@ -134,31 +135,32 @@ Reference List
- read or modify the window title
* - :ref:`user <frontend/services/user>`
- provides some information related to the current user
```
.. _frontend/services/cookie:
(frontend-services-cookie)=
Cookie service
--------------
### Cookie service
Overview
~~~~~~~~
#### Overview
- Technical name: `cookie`
- Dependencies: none
Provides a way to manipulate cookies. For example:
.. code-block:: javascript
```javascript
cookieService.setCookie("hello", "odoo");
```
cookieService.setCookie("hello", "odoo");
API
~~~
#### API
```{eval-rst}
.. js:data:: current
Object representing each cookie and its value if any (or empty string)
```
```{eval-rst}
.. js:function:: setCookie(name[, value, ttl])
:param string name: the name of the cookie that should be set
@@ -166,78 +168,80 @@ API
:param number ttl: optional. the time in seconds before the cookie will be deleted (default=1 year)
Sets the cookie `name` to the value `value` with a max age of `ttl`
```
```{eval-rst}
.. js:function:: deleteCookie(name)
:param string name: name of the cookie
Deletes the cookie `name`.
```
.. _frontend/services/effect:
(frontend-services-effect)=
Effect service
--------------
### Effect service
Overview
~~~~~~~~
#### Overview
* Technical name: `effect`
* Dependencies: None
- Technical name: `effect`
- Dependencies: None
Effects are graphical elements that can be temporarily displayed on top of the page, usually to provide feedback to the user that something interesting happened.
A good example would be the rainbow man:
.. image:: services/rainbow_man.png
:alt: The rainbow man effect
:width: 600
:align: center
```{image} services/rainbow_man.png
:align: center
:alt: The rainbow man effect
:width: 600
```
Here's how this can be displayed:
.. code-block:: javascript
```javascript
const effectService = useService("effect");
effectService.add({
type: "rainbow_man", // can be omitted, default type is already "rainbow_man"
message: "Boom! Team record for the past 30 days.",
});
```
const effectService = useService("effect");
effectService.add({
type: "rainbow_man", // can be omitted, default type is already "rainbow_man"
message: "Boom! Team record for the past 30 days.",
});
:::{warning}
The hook `useEffect` is not related to the effect service.
:::
.. warning ::
The hook `useEffect` is not related to the effect service.
API
~~~
#### API
```{eval-rst}
.. js:function:: effectService.add(options)
:param object options: the options for the effect. They will get passed along to the underlying effect component.
Display an effect.
```
The options are defined by:
.. code-block:: ts
```ts
interface EffectOptions {
// The name of the desired effect
type?: string;
[paramName: string]: any;
}
```
interface EffectOptions {
// The name of the desired effect
type?: string;
[paramName: string]: any;
}
Available effects
~~~~~~~~~~~~~~~~~
#### Available effects
Currently, the only effect is the rainbow man.
RainbowMan
**********
##### RainbowMan
.. code-block:: javascript
effectService.add({ type: "rainbow_man" });
```javascript
effectService.add({ type: "rainbow_man" });
```
```{eval-rst}
.. list-table::
:widths: 20 40 40
:header-rows: 1
@@ -278,22 +282,23 @@ RainbowMan
`"no"` will keep rainbowman on screen until user clicks anywhere outside rainbowman.
```
How to add an effect
~~~~~~~~~~~~~~~~~~~~
#### How to add an effect
.. _frontend/services/effect_registry:
(frontend-services-effect-registry)=
The effects are stored in a registry called `effects`.
You can add new effects by providing a name and a function.
.. code-block:: javascript
const effectRegistry = registry.category("effects");
effectRegistry.add("rainbow_man", rainbowManEffectFunction);
```javascript
const effectRegistry = registry.category("effects");
effectRegistry.add("rainbow_man", rainbowManEffectFunction);
```
The function must follow this API:
```{eval-rst}
.. js:function:: <newEffectFunction>(env, params)
:param Env env: the environment received by the service
@@ -301,76 +306,74 @@ The function must follow this API:
:param object params: the params received from the add function on the service.
:returns: `({Component, props} | void)` A component and its props or nothing.
```
This function must create a component and return it. This component is mounted inside the
effect component container.
Example
~~~~~~~
#### Example
Let's say we want to add an effect that add a sepia look at the page.
.. code-block:: javascript
```javascript
import { registry } from "@web/core/registry";
import { Component, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { Component, xml } from "@odoo/owl";
class SepiaEffect extends Component {
static template = xml`
<div style="
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: rgba(124,87,0, 0.4);
"></div>
`;
}
class SepiaEffect extends Component {
static template = xml`
<div style="
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: rgba(124,87,0, 0.4);
"></div>
`;
}
export function sepiaEffectProvider(env, params = {}) {
return {
Component: SepiaEffect,
};
}
const effectRegistry = registry.category("effects");
effectRegistry.add("sepia", sepiaEffectProvider);
export function sepiaEffectProvider(env, params = {}) {
return {
Component: SepiaEffect,
};
}
const effectRegistry = registry.category("effects");
effectRegistry.add("sepia", sepiaEffectProvider);
```
And then, call it somewhere you want and you will see the result.
Here, it is called in webclient.js to make it visible everywhere for the example.
.. code-block:: javascript
```javascript
const effectService = useService("effect");
effectService.add({ type: "sepia" });
```
const effectService = useService("effect");
effectService.add({ type: "sepia" });
```{image} services/odoo_sepia.png
:align: center
:alt: Odoo in sepia
:width: 600
```
.. image:: services/odoo_sepia.png
:alt: Odoo in sepia
:width: 600
:align: center
(frontend-services-http)=
.. _frontend/services/http:
### Http Service
Http Service
------------
#### Overview
Overview
~~~~~~~~
* Technical name: `http`
* Dependencies: None
- Technical name: `http`
- Dependencies: None
While most interactions between the client and the server in odoo are `RPCs` (`XMLHTTPRequest`), lower level
control on requests may sometimes be required.
This service provides a way to send `get` and `post` `http requests <https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods>`_.
This service provides a way to send `get` and `post` [http requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods).
API
~~~
#### API
```{eval-rst}
.. js:function:: async get(route[,readMethod = "json"])
:param string route: the url to send the request to
@@ -378,7 +381,9 @@ API
:returns: the result of the request with the format defined by the readMethod argument.
Sends a get request.
```
```{eval-rst}
.. js:function:: async post(route [,params = {}, readMethod = "json"])
:param string route: the url to send the request to
@@ -387,38 +392,36 @@ API
:returns: the result of the request with the format defined by the readMethod argument.
Sends a post request.
```
Example
~~~~~~~
#### Example
.. code-block:: javascript
```javascript
const httpService = useService("http");
const data = await httpService.get("https://something.com/posts/1");
// ...
await httpService.post("https://something.com/posts/1", { title: "new title", content: "new content" });
```
const httpService = useService("http");
const data = await httpService.get("https://something.com/posts/1");
// ...
await httpService.post("https://something.com/posts/1", { title: "new title", content: "new content" });
(frontend-services-notification)=
.. _frontend/services/notification:
### Notification service
Notification service
--------------------
#### Overview
Overview
~~~~~~~~
* Technical name: `notification`
* Dependencies: None
- Technical name: `notification`
- Dependencies: None
The `notification` service allows to display notifications on the screen.
.. code-block:: javascript
```javascript
const notificationService = useService("notification");
notificationService.add("I'm a very simple notification");
```
const notificationService = useService("notification");
notificationService.add("I'm a very simple notification");
API
~~~
#### API
```{eval-rst}
.. js:function:: add(message[, options])
:param string message: the notification message to display
@@ -477,65 +480,64 @@ API
* - `primary`
- boolean
- whether the button should be styled as a primary button
```
Examples
~~~~~~~~
#### Examples
A notification for when a sale deal is made with a button to go some kind of commission page.
.. code-block:: javascript
```javascript
// in setup
this.notificationService = useService("notification");
this.actionService = useService("action");
// in setup
this.notificationService = useService("notification");
this.actionService = useService("action");
// later
this.notificationService.add("You closed a deal!", {
title: "Congrats",
type: "success",
buttons: [
{
name: "See your Commission",
onClick: () => {
this.actionService.doAction("commission_action");
},
},
],
});
```
// later
this.notificationService.add("You closed a deal!", {
title: "Congrats",
type: "success",
buttons: [
{
name: "See your Commission",
onClick: () => {
this.actionService.doAction("commission_action");
},
},
],
});
.. image:: services/notification_service.png
:width: 600 px
:alt: Example of notification
:align: center
```{image} services/notification_service.png
:align: center
:alt: Example of notification
:width: 600 px
```
A notification that closes after a second:
.. code-block:: javascript
```javascript
const notificationService = useService("notification");
const close = notificationService.add("I will be quickly closed");
setTimeout(close, 1000);
```
const notificationService = useService("notification");
const close = notificationService.add("I will be quickly closed");
setTimeout(close, 1000);
(frontend-services-router)=
.. _frontend/services/router:
### Router Service
Router Service
--------------
Overview
~~~~~~~~
#### Overview
- Technical name: `router`
- Dependencies: none
The `router` service provides three features:
* information about the current route
* a way for the application to update the url, depending on its state
* listens to every hash change, and notifies the rest of the application
- information about the current route
- a way for the application to update the url, depending on its state
- listens to every hash change, and notifies the rest of the application
API
~~~
#### API
```{eval-rst}
.. js:data:: current
:noindex:
@@ -547,19 +549,21 @@ API
from the url to its value. An empty string is the value if no value was
explicitely given
* `hash (object)`: same as above, but for values described in the hash.
```
For example:
.. code-block:: javascript
```javascript
// url = /web?debug=assets#action=123&owl&menu_id=174
const { pathname, search, hash } = env.services.router.current;
console.log(pathname); // /web
console.log(search); // { debug="assets" }
console.log(hash); // { action:123, owl: "", menu_id: 174 }
```
// url = /web?debug=assets#action=123&owl&menu_id=174
const { pathname, search, hash } = env.services.router.current;
console.log(pathname); // /web
console.log(search); // { debug="assets" }
console.log(hash); // { action:123, owl: "", menu_id: 174 }
Updating the URL is done with the `pushState` method:
Updating the URL is done with the `pushState` method:
```{eval-rst}
.. js:function:: pushState(hash: object[, replace?: boolean])
:param Object hash: object containing a mapping from some keys to some values
@@ -578,20 +582,21 @@ Updating the URL is done with the `pushState` method:
This is because this method is intended to only updates the url. The code calling
this method has the responsibility to make sure that the screen is updated as
well.
```
For example:
.. code-block:: javascript
// url = /web#action_id=123
routerService.pushState({ menu_id: 321 });
// url is now /web#action_id=123&menu_id=321
routerService.pushState({ yipyip: "" }, replace: true);
// url is now /web#yipyip
```javascript
// url = /web#action_id=123
routerService.pushState({ menu_id: 321 });
// url is now /web#action_id=123&menu_id=321
routerService.pushState({ yipyip: "" }, replace: true);
// url is now /web#yipyip
```
Finally, the `redirect` method will redirect the browser to a specified url:
```{eval-rst}
.. js:function:: redirect(url[, wait])
:param string url: a valid url
@@ -601,44 +606,44 @@ Finally, the `redirect` method will redirect the browser to a specified url:
argument is rarely used: it is useful in some cases where we know that the
server will be unavailable for a short duration, typically just after an addon
update or install operation.
```
.. note::
The router service emits a `ROUTE_CHANGE` event on the :ref:`main bus <frontend/framework/bus>`
whenever the current route has changed.
:::{note}
The router service emits a `ROUTE_CHANGE` event on the {ref}`main bus <frontend/framework/bus>`
whenever the current route has changed.
:::
.. _frontend/services/rpc:
(frontend-services-rpc)=
RPC service
-----------
### RPC service
Overview
~~~~~~~~
#### Overview
- Technical name: `rpc`
- Dependencies: none
The `rpc` service provides a single asynchronous function to send requests to
the server. Calling a controller is very simple: the route should be the first
argument and optionally, a ``params`` object can be given as a second argument.
argument and optionally, a `params` object can be given as a second argument.
.. code-block:: javascript
```javascript
// in setup
this.rpc = useService("rpc");
// in setup
this.rpc = useService("rpc");
// somewhere else, in an async function:
const result = await this.rpc("/my/route", { some: "value" });
```
// somewhere else, in an async function:
const result = await this.rpc("/my/route", { some: "value" });
:::{note}
Note that the `rpc` service is considered a low-level service. It should
only be used to interact with Odoo controllers. To work with models (which
is by far the most important usecase), one should use the `orm` service
instead.
:::
.. note::
Note that the ``rpc`` service is considered a low-level service. It should
only be used to interact with Odoo controllers. To work with models (which
is by far the most important usecase), one should use the ``orm`` service
instead.
API
~~~
#### API
```{eval-rst}
.. js:function:: rpc(route, params, settings)
:param string route: route targeted by the request
@@ -652,63 +657,56 @@ API
is useful when one accesses advanced features of the `XMLHTTPRequest` API.
- ``silent (boolean)`` If set to ``true``, the web client will not provide
a feedback that there is a pending rpc.
```
The ``rpc`` service communicates with the server by using a ``XMLHTTPRequest``
object, configured to work with the ``application/json`` content type. So clearly
The `rpc` service communicates with the server by using a `XMLHTTPRequest`
object, configured to work with the `application/json` content type. So clearly
the content of the request should be JSON serializable. Each request done by
this service uses the ``POST`` http method.
this service uses the `POST` http method.
Server errors actually return the response with an http code 200. But the ``rpc``
Server errors actually return the response with an http code 200. But the `rpc`
service will treat them as error.
Error Handling
~~~~~~~~~~~~~~
#### Error Handling
An rpc can fail for two main reasons:
* either the odoo server returns an error (so, we call this a ``server`` error).
- either the odoo server returns an error (so, we call this a `server` error).
In that case the http request will return with an http code 200 BUT with a
response object containing an ``error`` key.
* or there is some other kind of network error
response object containing an `error` key.
- or there is some other kind of network error
When a rpc fails, then:
* the promise representing the rpc is rejected, so the calling code will crash,
- the promise representing the rpc is rejected, so the calling code will crash,
unless it handles the situation
* an event ``RPC_ERROR`` is triggered on the main application bus. The event payload
- an event `RPC_ERROR` is triggered on the main application bus. The event payload
contains a description of the cause of the error:
If it is a server error (the server code threw an exception). In that case
the event payload will be an object with the following keys:
* ``type = 'server'``
* ``message(string)``
*
``code(number)``
*
``name(string)`` (optional, used by the error service to look for an appropriate
- `type = 'server'`
- `message(string)`
- `code(number)`
- `name(string)` (optional, used by the error service to look for an appropriate
dialog to use when handling the error)
* ``subType(string)`` (optional, often used to determine the dialog title)
* ``data(object)`` (optional object that can contain various keys among which
``debug`` : the main debug information, with the call stack)
- `subType(string)` (optional, often used to determine the dialog title)
- `data(object)` (optional object that can contain various keys among which
`debug` : the main debug information, with the call stack)
If it is a network error, then the error description is simply an object
``{type: 'network'}``.
When a network error occurs, a :ref:`notification <frontend/services/notification>` is
`{type: 'network'}`.
When a network error occurs, a {ref}`notification <frontend/services/notification>` is
displayed and the server is regularly contacted until it responds. The
notification is closed as soon as the server responds.
.. _frontend/services/scroller:
(frontend-services-scroller)=
Scroller service
----------------
### Scroller service
Overview
~~~~~~~~
#### Overview
- Technical name: `scroller`
- Dependencies: none
@@ -726,11 +724,11 @@ It may allow other parts to handle a behavior relative to anchors themselves. Th
given as it might need to be prevented. If the event is not prevented, then the user interface will
scroll to the target element.
API
~~~
#### API
The following values are contained in the `anchor-link-clicked` custom event explained above.
```{eval-rst}
.. list-table::
:widths: 25 25 50
:header-rows: 1
@@ -747,19 +745,19 @@ The following values are contained in the `anchor-link-clicked` custom event exp
* - `originalEv`
- `Event`
- The original click event
```
.. note::
The scroller service emits a `SCROLLER:ANCHOR_LINK_CLICKED` event on the :ref:`main bus <frontend/framework/bus>`.
To avoid the default scroll behavior of the scroller service, you must use `preventDefault()` on the event given
to the listener so that you can implement your own behavior correctly from the listener.
:::{note}
The scroller service emits a `SCROLLER:ANCHOR_LINK_CLICKED` event on the {ref}`main bus <frontend/framework/bus>`.
To avoid the default scroll behavior of the scroller service, you must use `preventDefault()` on the event given
to the listener so that you can implement your own behavior correctly from the listener.
:::
.. _frontend/services/title:
(frontend-services-title)=
Title Service
-------------
### Title Service
Overview
~~~~~~~~
#### Overview
- Technical name: `title`
- Dependencies: none
@@ -768,41 +766,44 @@ The `title` service offers a simple API that allows to read/modify the document
title. For example, if the current document title is "Odoo", we can change it
to "Odoo 15 - Apple" by using the following command:
.. code-block:: javascript
```javascript
// in some component setup method
const titleService = useService("title");
// in some component setup method
const titleService = useService("title");
titleService.setParts({ odoo: "Odoo 15", fruit: "Apple" });
```
titleService.setParts({ odoo: "Odoo 15", fruit: "Apple" });
#### API
API
~~~
The `title` service manipulates the following interface:
The ``title`` service manipulates the following interface:
.. code-block:: ts
interface Parts {
[key: string]: string | null;
}
```ts
interface Parts {
[key: string]: string | null;
}
```
Each key represents the identity of a part of the title, and each value is the
string that is displayed, or `null` if it has been removed.
Its API is:
```{eval-rst}
.. js:data:: current
:noindex:
This is a string representing the current title. It is structured in the
following way: ``value_1 - ... - value_n`` where each `value_i` is a (non null)
value found in the `Parts` object (returned by the `getParts` function)
```
```{eval-rst}
.. js:function:: getParts
:returns: Parts the current `Parts` object maintained by the title service
```
```{eval-rst}
.. js:function:: setParts(parts)
:param Parts parts: object representing the required change
@@ -827,25 +828,23 @@ Its API is:
will change the title to ``Odoo``.
```
.. _frontend/services/user:
(frontend-services-user)=
User service
------------
### User service
Overview
~~~~~~~~
#### Overview
* Technical name: `user`
* Dependencies: `rpc`
- Technical name: `user`
- Dependencies: `rpc`
The `user` service provides a bunch of data and a few helper functions concerning
the connected user.
#### API
API
~~~
```{eval-rst}
.. list-table::
:widths: 25 25 50
:header-rows: 1
@@ -887,7 +886,9 @@ API
- ``string``
- Alternative nick name of the user
```
```{eval-rst}
.. js:function:: updateContext(update)
:param object update: the object to update the context with
@@ -897,7 +898,9 @@ API
.. code-block:: javascript
userService.updateContext({ isFriend: true })
```
```{eval-rst}
.. js:function:: removeFromContext(key)
:param string key: the key of the targeted attribute
@@ -907,7 +910,9 @@ API
.. code-block:: javascript
userService.removeFromContext("isFriend")
```
```{eval-rst}
.. js:function:: hasGroup(group)
:param string group: the xml_id of the group to look for
@@ -919,3 +924,5 @@ API
.. code-block:: javascript
const isInSalesGroup = await userService.hasGroup("sale.group_sales")
```
@@ -0,0 +1,13 @@
---
nosearch: true
---
# Standard modules
```{toctree}
:titlesonly: true
standard_modules/account
standard_modules/payment
```
@@ -1,11 +0,0 @@
:nosearch:
================
Standard modules
================
.. toctree::
:titlesonly:
standard_modules/account
standard_modules/payment
@@ -0,0 +1,19 @@
---
nosearch: true
---
# Accounting
```{toctree}
:titlesonly: true
account/account_account_tag
account/account_account
account/account_fiscal_position
account/account_group
account/account_report
account/account_report_line
account/account_tax
account/account_tax_repartition
```
@@ -1,17 +0,0 @@
:nosearch:
==========
Accounting
==========
.. toctree::
:titlesonly:
account/account_account_tag
account/account_account
account/account_fiscal_position
account/account_group
account/account_report
account/account_report_line
account/account_tax
account/account_tax_repartition
@@ -1,9 +1,8 @@
.. _reference/account_account:
(reference-account-account)=
=======
Account
=======
# Account
```{eval-rst}
.. automodel:: odoo.addons.account.models.account_account.AccountAccount
:main:
@@ -15,3 +14,5 @@ Account
.. autofield:: note
.. autofield:: tax_ids
.. autofield:: tag_ids
```
@@ -1,9 +1,8 @@
.. _reference/account_account_tag:
(reference-account-account-tag)=
===========
Account Tag
===========
# Account Tag
```{eval-rst}
.. automodel:: odoo.addons.account.models.account_account_tag.AccountAccountTag
:main:
@@ -13,3 +12,5 @@ Account Tag
.. autofield:: active
.. autofield:: tax_negate
.. autofield:: country_id
```
@@ -1,9 +1,8 @@
.. _reference/account_fiscal_position:
(reference-account-fiscal-position)=
===============
Fiscal Position
===============
# Fiscal Position
```{eval-rst}
.. automodel:: odoo.addons.account.models.partner.AccountFiscalPosition
:main:
@@ -19,3 +18,5 @@ Fiscal Position
.. autofield:: state_ids
.. autofield:: zip_from
.. autofield:: zip_to
```

Some files were not shown because too many files have changed in this diff Show More