convert rst to markdown
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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>`_
|
||||
@@ -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.
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
+90
-94
@@ -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)
|
||||
+30
-30
@@ -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.
|
||||
+67
-79
@@ -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"/>
|
||||
@@ -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"><!DOCTYPE html>
|
||||
<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"><!DOCTYPE html>
|
||||
<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.
|
||||
@@ -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/
|
||||
|
||||
@@ -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/
|
||||
+96
-101
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
:::
|
||||
|
||||
@@ -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
|
||||
+26
-23
@@ -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
|
||||
+14
-15
@@ -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"> </div>
|
||||
<div class="media_iframe_video_size" contenteditable="false"> </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"> </div>
|
||||
<div class="media_iframe_video_size" contenteditable="false"> </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
|
||||
+91
-96
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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:
|
||||
+350
-303
File diff suppressed because it is too large
Load Diff
@@ -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>`_
|
||||
+95
-78
@@ -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="{"no_marker": True}"/>
|
||||
<...>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
```
|
||||
|
||||
The main template calls the translatable template with `doc.partner_id.lang` as a
|
||||
`t-lang` parameter, so it will be rendered in the language of the partner. This way,
|
||||
each Sale Order will be printed in the language of the corresponding customer. If you wish
|
||||
to translate only the body of the document, but keep the header and footer in a default
|
||||
language, you could call the report's external layout this way:
|
||||
|
||||
```
|
||||
<t t-call="web.external_layout" t-lang="en_US">
|
||||
```
|
||||
|
||||
:::{tip}
|
||||
Please take note that this works only when calling external templates, you will not be
|
||||
able to translate part of a document by setting a `t-lang` attribute on an xml node other
|
||||
than `t-call`. If you wish to translate part of a template, you can create an external
|
||||
template with this partial template and call it from the main one with the `t-lang`
|
||||
attribute.
|
||||
:::
|
||||
|
||||
### Barcodes
|
||||
|
||||
Barcodes are images returned by a controller and can easily be embedded in
|
||||
reports thanks to the QWeb syntax (e.g. see {ref}`reference/qweb/attributes`):
|
||||
|
||||
```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&value=%s&width=%s&height=%s'%('QR', 'text', 200, 200)"/>
|
||||
```
|
||||
|
||||
### Useful Remarks
|
||||
|
||||
- Twitter Bootstrap and FontAwesome classes can be used in your report
|
||||
template
|
||||
|
||||
- Local CSS can be put directly in the template
|
||||
|
||||
- Global CSS can be inserted in the main report layout by inheriting its
|
||||
template and inserting your CSS:
|
||||
|
||||
```
|
||||
<template id="report_saleorder_style" inherit_id="report.style">
|
||||
<xpath expr=".">
|
||||
<t>
|
||||
.example-css-class {
|
||||
background-color: red;
|
||||
}
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
```
|
||||
|
||||
- If it appears that your PDF report is missing the styles, please check
|
||||
{ref}`these instructions <reference/backend/reporting/printed-reports/pdf-without-styles>`.
|
||||
|
||||
(reference-reports-paper-formats)=
|
||||
|
||||
## Paper Format
|
||||
|
||||
Paper formats are records of `report.paperformat` and can contain the
|
||||
following attributes:
|
||||
|
||||
`name` (mandatory)
|
||||
|
||||
: only useful as a mnemonic/description of the report when looking for one
|
||||
in a list of some sort
|
||||
|
||||
`description`
|
||||
|
||||
: a small description of your format
|
||||
|
||||
`format`
|
||||
|
||||
: either a predefined format (A0 to A9, B0 to B10, Legal, Letter,
|
||||
Tabloid,...) or `custom`; A4 by default. You cannot use a non-custom
|
||||
format if you define the page dimensions.
|
||||
|
||||
`dpi`
|
||||
|
||||
: output DPI; 90 by default
|
||||
|
||||
`margin_top`, `margin_bottom`, `margin_left`, `margin_right`
|
||||
|
||||
: margin sizes in mm
|
||||
|
||||
`page_height`, `page_width`
|
||||
|
||||
: page dimensions in mm
|
||||
|
||||
`orientation`
|
||||
|
||||
: Landscape or Portrait
|
||||
|
||||
`header_line`
|
||||
|
||||
: boolean to display a header line
|
||||
|
||||
`header_spacing`
|
||||
|
||||
: header spacing in mm
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
<record id="paperformat_frenchcheck" model="report.paperformat">
|
||||
<field name="name">French Bank Check</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="format">custom</field>
|
||||
<field name="page_height">80</field>
|
||||
<field name="page_width">175</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">3</field>
|
||||
<field name="margin_bottom">3</field>
|
||||
<field name="margin_left">3</field>
|
||||
<field name="margin_right">3</field>
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">3</field>
|
||||
<field name="dpi">80</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
(reference-reports-custom-reports)=
|
||||
|
||||
## Custom Reports
|
||||
|
||||
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="{"no_marker": True}"/>
|
||||
<...>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
|
||||
The main template calls the translatable template with ``doc.partner_id.lang`` as a
|
||||
``t-lang`` parameter, so it will be rendered in the language of the partner. This way,
|
||||
each Sale Order will be printed in the language of the corresponding customer. If you wish
|
||||
to translate only the body of the document, but keep the header and footer in a default
|
||||
language, you could call the report's external layout this way::
|
||||
|
||||
<t t-call="web.external_layout" t-lang="en_US">
|
||||
|
||||
.. tip::
|
||||
|
||||
Please take note that this works only when calling external templates, you will not be
|
||||
able to translate part of a document by setting a ``t-lang`` attribute on an xml node other
|
||||
than ``t-call``. If you wish to translate part of a template, you can create an external
|
||||
template with this partial template and call it from the main one with the ``t-lang``
|
||||
attribute.
|
||||
|
||||
|
||||
Barcodes
|
||||
--------
|
||||
|
||||
Barcodes are images returned by a controller and can easily be embedded in
|
||||
reports thanks to the QWeb syntax (e.g. see :ref:`reference/qweb/attributes`):
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<img t-att-src="'/report/barcode/QR/%s' % 'My text in qr code'"/>
|
||||
|
||||
More parameters can be passed as a query string
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<img t-att-src="'/report/barcode/?
|
||||
barcode_type=%s&value=%s&width=%s&height=%s'%('QR', 'text', 200, 200)"/>
|
||||
|
||||
|
||||
Useful Remarks
|
||||
--------------
|
||||
|
||||
* Twitter Bootstrap and FontAwesome classes can be used in your report
|
||||
template
|
||||
* Local CSS can be put directly in the template
|
||||
* Global CSS can be inserted in the main report layout by inheriting its
|
||||
template and inserting your CSS::
|
||||
|
||||
<template id="report_saleorder_style" inherit_id="report.style">
|
||||
<xpath expr=".">
|
||||
<t>
|
||||
.example-css-class {
|
||||
background-color: red;
|
||||
}
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
* If it appears that your PDF report is missing the styles, please check
|
||||
:ref:`these instructions <reference/backend/reporting/printed-reports/pdf-without-styles>`.
|
||||
|
||||
.. _reference/reports/paper_formats:
|
||||
|
||||
Paper Format
|
||||
============
|
||||
|
||||
Paper formats are records of ``report.paperformat`` and can contain the
|
||||
following attributes:
|
||||
|
||||
``name`` (mandatory)
|
||||
only useful as a mnemonic/description of the report when looking for one
|
||||
in a list of some sort
|
||||
``description``
|
||||
a small description of your format
|
||||
``format``
|
||||
either a predefined format (A0 to A9, B0 to B10, Legal, Letter,
|
||||
Tabloid,...) or ``custom``; A4 by default. You cannot use a non-custom
|
||||
format if you define the page dimensions.
|
||||
``dpi``
|
||||
output DPI; 90 by default
|
||||
``margin_top``, ``margin_bottom``, ``margin_left``, ``margin_right``
|
||||
margin sizes in mm
|
||||
``page_height``, ``page_width``
|
||||
page dimensions in mm
|
||||
``orientation``
|
||||
Landscape or Portrait
|
||||
``header_line``
|
||||
boolean to display a header line
|
||||
``header_spacing``
|
||||
header spacing in mm
|
||||
|
||||
Example::
|
||||
|
||||
<record id="paperformat_frenchcheck" model="report.paperformat">
|
||||
<field name="name">French Bank Check</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="format">custom</field>
|
||||
<field name="page_height">80</field>
|
||||
<field name="page_width">175</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">3</field>
|
||||
<field name="margin_bottom">3</field>
|
||||
<field name="margin_left">3</field>
|
||||
<field name="margin_right">3</field>
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">3</field>
|
||||
<field name="dpi">80</field>
|
||||
</record>
|
||||
|
||||
.. _reference/reports/custom_reports:
|
||||
|
||||
Custom Reports
|
||||
==============
|
||||
|
||||
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> <foo>')
|
||||
>>> Markup('<em>Hello</em> %s') % '<foo>'
|
||||
Markup('<em>Hello</em> <foo>')
|
||||
```
|
||||
|
||||
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>').replace('>', 'x')
|
||||
Markup('<ax')
|
||||
>>> Markup('<a>').replace('>', '&')
|
||||
Markup('<a&')
|
||||
```
|
||||
|
||||
:::{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("<R&D>")
|
||||
>>> escape(record.get_name(True))
|
||||
Markup("<strong><R&D></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 <R&D></p>')
|
||||
>>> Markup("%s <br/> %s") % ("<R&D>", Markup("<p>Hello</p>"))
|
||||
Markup('<R&D> <br/> <p>Hello</p>')
|
||||
>>> escape("<R&D>")
|
||||
Markup('<R&D>')
|
||||
>>> _("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 <R&D>: <ul><li>First <R&D> 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 <R&D></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 <<a href=mailto:george@abitbol.example>george@abitbol.example</a>>')
|
||||
```
|
||||
|
||||
### 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('<R&D>')
|
||||
|
||||
# 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><R&D></p>')
|
||||
```
|
||||
|
||||
Sanitizing can break features, depending on whether the *CODE* is expected to
|
||||
contain patterns that are not safe. That's why `fields.Html` and
|
||||
`tools.html_sanitize()` have options to fine-tune the level of sanitization for
|
||||
styles, etc. Those options have to be carefully considered depending on where
|
||||
the data comes from, and the desired features. The sanitization safety is
|
||||
balanced against sanitization breakages: the safer the sanitisation the more
|
||||
likely it is to break things.
|
||||
|
||||
```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> <foo>')
|
||||
>>> Markup('<em>Hello</em> %s') % '<foo>'
|
||||
Markup('<em>Hello</em> <foo>')
|
||||
|
||||
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>').replace('>', 'x')
|
||||
Markup('<ax')
|
||||
>>> Markup('<a>').replace('>', '&')
|
||||
Markup('<a&')
|
||||
|
||||
.. 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("<R&D>")
|
||||
>>> escape(record.get_name(True))
|
||||
Markup("<strong><R&D></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 <R&D></p>')
|
||||
>>> Markup("%s <br/> %s") % ("<R&D>", Markup("<p>Hello</p>"))
|
||||
Markup('<R&D> <br/> <p>Hello</p>')
|
||||
>>> escape("<R&D>")
|
||||
Markup('<R&D>')
|
||||
>>> _("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 <R&D>: <ul><li>First <R&D> 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 <R&D></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 <<a href=mailto:george@abitbol.example>george@abitbol.example</a>>')
|
||||
|
||||
|
||||
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('<R&D>')
|
||||
|
||||
# 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><R&D></p>')
|
||||
|
||||
Sanitizing can break features, depending on whether the *CODE* is expected to
|
||||
contain patterns that are not safe. That's why `fields.Html` and
|
||||
`tools.html_sanitize()` have options to fine-tune the level of sanitization for
|
||||
styles, etc. Those options have to be carefully considered depending on where
|
||||
the data comes from, and the desired features. The sanitization safety is
|
||||
balanced against sanitization breakages: the safer the sanitisation the more
|
||||
likely it is to break things.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> code = "<p class='text-warning'>Important Information</p>"
|
||||
# this will remove the style, which may break features
|
||||
# but is necessary if the source is untrusted
|
||||
>>> html_sanitize(code, strip_classes=True)
|
||||
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
+245
-195
@@ -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
|
||||
@@ -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 user’s 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>
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
+154
-161
@@ -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.
|
||||
|
||||
+148
-171
@@ -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.
|
||||
|
||||
+245
-238
@@ -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)
|
||||
:::
|
||||
|
||||
+120
-124
@@ -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.
|
||||
```
|
||||
|
||||
+187
-193
@@ -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
+98
-87
@@ -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
|
||||
+58
-51
@@ -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)
|
||||
```
|
||||
|
||||
+253
-255
@@ -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
+100
-102
@@ -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),
|
||||
};
|
||||
});
|
||||
+318
-311
@@ -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
|
||||
+5
-4
@@ -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
|
||||
```
|
||||
|
||||
+5
-4
@@ -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
|
||||
```
|
||||
|
||||
+5
-4
@@ -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
Reference in New Issue
Block a user