[MOV] tutorials/getting_started/*: promote advanced chapters to independent tutorials
task-2991663
X-original-commit: a8c78a80ba
Part-of: odoo/documentation#3660
@@ -84,8 +84,6 @@ Static web data
|
||||
None of these elements are mandatory. Some modules may only add data files (e.g. country-specific
|
||||
accounting configuration), while others may only add business objects. During this training, we will
|
||||
create business objects, object views and data files.
|
||||
:ref:`Web controllers <tutorials/getting_started/G_website>` and
|
||||
:ref:`static web data <tutorials/getting_started/I_jswidget>` are advanced topics.
|
||||
|
||||
Module structure
|
||||
----------------
|
||||
|
||||
@@ -9,8 +9,8 @@ intended to store business data. In a business application such as Odoo, one of
|
||||
to consider is who\ [#who]_ can access the data. Odoo provides a security mechanism to allow access
|
||||
to the data for specific groups of users.
|
||||
|
||||
The topic of security is covered in more detail in :ref:`tutorials/getting_started/B_acl_irrules`.
|
||||
This chapter aims to cover the minimum required for our new module.
|
||||
The topic of security is covered in more detail in :doc:`../restrict_data_access`. This chapter aims
|
||||
to cover the minimum required for our new module.
|
||||
|
||||
Data Files (CSV)
|
||||
================
|
||||
@@ -105,9 +105,7 @@ Here is an example for our previous ``test.model``:
|
||||
- ``model_id/id`` refers to the model which the access right applies to. The standard way to refer
|
||||
to the model is ``model_<model_name>``, where ``<model_name>`` is the ``_name`` of the model
|
||||
with the ``.`` replaced by ``_``. Seems cumbersome? Indeed it is...
|
||||
- ``group_id/id`` refers to the group which the access right applies to. We will cover the concept
|
||||
of groups in the :ref:`advanced topic <tutorials/getting_started/N_security>` dedicated to the
|
||||
security.
|
||||
- ``group_id/id`` refers to the group which the access right applies to.
|
||||
- ``perm_read,perm_write,perm_create,perm_unlink``: read, write, create and unlink permissions
|
||||
|
||||
.. exercise:: Add access rights.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. _tutorials/getting_started/A_i18n:
|
||||
|
||||
================================
|
||||
Advanced A: Internationalization
|
||||
================================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
||||
@@ -1,526 +0,0 @@
|
||||
.. _tutorials/getting_started/B_acl_irrules:
|
||||
|
||||
================================
|
||||
Advanced B: ACL and Record Rules
|
||||
================================
|
||||
|
||||
ACL stands for "Access Control List"
|
||||
|
||||
.. warning::
|
||||
|
||||
This tutorial assumes you have completed the :ref:`Core Training <tutorials/getting_started>`.
|
||||
|
||||
To follow the exercise, it is recommended that you fetch the branch
|
||||
{BRANCH}-core from the `technical training solutions
|
||||
<https://github.com/odoo/technical-training-solutions/tree/{BRANCH}-core>`_ repository. It
|
||||
contains a version of the module created during the core training we can use as a starting
|
||||
point.
|
||||
|
||||
So far we have mostly concerned ourselves with implementing useful features.
|
||||
However in most business scenarios *security* quickly becomes a concern:
|
||||
currently,
|
||||
|
||||
* Any employee (which is what ``group_user`` stands for) can create, read,
|
||||
update or delete properties, property types, or property tags.
|
||||
* If ``estate_account`` is installed then only agents allowed to interact
|
||||
with invoicing can confirm sales as that's necessary to :ref:`create an
|
||||
invoice <tutorials/getting_started/14_other_module/create>`.
|
||||
|
||||
However:
|
||||
|
||||
* We do not want third parties to be able to access properties directly.
|
||||
* Not all our employees may be real-estate agents (e.g. administrative
|
||||
personnel, property managers, ...), we don't want non-agents to see the
|
||||
available properties.
|
||||
* Real-estate agents don't need or get to decide what property types or tags are
|
||||
*available*.
|
||||
* Real-estate agents can have *exclusive* properties, we do not want one agent
|
||||
to be able to manage another's exclusives.
|
||||
* All real-estate agents should be able to confirm the sale of a property they
|
||||
can manage, but we do not want them to be able to validate or mark as paid
|
||||
any invoice in the system.
|
||||
|
||||
.. note::
|
||||
|
||||
We may actually be fine with some or most of these for a small business.
|
||||
|
||||
Because it's easier for users to disable unnecessary security rules than it
|
||||
is to create them from nothing, it's better to err on the side of caution
|
||||
and limit access: users can relax that access if necessary or convenient.
|
||||
|
||||
Groups
|
||||
======
|
||||
|
||||
.. seealso::
|
||||
|
||||
The documentation related to this topic can be found in :ref:`the security
|
||||
reference <reference/security>`.
|
||||
|
||||
:doc:`/contributing/development/coding_guidelines` document the format and
|
||||
location of master data items.
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section,
|
||||
|
||||
- We can make employees *real-estate agents* or *real-estate managers*.
|
||||
- The ``admin`` user is a real-estate manager.
|
||||
- We have a new *real-estate agent* employee with no access to invoicing
|
||||
or administration.
|
||||
|
||||
It would not be practical to attach individual security rules to employees any
|
||||
time we need a change so *groups* link security rules and users. They correspond
|
||||
to roles that can be assigned to employees.
|
||||
|
||||
For most Odoo applications [#app]_ a good baseline is to have *user* and
|
||||
*manager* (or administrator) roles: the manager can change the configuration of
|
||||
the application and oversee the entirety of its use while the user can well,
|
||||
use the application [#appuser]_.
|
||||
|
||||
This baseline seems sufficient for us:
|
||||
|
||||
* Real estate managers can configure the system (manage available types and
|
||||
tags) as well as oversee every property in the pipeline.
|
||||
* Real estate agents can manage the properties under their care, or properties
|
||||
which are not specifically under the care of any agent.
|
||||
|
||||
In keeping with Odoo's data-driven nature, a group is no more than a record of
|
||||
the ``res.groups`` model. They are normally part of a module's :ref:`master data
|
||||
<tutorials/getting_started/C_data>`, defined in one of the module's data files.
|
||||
|
||||
As simple example `can be found here <https://github.com/odoo/odoo/blob/532c083cbbe0ee6e7a940e2bdc9c677bd56b62fa/addons/hr/security/hr_security.xml#L9-L14>`_.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create the ``security.xml`` file in the appropriate folder and add it to the ``__manifest__.py`` file.
|
||||
|
||||
#. If not already, add a ``'category'`` field to your ``__manifest__.py`` with value ``Real Estate/Brokerage``.
|
||||
|
||||
#. Add a record creating a group with the id ``estate_group_user``, the name "Agent"
|
||||
and the category ``base.module_category_real_estate_brokerage``.
|
||||
|
||||
#. Below that, add a record creating a group with the id ``estate_group_manager``,
|
||||
the name "Manager" and the category ``base.module_category_real_estate_brokerage``.
|
||||
The ``estate_group_manager`` group needs to imply ``estate_group_user``.
|
||||
|
||||
.. note::
|
||||
|
||||
Where does that **category** comes from ? It's a *module category*.
|
||||
Here we used the category id ``base.module_category_real_estate_brokerage``
|
||||
which was automatically generated by Odoo based on the `category` set in the ``__manifest__.py`` of the module.
|
||||
You can also find here the list of
|
||||
`default module categories <https://github.com/odoo/odoo/blob/71da80deb044852a2af6b111d695f94aad7803ac/odoo/addons/base/data/ir_module_category_data.xml>`_
|
||||
provided by Odoo.
|
||||
|
||||
.. tip::
|
||||
|
||||
Since we modified data files, remember to restart Odoo and update the
|
||||
module using ``-u estate``.
|
||||
|
||||
If you go to :menuselection:`Settings --> Manage Users` and open the
|
||||
``admin`` user ("Mitchell Admin"), you should see a new section:
|
||||
|
||||
.. figure:: B_acl_irrules/groups.png
|
||||
|
||||
Set the admin user to be a *Real Estate manager*.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Via the web interface, create a new user with only the "real estate agent"
|
||||
access. The user should not have any Invoicing or Administration access.
|
||||
|
||||
Use a private tab or window to log in with the new user (remember to set
|
||||
a password), as the real-estate agent you should only see the real estate
|
||||
application, and possibly the Discuss (chat) application:
|
||||
|
||||
.. figure:: B_acl_irrules/agent.png
|
||||
|
||||
Access Rights
|
||||
=============
|
||||
|
||||
.. seealso:: The documentation related to this topic can be found at
|
||||
:ref:`reference/security/acl`.
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section,
|
||||
|
||||
- Employees who are not at least real-estate agents will not see the
|
||||
real-estate application.
|
||||
- Real-estate agents will not be able to update the property types or tags.
|
||||
|
||||
Access rights were first introduced in :ref:`tutorials/getting_started/05_securityintro`.
|
||||
|
||||
Access rights are a way to give users access to models *via* groups: associate
|
||||
an access right to a group, then all users with that group will have the access.
|
||||
|
||||
For instance we don't want real-estate agents to be able to modify what property
|
||||
types are available, so we would not link that access to the "user" group.
|
||||
|
||||
Access rights can only give access, they can't remove it: when access is
|
||||
checked, the system looks to see if *any* access right associated with the user
|
||||
(via any group) grants that access.
|
||||
|
||||
====== ====== ==== ====== ======
|
||||
group create read update delete
|
||||
------ ------ ---- ------ ------
|
||||
A X X
|
||||
B X
|
||||
C X
|
||||
====== ====== ==== ====== ======
|
||||
|
||||
A user with the groups A and C will be able to do anything but delete the object
|
||||
while one with B and C will be able to read and update it, but not create or delete it.
|
||||
|
||||
.. note::
|
||||
|
||||
* The group of an access right can be omitted, this means the ACL applies
|
||||
to *every user*, this is a useful but risky fallback as depending on the
|
||||
applications installed it can grant even non-users access to the model.
|
||||
* If no access right applies to a user, they are not granted access
|
||||
(default-deny).
|
||||
* If a menu item points to a model to which a user doesn't have access and
|
||||
has no submenus which the user can see, the menu will not be displayed.
|
||||
|
||||
.. exercise:: Update the access rights file to:
|
||||
|
||||
* Give full access to all objects to your Real Estate Manager group.
|
||||
* Give agents (real estate users) only read access to types and tags.
|
||||
* Give nobody the right to delete properties.
|
||||
* Check that your agent user is not able to alter types or tags, or to
|
||||
delete properties, but that they can otherwise create or update
|
||||
properties.
|
||||
|
||||
.. warning::
|
||||
|
||||
Remember to give different xids to your ``ir.model.access`` records
|
||||
otherwise they will overwrite one another.
|
||||
|
||||
Since the "demo" user was not made a real-estate agent or manager, they should
|
||||
not even be able to see the real-estate application. Use a private tab or window
|
||||
to check for this (the "demo" user has the password "demo").
|
||||
|
||||
Record Rules
|
||||
============
|
||||
|
||||
.. seealso:: The documentation related to this topic can be found at
|
||||
:ref:`reference/security/rules`.
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, agents will not be able to see the properties
|
||||
exclusive to their colleagues; but managers will still be able to see
|
||||
everything.
|
||||
|
||||
Access rights can grant access to an entire model but often we need to be
|
||||
more specific: while an agent can interact with properties in general we may not
|
||||
want them to update or even see properties managed by one of their colleagues.
|
||||
|
||||
Record *rules* provide that precision: they can grant or reject access to
|
||||
individual records:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="rule_id" model="ir.rule">
|
||||
<field name="name">A description of the rule's role</field>
|
||||
<field name="model_id" ref="model_to_manage"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="groups" eval="[Command.link(ref('base.group_user'))]"/>
|
||||
<field name="domain_force">[
|
||||
'|', ('user_id', '=', user.id),
|
||||
('user_id', '=', False)
|
||||
]</field>
|
||||
</record>
|
||||
|
||||
The :ref:`reference/orm/domains` is how access is managed: if the record passes
|
||||
then access is granted, otherwise access is rejected.
|
||||
|
||||
.. tip::
|
||||
|
||||
Because rules tends to be rather complex and not created in bulk, they're
|
||||
usually created in XML rather than the CSV used for access rights.
|
||||
|
||||
The rule above:
|
||||
|
||||
* Only applies to the "create", "update" (write) and "delete" (unlink)
|
||||
operations: here we want every employee to be able to see other users' records
|
||||
but only the author / assignee can update a record.
|
||||
* Is :ref:`non-global <reference/security/rules/global>` so we can provide an
|
||||
additional rule for e.g. managers.
|
||||
* Allows the operation if the current user (``user.id``) is set (e.g. created,
|
||||
or is assigned) on the record, or if the record has no associated user at all.
|
||||
|
||||
.. note::
|
||||
|
||||
If no rule is defined or applies to a model and operation, then the
|
||||
operation is allowed (*default-allow*), this can have odd effects
|
||||
if access rights are not set up correctly (are too permissive).
|
||||
|
||||
.. exercise::
|
||||
|
||||
Define a rule which limits agents to only being able to see or modify
|
||||
properties which have no salesperson, or for which they are the salesperson.
|
||||
|
||||
You may want to create a second real-estate agent user, or create a few
|
||||
properties for which the salesperson is a manager or some other user.
|
||||
|
||||
Verify that your real estate manager(s) can still see all properties. If
|
||||
not, why not? Remember:
|
||||
|
||||
The ``estate_group_manager`` group needs to imply ``estate_group_user``.
|
||||
|
||||
Security Override
|
||||
=================
|
||||
|
||||
Bypassing Security
|
||||
------------------
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, agents should be able to confirm property sales
|
||||
without needing invoicing access.
|
||||
|
||||
If you try to mark a property as "sold" as the real estate agent, you should get
|
||||
an access error:
|
||||
|
||||
.. figure:: B_acl_irrules/error.png
|
||||
|
||||
This happens because ``estate_account`` tries to create an invoice during the
|
||||
process, but creating an invoice requires the right to all invoice management.
|
||||
|
||||
We want agents to be able to confirm a sale without them having full invoicing
|
||||
access, which means we need to *bypass* the normal security checks of Odoo in
|
||||
order to create an invoice *despite* the current user not having the right to
|
||||
do so.
|
||||
|
||||
There are two main ways to bypass existing security checks in Odoo, either
|
||||
wilfully or as a side-effect:
|
||||
|
||||
* The ``sudo()`` method will create a new recordset in "sudo mode", this ignores
|
||||
all access rights and record rules (although hard-coded group and user checks
|
||||
may still apply).
|
||||
* Performing raw SQL queries will bypass access rights and record rules as a
|
||||
side-effect of bypassing the ORM itself.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Update ``estate_account`` to bypass access rights and rules when creating
|
||||
the invoice.
|
||||
|
||||
.. danger::
|
||||
|
||||
These features should generally be avoided, and only used with extreme care,
|
||||
after having checked that the current user and operation should be able to
|
||||
bypass normal access rights validation.
|
||||
|
||||
Operations performed in such modes should also rely on user input as little
|
||||
as possible, and should validate it to the maximum extent they can.
|
||||
|
||||
Programmatically checking security
|
||||
----------------------------------
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, the creation of the invoice should be resilient
|
||||
to security issues regardless to changes to ``estate``.
|
||||
|
||||
In Odoo, access rights and record rules are only checked *when performing data
|
||||
access via the ORM* e.g. creating, reading, searching, writing, or unlinking a
|
||||
record via ORM methods. Other methods do *not* necessarily check against any
|
||||
sort of access rights.
|
||||
|
||||
In the previous section, we bypassed the record rules when creating the invoice
|
||||
in ``action_sold``. This bypass can be reached by any user without any access
|
||||
right being checked:
|
||||
|
||||
- Add a print to ``action_sold`` in ``estate_account`` before the creation of
|
||||
the invoice (as creating the invoice accesses the property, therefore triggers
|
||||
an ACL check) e.g.::
|
||||
|
||||
print(" reached ".center(100, '='))
|
||||
|
||||
You should see ``reached`` in your Odoo log, followed by an access error.
|
||||
|
||||
.. danger:: Just because you're already in Python code does not mean any access
|
||||
right or rule has or will be checked.
|
||||
|
||||
*Currently* the accesses are implicitly checked by accessing data on ``self`` as
|
||||
well as calling ``super()`` (which does the same and *updates* ``self``),
|
||||
triggering access errors and cancelling the transaction "uncreating" our
|
||||
invoice.
|
||||
|
||||
*However* if this changes in the future, or we add side-effects to the method
|
||||
(e.g. reporting the sale to a government agency), or bugs are introduced in
|
||||
``estate``, ... it would be possible for non-agents to trigger operations they
|
||||
should not have access to.
|
||||
|
||||
Therefore when performing non-CRUD operations, or legitimately bypassing the
|
||||
ORM or security, or when triggering other side-effects, it is extremely
|
||||
important to perform *explicit security checks*.
|
||||
|
||||
Explicit security checks can be performed by:
|
||||
|
||||
* Checking who the current user is (``self.env.user``) and match them against
|
||||
specific models or records.
|
||||
* Checking that the current user has specific groups hard-coded to allow or deny
|
||||
an operation (``self.env.user.has_group``).
|
||||
* Calling the ``check_access_rights(operation)`` method on a recordset, this
|
||||
verifies whether the current user has access to the model itself.
|
||||
* Calling ``check_access_rule(operations)`` on a non-empty recordset, this
|
||||
verifies that the current user is allowed to perform the operation on *every*
|
||||
record of the set.
|
||||
|
||||
.. warning:: Checking access rights and checking record rules are separate
|
||||
operations, if you're checking record rules you usually want to
|
||||
also check access rights beforehand.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Before creating the invoice, use ``check_access_rights`` and
|
||||
``check_access_rule`` to ensure that the current user can update properties
|
||||
in general as well as the specific property the invoice is for.
|
||||
|
||||
Re-run the bypass script, check that the error occurs before the print.
|
||||
|
||||
.. _tutorials/getting_started/B_acl_irrules/multicompany:
|
||||
|
||||
Multi-company security
|
||||
======================
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`reference/howtos/company` for an overview of multi-company facilities
|
||||
in general, and :ref:`multi-company security rules <howto/company/security>`
|
||||
in particular.
|
||||
|
||||
Documentation on rules in general can, again, be found at
|
||||
:ref:`reference/security/rules`.
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, agents should only have access to properties
|
||||
of their agency (or agencies).
|
||||
|
||||
For one reason or another we might need to manage our real-estate business
|
||||
as multiple companies e.g. we might have largely autonomous agencies, a
|
||||
franchise setup, or multiple brands (possibly from having acquired other
|
||||
real-estate businesses) which remain legally or financially separate from one
|
||||
another.
|
||||
|
||||
Odoo can be used to manage multiple companies inside the same system, however
|
||||
the actual handling is up to individual modules: Odoo itself provides the tools
|
||||
to manage the issue of company-dependent fields and *multi-company rules*,
|
||||
which is what we're going to concern ourselves with.
|
||||
|
||||
We want different agencies to be "siloed" from one another, with properties
|
||||
belonging to a given agency and users (whether agents or managers) only able to
|
||||
see properties linked to their agency.
|
||||
|
||||
As before, because this is based on non-trivial records it's easier for a user
|
||||
to relax rules than to tighten them so it makes sense to default to a
|
||||
relatively stronger security model.
|
||||
|
||||
Multi-company rules are simply record rules based on the ``company_ids`` or
|
||||
``company_id`` fields:
|
||||
|
||||
* ``company_ids`` is all the companies to which the current user has access
|
||||
* ``company_id`` is the currently active company (the one the user is currently
|
||||
working in / for).
|
||||
|
||||
Multi-company rules will *usually* use the former i.e. check if the record is
|
||||
associated with *one* of the companies the user has access to:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.rule" id="hr_appraisal_plan_comp_rule">
|
||||
<field name="name">Appraisal Plan multi-company</field>
|
||||
<field name="model_id" ref="model_hr_appraisal_plan"/>
|
||||
<field name="domain_force">[
|
||||
'|', ('company_id', '=', False),
|
||||
('company_id', 'in', company_ids)
|
||||
]</field>
|
||||
</record>
|
||||
|
||||
.. danger::
|
||||
|
||||
Multi-company rules are usually :ref:`global <reference/security/rules/global>`,
|
||||
otherwise there is a high risk that additional rules would allow bypassing
|
||||
the multi-company rules.
|
||||
|
||||
.. exercise::
|
||||
|
||||
* Add a ``company_id`` field to ``estate.property``, it should be required
|
||||
(we don't want agency-less properties), and should default to the current
|
||||
user's current company.
|
||||
* Create a new company, with a new estate agent in that company.
|
||||
* The manager should be a member of both companies.
|
||||
* The old agent should only be a member of the old company.
|
||||
* Create a few properties in each company (either use the company selector
|
||||
as the manager or use the agents). Unset the default salesman to avoid
|
||||
triggering *that* rule.
|
||||
* All agents can see all companies, which is not desirable, add the record
|
||||
rule restricting this behaviour.
|
||||
|
||||
.. warning:: remember to ``--update`` your module when you change its model or
|
||||
data
|
||||
|
||||
Visibility != security
|
||||
======================
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, real-estate agents should not see the Settings
|
||||
menu of the real-estate application, but should still be able to set the
|
||||
property type or tags.
|
||||
|
||||
Specific Odoo models can be associated directly with groups (or companies, or
|
||||
users). It is important to figure out whether this association is a *security*
|
||||
or a *visibility* feature before using it:
|
||||
|
||||
* *Visibility* features mean a user can still access the model or record
|
||||
otherwise, either through another part of the interface or by :doc:`performing
|
||||
operations remotely using RPC <../../api/external_api>`, things might just not be
|
||||
visible in the web interface in some contexts.
|
||||
* *Security* features mean a user can not access records, fields or operations.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
* Groups on *model fields* (in Python) are a security feature, users outside the
|
||||
group will not be able to retrieve the field, or even know it exists.
|
||||
|
||||
Example: in server actions, `only system users can see or update Python code
|
||||
<https://github.com/odoo/odoo/blob/7058e338a980268df1c502b8b2860bdd8be9f727/odoo/addons/base/models/ir_actions.py#L414-L417>`_.
|
||||
* Groups on *view elements* (in XML) are a visibility feature, users outside the
|
||||
group will not be able to see the element or its content in the form but they
|
||||
will otherwise be able to interact with the object (including that field).
|
||||
|
||||
Example: `only managers have an immediate filter to see their teams' leaves
|
||||
<https://github.com/odoo/odoo/blob/8e19904bcaff8300803a7b596c02ec45fcf36ae6/addons/hr_holidays/report/hr_leave_reports.xml#L16>`_.
|
||||
* Groups on menus and actions are visibility features, the menu or action will
|
||||
not be shown in the interface but that doesn't prevent directly interacting
|
||||
with the underlying object.
|
||||
|
||||
Example: `only system administrators can see the elearning settings menu
|
||||
<https://github.com/odoo/odoo/blob/ff828a3e0c5386dc54e6a46fd71de9272ef3b691/addons/website_slides/views/website_slides_menu_views.xml#L64-L69>`_.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Real Estate agents can not add property types or tags, but can see their
|
||||
options from the Property form view when creating it.
|
||||
|
||||
The Settings menu just adds noise to their interface, make it only
|
||||
visible to managers.
|
||||
|
||||
Despite not having access to the Property Types and Property Tags menus anymore,
|
||||
agents can still access the underlying objects since they can still select
|
||||
tags or a type to set on their properties.
|
||||
|
||||
.. [#app] An Odoo Application is a group of related modules covering a business
|
||||
area or field, usually composed of a base module and a number of
|
||||
expansions on that base to add optional or specific features, or link
|
||||
to other business areas.
|
||||
|
||||
.. [#appuser] For applications which would be used by most or every employees,
|
||||
the "application user" role might be done away with and its
|
||||
abilities granted to all employees directly e.g. generally all
|
||||
employees can submit expenses or take time off.
|
||||
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
@@ -1,421 +0,0 @@
|
||||
.. _tutorials/getting_started/C_data:
|
||||
|
||||
================================
|
||||
Advanced C: Master and Demo Data
|
||||
================================
|
||||
|
||||
.. tip:: This tutorial assumes you followed the Core Training.
|
||||
|
||||
To do the exercise, fetch the branch {BRANCH}-core from the `technical training solutions
|
||||
<https://github.com/odoo/technical-training-solutions/tree/{BRANCH}-core>`_ repository. It
|
||||
contains a basic module we will use as a starting point
|
||||
|
||||
Data Types
|
||||
==========
|
||||
|
||||
Master Data
|
||||
-----------
|
||||
|
||||
Master data is usually part of the technical or business requirements for the module. In other
|
||||
words, such data is often necessary for the module to work properly. This data will always be
|
||||
installed when installing the module.
|
||||
|
||||
We already met technical data previously since we have defined
|
||||
:ref:`security rules <tutorials/getting_started/N_security>`, :ref:`views<reference/views>` and
|
||||
:ref:`actions<reference/actions>`. Those are one kind of master data.
|
||||
|
||||
On top of technical data, business data can be defined, e.g. countries, currencies, units of measure,
|
||||
as well as complete country localization (legal reports, tax definitions, chart of account), and much
|
||||
more...
|
||||
|
||||
Demo Data
|
||||
---------
|
||||
|
||||
In additional to master data, which are requirements for a module to work properly, we also like
|
||||
having data for demonstration purposes:
|
||||
|
||||
* Help the sales representatives make their demos quickly.
|
||||
* Have a set of working data for developers to test new features and see how these new features look
|
||||
with data they might not have added themselves.
|
||||
* Test that the data is loaded correctly, without raising an error.
|
||||
* Setup most of the features to be used quickly when creating a new database.
|
||||
|
||||
Demo data is automatically loaded when you start the server if you don't explicitly say you don't
|
||||
want it. This can be done in the database manager or with the command line.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./odoo-bin -h
|
||||
Usage: odoo-bin [options]
|
||||
|
||||
Options:
|
||||
--version show program's version number and exit
|
||||
-h, --help show this help message and exit
|
||||
|
||||
Common options:
|
||||
[...]
|
||||
--without-demo=WITHOUT_DEMO
|
||||
disable loading demo data for modules to be installed
|
||||
(comma-separated, use "all" for all modules). Requires
|
||||
-d and -i. Default is none
|
||||
[...]
|
||||
|
||||
$ ./odoo-bin --addons-path=... -d db -i account --without-demo=all
|
||||
|
||||
Data Declaration
|
||||
================
|
||||
|
||||
Manifest
|
||||
--------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`Module Manifests<reference/module/manifest>`.
|
||||
|
||||
Data is declared either in CSV or in XML.
|
||||
Each file containing data must be added in the manifest for them to be loaded.
|
||||
|
||||
The keys to use in the manifest to add new data are ``data`` for the master data and ``demo`` for
|
||||
the demo data. Both values should be a list of strings representing the relative paths to the files
|
||||
declaring the data.
|
||||
|
||||
Usually, demo data is in a ``demo`` folder, views and actions are in a ``views``
|
||||
folder, security related data is in a ``security`` folder, and other data is in a
|
||||
``data`` folder.
|
||||
|
||||
If your work tree looks like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
estate
|
||||
├── data
|
||||
│ └── master_data.xml
|
||||
├── demo
|
||||
│ └── demo_data.xml
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── security
|
||||
│ └── ir.model.access.csv
|
||||
├── views
|
||||
│ └── estate_property_offer_views.xml
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
Your manifest should look like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
"name": "Real Estate",
|
||||
"depends": [
|
||||
...
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv", # CSV and XML files are loaded at the same place
|
||||
"views/estate_property_offer_views.xml", # Views are data too
|
||||
"data/master_data.xml", # Split the data in multiple files depending on the model
|
||||
],
|
||||
"demo": [
|
||||
"demo/demo_data.xml",
|
||||
]
|
||||
"application": True,
|
||||
}
|
||||
|
||||
CSV
|
||||
---
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`CSV data files<reference/data/csvdatafiles>`.
|
||||
|
||||
The easiest way to declare simple data is by using the CSV format. This is however limited in terms
|
||||
of features: use it for long lists of simple models, but prefer XML otherwise.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
id,field_a,field_b,related_id:id
|
||||
id1,valueA1,valueB1,module.relatedid
|
||||
id2,valueA2,valueB2,module.relatedid
|
||||
|
||||
.. tip:: Your IDE has probably an extension to have a syntax highlighting of the CSV files
|
||||
|
||||
* `Atom <https://atom.io/packages/rainbow-csv>`__.
|
||||
* `PyCharm/IntelliJ <https://plugins.jetbrains.com/plugin/10037-csv-plugin>`__.
|
||||
* `Vim <https://github.com/mechatroner/rainbow_csv>`__.
|
||||
* `Visual Studio <https://marketplace.visualstudio.com/items?itemName=mechatroner.rainbow-csv>`__.
|
||||
|
||||
.. exercise:: Add some standard Real Estate Property Types for the `estate` module: Residential,
|
||||
Commercial, Industrial and Land. These should always be installed.
|
||||
|
||||
XML
|
||||
---
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`Data Files<reference/data>`.
|
||||
|
||||
When the data to create is more complex it can be useful, or even necessary, to do it in XML.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="field_a">valueA1</field>
|
||||
<field name="field_b">valueB1</field>
|
||||
</record>
|
||||
|
||||
<record id="id2" model="tutorial.example">
|
||||
<field name="field_a">valueA2</field>
|
||||
<field name="field_b">valueB2</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. exercise:: Create some demo data for the `estate` module.
|
||||
|
||||
================== ==================== ======================
|
||||
Field Values Values
|
||||
================== ==================== ======================
|
||||
name Big Villa Trailer home
|
||||
state New Canceled
|
||||
description A nice and big villa Home in a trailer park
|
||||
postcode 12345 54321
|
||||
date_availability 2020-02-02 1970-01-01
|
||||
expected_price 1,600,000 100,000
|
||||
selling_price 120,000
|
||||
bedrooms 6 1
|
||||
living_area 100 10
|
||||
facades 4 4
|
||||
garage True False
|
||||
garden True
|
||||
garden_area 100000
|
||||
garden_orientation South
|
||||
================== ==================== ======================
|
||||
|
||||
Data Extension
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
During the Core Training, we saw in the :ref:`tutorials/getting_started/13_inheritance` chapter we
|
||||
could inherit (extend) an existing view. This was a special case of data extension: any data can be
|
||||
extended in a module.
|
||||
|
||||
When you are adding new fields to an existing model in a new module, you might want to populate
|
||||
those fields on the records created in the modules you are depending on. This is done by giving the
|
||||
`xml_id` of the record you want to extend. It won't replace it, in this case we will set the
|
||||
``field_c`` to the given value for both records.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="field_c">valueC1</field>
|
||||
</record>
|
||||
|
||||
<record id="id2" model="tutorial.example">
|
||||
<field name="field_c">valueC2</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
||||
``ref``
|
||||
~~~~~~~
|
||||
|
||||
Related fields can be set using the ``ref`` key. The value of that key is the ``xml_id`` of the
|
||||
record you want to link. Remember the ``xml_id`` is composed of the name of the module where the
|
||||
data is first declared, followed by a dot, followed by the ``id`` of the record (just the ``id``
|
||||
works too if you are in the module declaring it).
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="related_id" ref="module.relatedid"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. exercise:: Create some demo data offers for the properties you created.
|
||||
|
||||
Create offers using the partners defined in ``base``
|
||||
|
||||
============== ========= ======= ========
|
||||
Partner Estate Price Validity
|
||||
============== ========= ======= ========
|
||||
Azure Interior Big Villa 10000 14
|
||||
Azure Interior Big Villa 1500000 14
|
||||
Deco Addict Big Villa 1500001 14
|
||||
============== ========= ======= ========
|
||||
|
||||
.. exercise:: Ensure both of your demo properties are created with their Property Type set to Residential.
|
||||
|
||||
``eval``
|
||||
~~~~~~~~
|
||||
|
||||
The value to assign to a field is not always a simple string and you might need to compute it.
|
||||
It can also be used to optimize the insertion of related values, or because a constraint forces you
|
||||
to add the related values in batch. See ::ref:`Add X2many fields
|
||||
<tutorials/getting_started/C_data/x2m>`.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="year" eval="datetime.now().year+1"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. exercise:: The offers you added should always be in a date relative to the installation of the
|
||||
module.
|
||||
|
||||
``search``
|
||||
~~~~~~~~~~
|
||||
|
||||
Sometimes, you need to call the ORM to do a ``search``. This is not feasible with the CSV format.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="account.move.line">
|
||||
<field name="account_id" search="[
|
||||
('user_type_id', '=', ref('account.data_account_type_direct_costs')),
|
||||
('company_id', '=', obj().env.company.id)]
|
||||
"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
In this code snippet, it is needed because the master data depends on the localization
|
||||
installed.
|
||||
|
||||
``function``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
You might also need to execute python code when loading data.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<function model="tutorial.example" name="action_validate">
|
||||
<value eval="[ref('demo_invoice_1')]"/>
|
||||
</function>
|
||||
|
||||
.. exercise:: Validate one of the demo data offers by using the "Accept Offer" button. Refuse the
|
||||
others.
|
||||
|
||||
|
||||
.. _tutorials/getting_started/C_data/x2m:
|
||||
|
||||
Add X2many fields
|
||||
-----------------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:class:`~odoo.fields.Command`.
|
||||
|
||||
If you need to add related data in a One2many or a Many2many field, you can do so by using the
|
||||
:class:`~odoo.fields.Command` methods.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="related_ids" eval="[
|
||||
Command.create({
|
||||
'name': 'My name',
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Your name',
|
||||
}),
|
||||
Command.link(ref('model.xml_id')),
|
||||
]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. exercise:: Create one new Property, but this time with some offers created directly inside the
|
||||
One2many field linked to the Offers.
|
||||
|
||||
Accessing the data
|
||||
==================
|
||||
|
||||
.. warning:: You should never access demo data outside of the demo data declaration, not even in
|
||||
tests.
|
||||
|
||||
There are multiple ways to access the master/demo data.
|
||||
|
||||
In python code, you can use the ``env.ref(self, xml_id, raise_if_not_found=True)`` method. It
|
||||
returns the recordset linked to the ``xml_id`` you specify.
|
||||
|
||||
In XML, you can use the `ref` key like this
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="related_id" ref="module.relatedid"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
It will call the ref method, and store the id of the record returned on the field ``related_id`` of
|
||||
the record of type ``tutorial.example`` with id ``id1``.
|
||||
|
||||
In CSV, the title of the column must be suffixed with ``:id`` or ``/id``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
id,parent_id:id,name
|
||||
"child1","module.parent","Name1"
|
||||
"child2","module.parent","Name2"
|
||||
"child3","module.parent","Name3"
|
||||
|
||||
In SQL, it is more complicated, see :ref:`the advanced section
|
||||
<tutorials/getting_started/C_data/xml_id>`.
|
||||
|
||||
.. warning:: Data can always be deleted by the user. Always code defensively, taking this into
|
||||
account.
|
||||
|
||||
|
||||
|
||||
|
||||
Advanced
|
||||
========
|
||||
|
||||
.. _tutorials/getting_started/C_data/xml_id:
|
||||
|
||||
What is the XML id?
|
||||
-------------------
|
||||
|
||||
Because we don't want a column ``xml_id`` in every single SQL table of the database, we need a
|
||||
mechanism to store it. This is done with the ``ir.model.data`` model.
|
||||
|
||||
It contains the name of the record (the ``xml_id``) along with the module in which it is defined,
|
||||
the model defining it, and the id of it.
|
||||
|
||||
No update
|
||||
---------
|
||||
|
||||
The records created with the ``noupdate`` flag won't be updated when upgrading the module that
|
||||
created them, but it will be created if it didn't exist yet.
|
||||
|
||||
.. note:: ``odoo-bin -i module`` will bypass this setting and always load the data. But normally
|
||||
one shouldn't do this on a production database.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo noupdate="1">
|
||||
<record id="id1" model="model">
|
||||
<field name="fieldA" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
||||
Import as SQL
|
||||
-------------
|
||||
|
||||
In some cases, it makes sense to do the import directly in SQL. This is however discouraged as it
|
||||
bypasses all the features of the ORM, computed fields (including metadata) and python constraints.
|
||||
|
||||
.. note:: Generally using raw SQL also bypasses ACLs and increases the risks of injections.
|
||||
|
||||
**Reference**: :ref:`Security in Odoo<reference/security>`
|
||||
|
||||
* It can help to speed the import time by a lot
|
||||
`with huge files <https://github.com/odoo/enterprise/blob/d46cceef8c594b9056d0115edb7169e207a5986f/product_unspsc/hooks.py#L19>`__.
|
||||
* For more complex imports like for the
|
||||
`translations <https://github.com/odoo/odoo/blob/e1f8d549895cd9c459e6350430f30d541d02838a/odoo/addons/base/models/ir_translation.py#L24>`__.
|
||||
* It can be necessary to
|
||||
`initialize the database <https://github.com/odoo/odoo/blob/e1f8d549895cd9c459e6350430f30d541d02838a/odoo/addons/base/data/base_data.sql>`__.
|
||||
@@ -1,14 +0,0 @@
|
||||
.. _tutorials/getting_started/D_mixins:
|
||||
|
||||
==================
|
||||
Advanced D: Mixins
|
||||
==================
|
||||
|
||||
If you need to interface with common Odoo features such as the chatter, you can rely on
|
||||
:doc:`mixins <../../reference/backend/mixins>`.
|
||||
They are Odoo models exposing useful methods through inheritance.
|
||||
|
||||
To learn and play with mixins, visit `this repository <https://github.com/tivisse/odoodays-2018/>`_.
|
||||
This module for a plant nursery is training material developed for the OXP 2018. You don't need to
|
||||
code it on your side. But you can check the presentations in the :file:`/static/pdf` directory and
|
||||
play with the module to discover some magic features in Odoo.
|
||||
@@ -1,301 +0,0 @@
|
||||
.. _tutorials/getting_started/E_unittest:
|
||||
|
||||
=============================
|
||||
Advanced E: Python Unit Tests
|
||||
=============================
|
||||
|
||||
.. tip:: This tutorial assumes you followed the Core Training.
|
||||
|
||||
To do the exercise, fetch the branch {BRANCH}-core from the `technical training solutions
|
||||
<https://github.com/odoo/technical-training-solutions/tree/{BRANCH}-core>`_ repository. It
|
||||
contains a basic module we will use as a starting point
|
||||
|
||||
**Reference**:
|
||||
`Odoo's Test Framework: Learn Best Practices <https://www.youtube.com/watch?v=JEIscps0OOQ>`__
|
||||
(Odoo Experience 2020) on YouTube.
|
||||
|
||||
Writing tests is a necessity for multiple reasons. Here is a non-exhaustive list:
|
||||
|
||||
* Ensure code will not be broken in the future
|
||||
* Define the scope of your code
|
||||
* Give examples of use cases
|
||||
* It is one way to technically document the code
|
||||
* Help your coding by defining your goal before working towards it
|
||||
|
||||
Running Tests
|
||||
=============
|
||||
|
||||
Before knowing how to write tests, we need to know how to run them.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin -h
|
||||
Usage: odoo-bin [options]
|
||||
|
||||
Options:
|
||||
--version show program's version number and exit
|
||||
-h, --help show this help message and exit
|
||||
|
||||
[...]
|
||||
|
||||
Testing Configuration:
|
||||
--test-file=TEST_FILE
|
||||
Launch a python test file.
|
||||
--test-enable Enable unit tests.
|
||||
--test-tags=TEST_TAGS
|
||||
Comma-separated list of specs to filter which tests to
|
||||
execute. Enable unit tests if set. A filter spec has
|
||||
the format: [-][tag][/module][:class][.method] The '-'
|
||||
specifies if we want to include or exclude tests
|
||||
matching this spec. The tag will match tags added on a
|
||||
class with a @tagged decorator (all Test classes have
|
||||
'standard' and 'at_install' tags until explicitly
|
||||
removed, see the decorator documentation). '*' will
|
||||
match all tags. If tag is omitted on include mode, its
|
||||
value is 'standard'. If tag is omitted on exclude
|
||||
mode, its value is '*'. The module, class, and method
|
||||
will respectively match the module name, test class
|
||||
name and test method name. Example: --test-tags
|
||||
:TestClass.test_func,/test_module,external Filtering
|
||||
and executing the tests happens twice: right after
|
||||
each module installation/update and at the end of the
|
||||
modules loading. At each stage tests are filtered by
|
||||
--test-tags specs and additionally by dynamic specs
|
||||
'at_install' and 'post_install' correspondingly.
|
||||
--screencasts=DIR Screencasts will go in DIR/{db_name}/screencasts.
|
||||
--screenshots=DIR Screenshots will go in DIR/{db_name}/screenshots.
|
||||
Defaults to /tmp/odoo_tests.
|
||||
|
||||
$ # run all the tests of account, and modules installed by account
|
||||
$ # the dependencies already installed are not tested
|
||||
$ # this takes some time because you need to install the modules, but at_install
|
||||
$ # and post_install are respected
|
||||
$ odoo-bin -i account --test-enable
|
||||
$ # run all the tests in this file
|
||||
$ odoo-bin --test-file=addons/account/tests/test_account_move_entry.py
|
||||
$ # test tags can help you filter quite easily
|
||||
$ odoo-bin --test-tags=/account:TestAccountMove.test_custom_currency_on_account_1
|
||||
|
||||
Integration Bots
|
||||
================
|
||||
|
||||
.. note:: This section is only for Odoo employees and people that are contributing to
|
||||
`github.com/odoo`. We highly recommend having your own CI otherwise.
|
||||
|
||||
When a test is written, it is important to make sure it always passes when modifications are
|
||||
applied to the source code. To automate this task, we use a development practice called
|
||||
Continuous Integration (CI). This is why we have some bots running all the tests at different
|
||||
moments.
|
||||
Whether you are working at Odoo or not, if you are trying to merge something inside `odoo/odoo`,
|
||||
`odoo/enterprise`, `odoo/upgrade` or on odoo.sh, you will have to go through the CI. If you are
|
||||
working on another project, you should think of adding your own CI.
|
||||
|
||||
Runbot
|
||||
------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
`Runbot FAQ <https://runbot.odoo.com/doc>`__.
|
||||
|
||||
Most of the tests are run on `Runbot <https://runbot.odoo.com>`__ every time a commit is pushed on
|
||||
GitHub.
|
||||
|
||||
You can see the state of a commit/branch by filtering on the runbot dashboard.
|
||||
|
||||
A **bundle** is created for each branch. A bundle consists of a configuration and
|
||||
batches.
|
||||
|
||||
A **batch** is a set of builds, depending on the parameters of the bundle.
|
||||
A batch is green (i.e. passes the tests) if all the builds are green.
|
||||
|
||||
A **build** is when we launch a server. It can be divided in sub-builds. Usually there are builds
|
||||
for the community version, the enterprise version (only if there is an enterprise branch but you
|
||||
can force the build), and the migration of the branch.
|
||||
A build is green if every sub-build is green.
|
||||
|
||||
A **sub-build** only does some parts of what a full build does. It is used to speed up the CI
|
||||
process. Generally it is used to split the post install tests in 4 parallel instances.
|
||||
A sub-build is green if all the tests are passing and there are no errors/warnings logged.
|
||||
|
||||
.. note::
|
||||
* All tests are run regardless of the modifications done. Correcting a typo in an error message or
|
||||
refactoring a whole module triggers the same tests. All modules will be installed as well. This means
|
||||
something might not work even if the Runbot is green, i.e. your changes depend on a module that the
|
||||
module the changes are in doesn't depend on.
|
||||
* The localization modules (i.e. country-specific modules) are not installed on Runbot (except
|
||||
the generic one). Some modules with external dependencies can also be excluded.
|
||||
* There is a nightly build running additional tests: module operations, localization, single
|
||||
module installs, multi-builds for nondeterministic bugs, etc.
|
||||
These are not kept in the standard CI to shorten the time of execution.
|
||||
|
||||
You can also login to a build built by Runbot. There are 3 users usable: `admin`, `demo` and
|
||||
`portal`. The password is the same as the login. This is useful to quickly test things on different
|
||||
versions without having to build it locally. The full logs are also available; these are used for
|
||||
monitoring.
|
||||
|
||||
Robodoo
|
||||
-------
|
||||
|
||||
You will most likely have to gain a little bit more experience before having the rights to summon
|
||||
robodoo, but here are a few remarks anyways.
|
||||
|
||||
Robodoo is the guy spamming the CI status as tags on your PRs, but he is also the guy that kindly
|
||||
integrates your commits into the main repositories.
|
||||
|
||||
When the last batch is green, the reviewer can ask robodoo to merge your PR (it is more
|
||||
a `rebase` than a `merge`). It will then go to the mergebot.
|
||||
|
||||
Mergebot
|
||||
--------
|
||||
|
||||
`Mergebot <https://mergebot.odoo.com>`__ is the last testing phase before merging a PR.
|
||||
|
||||
It will take the commits in your branch not yet present on the target, stage it and rerun the tests
|
||||
one more time, including the enterprise version even if you are only changing something in
|
||||
community.
|
||||
|
||||
This step can fail with a `Staging failed` error message. This could be due to
|
||||
|
||||
* a nondeterministic bug that is already on the target. If you are an Odoo employee, you can check
|
||||
those here: https://runbot.odoo.com/runbot/errors
|
||||
* a nondeterministic bug that you introduced but wasn't detected in the CI before
|
||||
* an incompatibility with another commit merged right before and what you are trying to merge
|
||||
* an incompatibility with the enterprise repository if you only did changes in the community repo
|
||||
|
||||
Always check that the issue does not come from you before asking the merge bot to retry: rebase
|
||||
your branch on the target and rerun the tests locally.
|
||||
|
||||
Modules
|
||||
=======
|
||||
|
||||
Because Odoo is modular, the tests need to be also modular. This means tests are defined in
|
||||
the module that adds the functionality you are adding in, and tests cannot depend on functionality
|
||||
coming from modules your module doesn't depend on.
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`Special Tags<reference/testing/tags>`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
# The CI will run these tests after all the modules are installed,
|
||||
# not right after installing the one defining it.
|
||||
@tagged('post_install', '-at_install') # add `post_install` and remove `at_install`
|
||||
class PostInstallTestCase(TransactionCase):
|
||||
def test_01(self):
|
||||
...
|
||||
|
||||
@tagged('at_install') # this is the default
|
||||
class AtInstallTestCase(TransactionCase):
|
||||
def test_01(self):
|
||||
...
|
||||
|
||||
|
||||
If the behavior you want to test can be changed by the installation of another module, you need to
|
||||
ensure that the tag `at_install` is set; otherwise, you can use the tag `post_install` to speed up
|
||||
the CI and ensure it is not changed if it shouldn't.
|
||||
|
||||
Writing a test
|
||||
==============
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
`Python unittest <https://docs.python.org/3/library/unittest.html>`__
|
||||
and :ref:`Testing Odoo<reference/testing>`.
|
||||
|
||||
Here are a few things to take into consideration before writing a test
|
||||
|
||||
* The tests should be independent of the data currently in the database (including demo data)
|
||||
* Tests should not impact the database by leaving/changing residual data. This is usually done by
|
||||
the test framework by doing a rollback. Therefore, you must never call ``cr.commit`` in a test
|
||||
(nor anywhere else in the business code).
|
||||
* For a bug fix, the test should fail before applying the fix and pass after.
|
||||
* Don't test something that is already tested elsewhere; you can trust the ORM. Most of the tests
|
||||
in business modules should only test the business flows.
|
||||
* You shouldn't need to flush data into the database.
|
||||
|
||||
.. note:: Remember that ``onchange`` only applies in the Form views, not by changing the attributes
|
||||
in python. This also applies in the tests. If you want to emulate a Form view, you can use
|
||||
``odoo.tests.common.Form``.
|
||||
|
||||
The tests should be in a ``tests`` folder at the root of your module. Each test file name
|
||||
should start with `test_` and be imported in the ``__init__.py`` of the test folder. You shouldn't
|
||||
import the test folder/module in the ``__init__.py`` of the module.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
estate
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── tests
|
||||
│ ├── test_*.py
|
||||
│ └── __init__.py
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
All the tests should extend ``odoo.tests.common.TransactionCase``. You usually define a
|
||||
``setUpClass`` and the tests. After writing the `setUpClass`, you have an `env` available in the
|
||||
class and can start interacting with the ORM.
|
||||
|
||||
These test classes are built on top of the ``unittest`` python module.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
# The CI will run these tests after all the modules are installed,
|
||||
# not right after installing the one defining it.
|
||||
@tagged('post_install', '-at_install')
|
||||
class EstateTestCase(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# add env on cls and many other things
|
||||
super(EstateTestCase, cls).setUpClass()
|
||||
|
||||
# create the data for each tests. By doing it in the setUpClass instead
|
||||
# of in a setUp or in each test case, we reduce the testing time and
|
||||
# the duplication of code.
|
||||
cls.properties = cls.env['estate.property'].create([...])
|
||||
|
||||
def test_creation_area(self):
|
||||
"""Test that the total_area is computed like it should."""
|
||||
self.properties.living_area = 20
|
||||
self.assertRecordValues(self.properties, [
|
||||
{'name': ..., 'total_area': ...},
|
||||
{'name': ..., 'total_area': ...},
|
||||
])
|
||||
|
||||
|
||||
def test_action_sell(self):
|
||||
"""Test that everything behaves like it should when selling a property."""
|
||||
self.properties.action_sold()
|
||||
self.assertRecordValues(self.properties, [
|
||||
{'name': ..., 'state': ...},
|
||||
{'name': ..., 'state': ...},
|
||||
])
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
self.properties.forbidden_action_on_sold_property()
|
||||
|
||||
.. note:: For better readability, split your tests into multiple files depending on the scope of the
|
||||
tests. You can also have a Common class that most of the tests should inherit from; this common
|
||||
class can define the whole setup for the module. For instance, in
|
||||
`account <{GITHUB_PATH}/addons/account/tests/common.py>`__.
|
||||
|
||||
.. exercise:: Update the code so no one can:
|
||||
|
||||
- Create an offer for a sold property
|
||||
- Sell a property with no accepted offers on it
|
||||
|
||||
and create tests for both of these cases. Additionally check that selling a property that can
|
||||
be sold is correctly marked as sold after selling it.
|
||||
|
||||
|
||||
.. exercise:: Someone keeps breaking the reset of Garden Area and Orientation when you uncheck the
|
||||
Garden checkbox. Make sure it doesn't happen again.
|
||||
|
||||
.. tip:: Tip: remember the note about `Form` a little bit above.
|
||||
@@ -1,9 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. _tutorials/getting_started/F_jstour:
|
||||
|
||||
====================
|
||||
Advanced F: JS Tours
|
||||
====================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
||||
@@ -1,9 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. _tutorials/getting_started/G_website:
|
||||
|
||||
=================================
|
||||
Advanced G: Controllers & Website
|
||||
=================================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
||||
@@ -1,9 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. _tutorials/getting_started/H_adv_views:
|
||||
|
||||
==========================
|
||||
Advanced H: Advanced Views
|
||||
==========================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
||||
@@ -1,9 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. _tutorials/getting_started/I_jswidget:
|
||||
|
||||
============================
|
||||
Advanced I: Custom JS Widget
|
||||
============================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
||||
@@ -1,315 +0,0 @@
|
||||
.. _tutorials/getting_started/J_reports:
|
||||
|
||||
=======================
|
||||
Advanced J: PDF Reports
|
||||
=======================
|
||||
|
||||
.. warning::
|
||||
|
||||
This tutorial assumes you have completed the :ref:`Core Training <tutorials/getting_started>`
|
||||
and have installed :ref:`wkhtmltopdf <setup/install/source/prepare>`.
|
||||
|
||||
To follow the exercise, it is recommended that you fetch the branch {BRANCH}-core from the
|
||||
`technical training solutions
|
||||
<https://github.com/odoo/technical-training-solutions/tree/{BRANCH}-core>`_ repository. It
|
||||
contains a version of the module created during the core training we can use as a starting
|
||||
point.
|
||||
|
||||
We were previously :ref:`introduced to QWeb <tutorials/getting_started/15_qwebintro>`
|
||||
in the Core Training where it was used to build a kanban view. Now we will expand on one of QWeb's
|
||||
other main uses: creating PDF reports. A common business requirement is the ability to create documents
|
||||
to send to customers and to use internally. These reports can be used to summarize and display
|
||||
information in an organized template to support the business in different ways. Odoo
|
||||
can additionally add our company's header and footer to our reports with minimal extra effort.
|
||||
|
||||
The documentation related to this topic can be found in :ref:`reference/qweb`,
|
||||
:ref:`reference/reports/report`, and the :ref:`reference/actions/report`
|
||||
section of the Actions reference.
|
||||
|
||||
File Structure
|
||||
==============
|
||||
|
||||
The bulk of a PDF report is its QWeb template. It also typically needs a corresponding
|
||||
``ir.actions.report`` to include the report within a module's business logic.
|
||||
There is no strict rule for the file names or where they are located, but these two parts are
|
||||
typically stored in 2 separate files within a ``report`` folder at the top level of your module's
|
||||
directory. If a module has many or multiple long report templates, then they are often organized
|
||||
logically across different files named after the report(s) they contain. All actions
|
||||
for the reports are usually stored in the same file ending with ``_reports.xml``, regardless of the
|
||||
number of reports it contains.
|
||||
|
||||
Therefore, it is expected that your work tree will look something like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
estate
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── report
|
||||
│ ├── estate_property_templates.xml
|
||||
│ └── estate_property_reports.xml
|
||||
├── security
|
||||
│ └── ir.model.access.csv
|
||||
├── views
|
||||
│ └── *.xml
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
Note that you will often see other non-QWeb and non-XML files containing "report" in their name also within
|
||||
the report folder. These are unrelated to the reports covered in this tutorial and are covered in
|
||||
:ref:`another advanced topic <tutorials/getting_started/K_dashboard>`. For now you can think of them
|
||||
as customized views that use direct SQL queries (sometimes referred to as SQL Views).
|
||||
|
||||
Don't forget to add whatever files your template and action view will be into to your ``__manifest__.py``.
|
||||
In this case, you will want to add the files to the ``data`` list and remember that the files listed in a manifest
|
||||
are loaded sequentially!
|
||||
|
||||
Basic Report
|
||||
============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will can print a report that displays all offers for a
|
||||
property.
|
||||
|
||||
.. image:: J_reports/simple_report.png
|
||||
:align: center
|
||||
:alt: Simple PDF report
|
||||
|
||||
In our real estate example there are many useful reports that we could create. One simple report we
|
||||
can create is one that displays all of a property's offers.
|
||||
|
||||
Report Data
|
||||
-----------
|
||||
|
||||
Before we do anything we first need some data to populate our reports or else this tutorial
|
||||
won't be very interesting. When creating reports, you will need some data to test your report code
|
||||
and check that the resulting look is as expected. It is a good idea to test with data that will cover most
|
||||
or all of your expected use cases. A good representation set for our simple report is:
|
||||
|
||||
* At least 3 properties where 1 is "sold", 1 is "offer received" and 1 is "new".
|
||||
* At least 2-3 offers for our "sold" and "offer received" properties
|
||||
|
||||
If you don't have a set of data like this already, you can either:
|
||||
|
||||
* Complete :ref:`tutorials/getting_started/C_data` (if you haven't done so already) and add the
|
||||
extra cases to your demo data (you may need to create a new database to load in the demo data).
|
||||
* Manually create the data in your database.
|
||||
* Copy this `data file
|
||||
<https://github.com/odoo/technical-training-solutions/blob/{BRANCH}-J_reports/estate/data/estate_demo.xml>`_
|
||||
into a new directory (data) in your estate module and copy `these lines
|
||||
<https://github.com/odoo/technical-training-solutions/blob/{BRANCH}-J_reports/estate/__manifest__.py#L21-L23>`_
|
||||
into your __manifest__.py file (you may need to create a new database to load in the demo data).
|
||||
|
||||
Before continuing, click through your data in your database and make sure your data is as expected.
|
||||
Of course you can add the data after you write your report code, but then you will not be able to
|
||||
incrementally test portions of your code as you write it. This can make checking for mistakes and
|
||||
debugging your code more difficult in the long run for complicated reports.
|
||||
|
||||
Minimal Template
|
||||
----------------
|
||||
|
||||
A minimal viable template is viewable under the "Minimal viable template" section of the
|
||||
:ref:`reference/reports/templates` documentation. We can modify this example to build
|
||||
our minimal property offers template file:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="report_property_offers">
|
||||
<t t-foreach="docs" t-as="property">
|
||||
<t t-call="web.html_container">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2>
|
||||
<span t-field="property.name"/>
|
||||
</h2>
|
||||
<div>
|
||||
<strong>Expected Price: </strong>
|
||||
<span t-field="property.expected_price"/>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-set="offers" t-value="property.mapped('offer_ids')"/>
|
||||
<tr t-foreach="offers" t-as="offer">
|
||||
<td>
|
||||
<span t-field="offer.price"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
Most of the Odoo specific (i.e. non-HTML) items in our file are explained in the minimal viable template section.
|
||||
Some additional features in our template are:
|
||||
|
||||
* The use of the ``class="table"`` attribute, so our table has some nice formatting. Twitter Bootstrap
|
||||
(we're using its table class in this case), and Font Awesome (useful for adding icons) classes can
|
||||
be used in your report template.
|
||||
* The use of ``t-set``, ``t-value``, ``t-foreach``, and ``t-as`` so that we can loop over all the ``offer_ids``.
|
||||
|
||||
If you are already familiar with website templating engines, then the QWeb directives (i.e. the `t-` commands)
|
||||
probably don't need much explanation and you can just look at its :ref:`documentation <reference/qweb>` and
|
||||
skip ahead to the next subsection.
|
||||
|
||||
Otherwise you are encouraged to read more about them (
|
||||
`Wikipedia <https://en.wikipedia.org/wiki/Template_processor>`__ has a good high level description), but
|
||||
the general idea is that QWeb provides the ability to dynamically generate web code based on Odoo data and
|
||||
simple commands. I.e. QWeb can access recordset data (and methods) and process simple programming operations
|
||||
such as setting and accessing temporary variables. For example, in the above example:
|
||||
|
||||
* ``t-set`` creates a temporary variable called "offers" that has its value set by ``t-value`` to the current
|
||||
``estate.property`` recordset's ``offer_ids``.
|
||||
* The ``t-foreach`` and ``t-as`` usage is the equivalent to the Python:
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
for offer in offers:
|
||||
|
||||
Report Action
|
||||
-------------
|
||||
|
||||
Now that we have a template, we need to make it accessible in our app via a ``ir.actions.report``.
|
||||
A practical example of ``ir.actions.report`` is
|
||||
`here <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/event/report/event_event_reports.xml#L20-L30>`__
|
||||
corresponding to
|
||||
`this template <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/event/report/event_event_templates.xml#L5>`__.
|
||||
Its contents are all explained in :ref:`the documentation <reference/actions/report>`.
|
||||
|
||||
An ``ir.actions.report`` is primarily used via the Print menu of a model's view. In the practical
|
||||
example, the ``binding_model_id`` specifies which model's views the report should show, and Odoo
|
||||
will auto-magically add it for you. Another common use case of the report action is to link it to
|
||||
a button as we learned in :ref:`tutorials/getting_started/10_actions`. This is handy for reports
|
||||
that only make sense under specific conditions. For example, if we wanted to make a "Final Sale"
|
||||
report, then we can link it to a "Print Sale Info" button that appears in the form view only when
|
||||
the property is "Sold".
|
||||
|
||||
.. image:: J_reports/print_menu.png
|
||||
:align: center
|
||||
:alt: Print Menu Button
|
||||
|
||||
You may have noticed or are wondered why our report template loops through a recordset. When our
|
||||
template is passed more than one record, it can produce one PDF report for all the records.
|
||||
Using the Print menu in the list view with multiple records selected will demonstrate this.
|
||||
|
||||
Make a Report
|
||||
-------------
|
||||
|
||||
Finally, you now know where to create your files and how the content of the files should look. Happy report making!
|
||||
|
||||
.. exercise:: Make a report.
|
||||
|
||||
- Add the property offers report from the minimal template subsection to the Print menu of the Property views.
|
||||
|
||||
- Improve the report by adding more data. Refer to the **Goal** of this section to see what additional
|
||||
data you can add and feel free to add even more.
|
||||
|
||||
- Bonus: Make an extra flexible report by adding in some logic so that when there are no offers on a property
|
||||
then we don't create a table and instead write something about how there are no offers yet. Hint: you will
|
||||
need to use ``t-if`` and ``t-else``.
|
||||
|
||||
Remember to check that your PDF reports match your data as expected.
|
||||
|
||||
|
||||
Sub-templates
|
||||
=============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will have a sub-template that we use in 2 reports.
|
||||
|
||||
.. image:: J_reports/report_subtemplate.png
|
||||
:align: center
|
||||
:alt: Report using a subtemplate
|
||||
|
||||
There are two main reasons for using sub-templates. One is to make the code easier to read when working with
|
||||
extra-long or complicated templates. The other is to reuse code where possible. Our simple property offers
|
||||
report is useful, but listing property offers information can be useful for more than just one report template.
|
||||
One example is a report that lists all of a salesman's properties' offers.
|
||||
|
||||
See if you can understand how to call a sub-template by reading the
|
||||
:ref:`documentation <reference/qweb/sub-templates>` on it and/or by looking at an
|
||||
`example <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/portal/static/src/xml/portal_chatter.xml#L147-L160>`__
|
||||
(remember QWeb uses the same control flows regardless if it is for a report or a view in Odoo.)
|
||||
|
||||
.. exercise:: Create and use a sub-template.
|
||||
|
||||
- Split the table portion of the offers into its own template. Remember to check that your
|
||||
original report still prints correctly afterwards.
|
||||
|
||||
- Add a new report for ``res.users`` that allows you to print all of the Real Estate Properties
|
||||
that are visible in their form view (i.e. in the "Settings" app). Include the offers for each
|
||||
of those saleman's properties in the same report. Hint: since the ``binding_model_id`` in this
|
||||
case will not be within the estate module, you will need to use ``ref="base.model_res_users"``.
|
||||
|
||||
Your end result should look similar to the image in the **Goal** of this section.
|
||||
|
||||
Remember to check that your reports match your data as expected!
|
||||
|
||||
Report Inheritance
|
||||
==================
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will inherit the property report in the ``estate_account``
|
||||
module.
|
||||
|
||||
.. image:: J_reports/inherited_report.png
|
||||
:align: center
|
||||
:alt: An inherited report
|
||||
|
||||
Inheritance in QWeb uses the same ``xpath`` elements as :ref:`views inheritance <reference/views/inheritance>`.
|
||||
A QWeb template refers to its parent template in a different way though. It is even easier to do by just adding
|
||||
the ``inherit_id`` attribute to the ``template`` element and setting it equal to the *module.parent_template_id*.
|
||||
|
||||
We didn't add any new fields to any of the estate models in `estate_account`, but we can still add information
|
||||
to our existing property report. For example, we know that any "Sold" properties will already have an invoice
|
||||
created for them, so we can add this information to our report.
|
||||
|
||||
.. exercise:: Inherit a report.
|
||||
|
||||
- Extend the property report to include some information about the invoice. You can look at the **Goal** of this
|
||||
section for inspiration (i.e. print a line when the property is Done, otherwise print nothing).
|
||||
|
||||
Again, remember to check that your reports match your data as expected!
|
||||
|
||||
Additional Features
|
||||
===================
|
||||
|
||||
All the following extra features are described further in the :ref:`reference/reports/report`
|
||||
documentation, including how to implement each of them.
|
||||
|
||||
Translations
|
||||
------------
|
||||
|
||||
We all know Odoo is used in multiple languages thanks to automated and manual translating. QWeb reports are no
|
||||
exception! Note that sometimes the translations do not work properly if there are unnecessary spaces in your
|
||||
template's text content, so try to avoid them when possible (especially leading spaces).
|
||||
|
||||
Reports are web pages
|
||||
---------------------
|
||||
|
||||
You probably are tired of hearing that QWeb creates HTML, but we're saying it again! One of the
|
||||
neat features of reports being written in QWeb is they can be viewed within the web browser.
|
||||
This can be useful if you want to embed a hyperlink that leads to a specific report. Note that
|
||||
the usual security checks will still apply to prevent unauthorized users from accessing the reports.
|
||||
|
||||
Barcodes
|
||||
--------
|
||||
|
||||
Odoo has a built-in barcode image creator that allows for barcodes to be embedded in your reports.
|
||||
Check out the corresponding
|
||||
`code <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/web/controllers/main.py#L2044-L2046>`__
|
||||
to see all the supported barcode types.
|
||||
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 46 KiB |
@@ -1,378 +0,0 @@
|
||||
.. _tutorials/getting_started/K_dashboard:
|
||||
|
||||
======================
|
||||
Advanced K: Dashboards
|
||||
======================
|
||||
|
||||
.. warning::
|
||||
|
||||
This tutorial assumes you have completed the :ref:`Core Training <tutorials/getting_started>`
|
||||
and have access to Odoo Enterprise features.
|
||||
|
||||
To follow the exercise, it is recommended that you fetch the branch
|
||||
{BRANCH}-core from the `technical training solutions
|
||||
<https://github.com/odoo/technical-training-solutions/tree/{BRANCH}-core>`_ repository. It
|
||||
contains a version of the module created during the core training we can use as a starting
|
||||
point.
|
||||
|
||||
The term "Dashboard" is used in Odoo for objects that display data, but involves different
|
||||
implementations. This tutorial will only focus on the Enterprise view that is used to provide
|
||||
aggregated data visualization. They can be added as a ``view_mode`` to an existing model (i.e. a
|
||||
view you can switch to via the view buttons in the top right of a view), but they are also often
|
||||
used as a view for to a special model customized for data visualization. You may hear these
|
||||
special views referred to as SQL views.
|
||||
|
||||
It is useful to note there is a "Dashboard" app in Odoo Community. This app allows users to create
|
||||
their own customized view of data, but the customization is only visible to each user and can
|
||||
only be viewed within the "Dashboard" app. Technically it is possible to make global dashboards
|
||||
using this ``board`` module, but it is much easier to do as an Enterprise view. Plus it looks nicer
|
||||
and has extra features not available in ``board``. Some other dashboards within Odoo also exist,
|
||||
but they are custom made and are beyond the scope of this tutorial.
|
||||
|
||||
The documentation related to this topic can be found in :ref:`reference/views/dashboard`.
|
||||
|
||||
File Structure
|
||||
==============
|
||||
|
||||
You probably have already guessed that since dashboard views are an Enterprise view, they must have
|
||||
a dependency on an Enterprise module. The Enterprise module is ``web_dashboard``. Don't forget to
|
||||
add it to your manifest file! It is standard to add dashboards intended to be used as a
|
||||
``view_mode`` for one of your module's models (in the ``model`` folder) to the views directory
|
||||
(i.e. the same file that contains the other views for the same model).
|
||||
|
||||
It is standard to create a separate Enterprise module to add extra Enterprise views and features to
|
||||
a Community module. This is done in a similar manner as the module link technique covered within
|
||||
:ref:`tutorials/getting_started/14_other_module`. The difference is that instead of linking 2
|
||||
different modules, we are extending our `estate` module. We do this by creating a new module and
|
||||
adding both the Community module and its necessary Enterprise module dependencies to its manifest.
|
||||
You will commonly see "enterprise" in the module's directory name. To keep this tutorial simple, we
|
||||
will add dashboards to our existing ``estate`` module.
|
||||
|
||||
SQL Views have 2 parts: their xml file (don't forget to add it to your manifest file) and their
|
||||
Python file (don't forget to add it to the appropriate ``__init.py__`` files). The former is the
|
||||
same format as the ``view_mode`` xml while the latter contains a custom model and SQL code to
|
||||
populate its fields. It is standard to add SQL view files to the ``report/`` directory. It
|
||||
is also common to include "report" in the name of the SQL view's files. You may be
|
||||
wondering why do we put the files in a report directory? We saw earlier that the dashboard is
|
||||
for data visualization, therefore it is not editable. You can think of dashboards as interactive
|
||||
reports where you can click on statistics, graphs, and charts to see the specific data contributing
|
||||
to them. Note it is also standard to store the xml code for
|
||||
:ref:`PDF report templates <tutorials/getting_started/J_reports>` in the report directory.
|
||||
|
||||
It is expected that your work tree will look something like:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
estate
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── report
|
||||
│ ├── __init__.py
|
||||
│ ├── estate_report.py
|
||||
│ └── estate_report_views.xml
|
||||
├── security
|
||||
│ └── ir.model.access.csv
|
||||
├── views
|
||||
│ ├── *.xml
|
||||
│ └── estate_property_views.xml
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
Dashboard View
|
||||
==============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will have a new dashboard view that displays
|
||||
different property statistics.
|
||||
|
||||
.. image:: K_dashboard/simple_dashboard.png
|
||||
:align: center
|
||||
:alt: Basic Dashboard view
|
||||
|
||||
Dashboards can display data in different ways, including:
|
||||
|
||||
* showing an ``aggregate`` of a field
|
||||
* using aggregated fields in a ``formula``
|
||||
* using a ``widget``
|
||||
* using another ``view`` as a subview
|
||||
|
||||
There are many useful statistics and visuals we can provide for our real estate example using
|
||||
these options. A full example to reference while doing the exercises in this section is
|
||||
`viewable here <https://github.com/odoo/enterprise/blob/6fd3244ae168dc73c348a9c1870796e89d8ef594/crm_enterprise/views/crm_lead_views.xml#L106-L133>`__
|
||||
(restricted github repository link).
|
||||
|
||||
Data
|
||||
----
|
||||
|
||||
To fully enjoy our dashboard view, we will need good test data to populate it. Test data will
|
||||
allow us to check that the resulting look and statistics are correct. It is a good idea to test
|
||||
with data that will cover most or all of your expected use cases, but is also easy to verify with
|
||||
that your statistics are correct. In our goal's case we are looking at count, sum, average,
|
||||
minimum, and maximum statistics, therefore a good representation set for our dashboard is:
|
||||
|
||||
* At least 3 properties with different property types, expected prices, and average living area.
|
||||
* At least 1 sold property and at least 1 canceled property
|
||||
|
||||
If you don't have a set of data like this already, you can either:
|
||||
|
||||
* Complete :ref:`tutorials/getting_started/C_data` (if you haven't done so already) and add the
|
||||
extra cases to your demo data (you may need to create a new database to load in the demo data).
|
||||
* Manually create the data in your database.
|
||||
* Copy this `data file
|
||||
<https://github.com/odoo/technical-training-solutions/blob/{BRANCH}-K_dashboard/estate/data/estate_demo.xml>`_
|
||||
into a new directory called ``data`` in your estate module and copy `these lines
|
||||
<https://github.com/odoo/technical-training-solutions/blob/{BRANCH}-K_dashboard/estate/__manifest__.py#L21-L23>`_
|
||||
into your __manifest__.py file (you may need to create a new database to load in the demo data).
|
||||
|
||||
Click through your database data and make sure it is what you expect. Of course you can add the
|
||||
data after you write your dashboard code and then test that your view is working as expected.
|
||||
|
||||
Aggregations
|
||||
------------
|
||||
|
||||
Building a dashboard view is very similar to what you have previously done in
|
||||
:ref:`tutorials/getting_started/07_basicviews`. For the dashboard view, we use the `dashboard` root
|
||||
element and choose from its possible tags (see all the possibilities and their attributes in the
|
||||
:ref:`reference/views/dashboard` documentation). So a simple dashboard example is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<dashboard>
|
||||
<group>
|
||||
<aggregate name="min_expected_price" string="Min Expected Price" field="expected_price"
|
||||
group_operator="min" help="Lowest expected price."/>
|
||||
</group>
|
||||
</dashboard>
|
||||
|
||||
In this example, ``<group>`` adds styling and ``<aggregate>`` declares an aggregation. We
|
||||
indicate which ``field`` we want to aggregate, what ``string`` to display with the value, and
|
||||
how to aggregate it with the `group_operator` attribute. The `group_operator` can use any valid
|
||||
PostgreSQL aggregate function plus the special Odoo defined ``count_distinct``.
|
||||
|
||||
Hopefully you remember how to add views to a window action `view_mode` (hint, it was
|
||||
covered in :ref:`tutorials/getting_started/06_firstui`). Now let's make some dashboards!
|
||||
|
||||
.. exercise:: Make a dashboard view.
|
||||
|
||||
- Create a dashboard of aggregated values for the ``estate.property`` model. You can
|
||||
look at the **Goal** of this section for some inspiration. Remember to check that your
|
||||
statistics are calculating as you expect and note that the calculated values take into
|
||||
consideration any applied view filters!
|
||||
|
||||
- Bonus: Add in some aggregations that need a `domain` to make sense (remember domains were
|
||||
also covered in :ref:`tutorials/getting_started/07_basicviews`).
|
||||
|
||||
Pie Charts
|
||||
----------
|
||||
|
||||
Adding pie charts to dashboards is a piece of cake using the `<widget>` element. An example is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<dashboard>
|
||||
<group>
|
||||
<widget name="pie_chart" title="Property Types" attrs="{'groupby': 'property_type_id'}"/>
|
||||
</group>
|
||||
</dashboard>
|
||||
|
||||
In this example, we indicate that we're using the `pie_chart` widget with the `name` attribute and
|
||||
the ``title`` for the pie chart, and that we're grouping it by property type.
|
||||
|
||||
.. exercise:: Add some pie charts.
|
||||
|
||||
- Add the pie charts from the **Goal** of this section to your dashboard. Hint: you will need
|
||||
to add `'measure': selling_price` to your pie chart `attrs` if you want to show selling
|
||||
prices grouped by property type.
|
||||
|
||||
- Hover over and click on the pie charts to check your charts counts values and don't forget
|
||||
that filters will also apply to the charts.
|
||||
|
||||
- Bonus: Add a domain to your selling price pie chart to only include "sold" properties (i.e.
|
||||
not "offer_accepted" ones). Note that the `'` will need to be escaped since it is declared
|
||||
as part of the `attrs`.
|
||||
|
||||
Subviews
|
||||
--------
|
||||
|
||||
Similar to how we can use the list view within the form view (we saw this automatically happen for
|
||||
One2many relationships in :ref:`tutorials/getting_started/08_relations`), we can add other views
|
||||
within our dashboard view. The most commonly added are the pivot and graph views, but the cohort
|
||||
view is also an option. These views are covered in more depth in
|
||||
:ref:`tutorials/getting_started/H_adv_views`. For this topic, you are only required to know their
|
||||
names. A dashboard with only subviews is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<dashboard>
|
||||
<view type="graph"/>
|
||||
<view type="pivot"/>
|
||||
</dashboard>
|
||||
|
||||
The `ref` attribute can be added to `<view>` elements to use a specific XML id for that view. If
|
||||
no XML id is provided for a graph or pivot view, then the default view will be used.
|
||||
The cohort view will not work in the dashboard without a specific XML id. If you have already
|
||||
created some of these views, then you are welcome to add them to your dashboard! Sample graph and
|
||||
pivot views are included in the `solution code
|
||||
<https://github.com/odoo/technical-training-solutions/blob/{BRANCH}-K_dashboard/estate/views/estate_property_views.xml#L169-L191>`_
|
||||
that you are welcome to use as well.
|
||||
|
||||
.. exercise:: Add subviews.
|
||||
|
||||
- Add in a graph and a pivot view to your dashboard. Try playing around with the layout of
|
||||
your subviews in relation to your pie charts and aggregated values and refer to the **Goal**
|
||||
of this section for an often used layout. Remember to check that your subviews are
|
||||
displaying your data as expected (and yes, they are also affected by the filters!).
|
||||
|
||||
SQL Views
|
||||
=========
|
||||
|
||||
.. warning::
|
||||
|
||||
This section expects you to have a basic knowledge of SQL. If you have little to no SQL
|
||||
knowledge then `this is a good tutorial to start with <https://selectstarsql.com/>`__
|
||||
and these `exercises <https://www.pgexercises.com/>`__ are good for those who need
|
||||
a refresher or extra practice.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will have a new SQL view that displays different
|
||||
property statistics.
|
||||
|
||||
.. image:: K_dashboard/report_dashboard.png
|
||||
:align: center
|
||||
:alt: SQL view
|
||||
|
||||
|
||||
Occasionally we want to show data that goes beyond what our model already has in it. We could add
|
||||
a lot of stored computed or related fields (non-stored fields cannot be aggregated
|
||||
or displayed in pie charts), but it would be impractical to store a bunch of fields only for this
|
||||
purpose. We can instead add a custom SQL view to minimize the computational load and keep our
|
||||
model clean of unnecessary fields.
|
||||
|
||||
Model
|
||||
-----
|
||||
|
||||
We will start with the more difficult part: our special report model. This file starts the same as
|
||||
any other model, except that we add 2 attributes ``_auto`` and ``_rec_name``::
|
||||
|
||||
from odoo import fields, models, tools
|
||||
|
||||
|
||||
class EstateReport(models.Model):
|
||||
_name = 'estate.report'
|
||||
_description = "Stock Report"
|
||||
_rec_name = 'id'
|
||||
_auto = False
|
||||
|
||||
``_auto = False`` indicates that we do not want to store the model in the database, and we will
|
||||
create a custom table by overriding the ``BaseModel.init()`` method. ``_rec_name`` indicates
|
||||
which of the model's fields represents a record's name (i.e. the name that will be used in the
|
||||
navigation breadcrumb when opening a record's form view). In this case, I left it as 'id' because
|
||||
our property offers don't have a name. We will need the `tools` import later (i.e.
|
||||
``odoo/odoo/tools``, which is full of all sorts of useful helper methods you will probably use in
|
||||
the future). Note that it is standard to include ``report`` in the model's name.
|
||||
|
||||
Remember, your new model will need to be added to your security file, as you learned in
|
||||
:ref:`tutorials/getting_started/05_securityintro`!
|
||||
|
||||
Then we define the fields we need for our dashboard the same way as any other model (like you
|
||||
learned in :ref:`tutorials/getting_started/04_basicmodel`), except that every field is
|
||||
``readonly=True``. After all, our model is for read-only purposes only.
|
||||
|
||||
Now we override the ``BaseModel.init()`` method mentioned earlier::
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||
self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
|
||||
SELECT
|
||||
%s
|
||||
FROM
|
||||
%s
|
||||
)""" % (self._table, self._select(), self._from()))
|
||||
|
||||
We use ``tools.drop_view_if_exists`` to ensure that we don't create a conflicting view and then
|
||||
execute the SQL query. It is standard to separate the different parts of the query to
|
||||
allow for easier model extension. Exactly how the query is split up across methods is not
|
||||
standardized, but you will often see at minimum ``_select`` and ``_from`` methods [or something
|
||||
similar], and of course all these methods will return strings. The columns from the SELECT
|
||||
will populate our model's fields, so ensure that your column names match your field names
|
||||
or use alias names that match.
|
||||
|
||||
.. exercise:: Create report model.
|
||||
|
||||
- Create a report model with the following fields:
|
||||
|
||||
========================= ========================= =========================
|
||||
Field Type Note
|
||||
========================= ========================= =========================
|
||||
id Integer Corresponds to ``id`` of ``estate.property.offer``
|
||||
offer_state Selection Equals ``state`` choices of ``estate.property.offer``
|
||||
property_id Many2one ``estate.property``
|
||||
property_state Selection Equals ``state`` choices of ``estate.property``
|
||||
property_type_id Many2one ``estate.property.type``
|
||||
========================= ========================= =========================
|
||||
|
||||
and write the SQL query necessary to populate the fields (hint, you will need 2 JOINs).
|
||||
|
||||
You won't be able to check if your model is correct until we create a view for it, but you are
|
||||
welcome to check your query directly in your database to see if the results are as you expect.
|
||||
If you struggle with this exercise, then
|
||||
`here is an example <https://github.com/odoo/odoo/blob/7417d8fc138b9de550bc631435bcc08628c29bed/addons/crm/report/crm_activity_report.py>`__
|
||||
to reference.
|
||||
|
||||
View
|
||||
----
|
||||
|
||||
Now that we have our model, we can make its dashboard view. There is no difference in how it's made,
|
||||
except that its file is in the ``report`` folder. Since it is a new model not linked to
|
||||
any other model, we will also have to add a new menuitem to view our dashboard. Typically, SQL views
|
||||
are added under a first-level menu called *Reporting* (because it's a report, surprise!). Do you
|
||||
remember how to add a ``menuitem``? If not, revisit :ref:`tutorials/getting_started/06_firstui`) again.
|
||||
|
||||
.. exercise:: Create report view.
|
||||
|
||||
- Recreate the dashboard in the **Goal** of this section. Hint: it uses the ``formula`` element,
|
||||
which we did not need for our previous dashboard.
|
||||
|
||||
- Bonus: Create ``list`` and ``form`` views for your new report model so we don't have to see the ugly
|
||||
defaults when you click on your pie charts.
|
||||
|
||||
Extra Tips
|
||||
----------
|
||||
|
||||
**Tip 1** A common mistake in SQL views is not considering the duplication of certain data
|
||||
due to table JOINs. For example, in our **Goal**, we have a pie chart of the offers' property types.
|
||||
We may be tempted to add a similar pie chart with a domain to only include canceled properties,
|
||||
so we think we are only counting the number of canceled properties by property type. In reality, we
|
||||
are still looking at all the offers per property, so any property with more than 1 offer will be
|
||||
counted per offer. This example is easily double-checked by clicking on the pie chart to see its
|
||||
list view:
|
||||
|
||||
.. image:: K_dashboard/report_list_detail.png
|
||||
:align: center
|
||||
:alt: Pie chart list view
|
||||
|
||||
But for cases such as average aggregations or using a subview such as the pivot view, it is easy to
|
||||
miss this mistake. It is also easy to miss this mistake when you have insufficient test data.
|
||||
To add a number of properties canceled by property type pie chart to this
|
||||
report, we would either have to do a hack (too advanced for this tutorial) or simply exclude it
|
||||
from this report.
|
||||
|
||||
**Tip 2** If you have a field that you do not want as a measure (i.e., in your pivot or
|
||||
graph views), then you can add ``store=False`` to it, and it will not show.
|
||||
|
||||
**Tip 3** If you have a SQL View that depends on context, then instead of overriding
|
||||
``BaseModel.init()`` set the ``_table_query`` property::
|
||||
|
||||
@property
|
||||
def _table_query(self):
|
||||
return 'SELECT %s FROM %s' % (self._select(), self._from())
|
||||
|
||||
The *select* and *from* methods remain the same.
|
||||
|
||||
`Here is an example <{GITHUB_PATH}/addons/account/report/account_invoice_report.py>`__
|
||||
of a report that depends on the currently selected companies (in a multi-company environment) context to
|
||||
determine the currency exchange rates to use for accurately displaying amounts when the selected companies
|
||||
have different currencies.
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 45 KiB |
@@ -1,9 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. _tutorials/getting_started/L_cron:
|
||||
|
||||
======================================
|
||||
Advanced L: Scheduled & Server Actions
|
||||
======================================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
||||
@@ -1,9 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. _tutorials/getting_started/M_migration:
|
||||
|
||||
======================
|
||||
Advanced M: Migrations
|
||||
======================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
||||
@@ -1,27 +0,0 @@
|
||||
.. _tutorials/getting_started/N_security:
|
||||
|
||||
====================
|
||||
Advanced N: Security
|
||||
====================
|
||||
|
||||
Every day we hear about data leaks and attacks in the newspapers.
|
||||
We expect you to be aware and careful about how to avoid security breaches in your future
|
||||
developments.
|
||||
|
||||
You already defined new models in your training. Did you check the access rights and define record
|
||||
rules on it ? Are you sure your module is not an open door to SQL injections ? Did you use getattr
|
||||
or t-raw in your code?
|
||||
|
||||
- No or wrong access rules on models.
|
||||
- Introduction of public methods that should actually be private.
|
||||
- Wrong manipulation of access tokens.
|
||||
- No or wrong `ir.rules` (restriction rules) on models.
|
||||
- Introduction of SQL injections.
|
||||
- etc.
|
||||
|
||||
Please check this `presentation from Olivier Dony (odo)
|
||||
<https://docs.google.com/presentation/d/1oDINxPtHWz31V8-2W0h2u2ubaKgz9lmbyfx9DJI4lTw/edit>`_ about
|
||||
common mistakes that you should **absolutely** avoid in your future developments.
|
||||
Also, please always use this `security checklist
|
||||
<https://docs.google.com/presentation/d/1oDINxPtHWz31V8-2W0h2u2ubaKgz9lmbyfx9DJI4lTw/edit#slide=id.g2faad955b1_0_6>`_
|
||||
to double-check your development and avoid further embarrassing issues.
|
||||
@@ -1,9 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. _tutorials/getting_started/O_perf:
|
||||
|
||||
========================
|
||||
Advanced O: Performances
|
||||
========================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
||||