[ADD] developer/howtos: R&D Training

The new R&D training is intended to replace the existing technical
training(s). It is organized as follow:

- A core training, with chapters to follow in order (1 - 16)
- Advanced topics, with independent chapters (A - O)

The advanced topics should be done after the core training.

Co-authored-by: Nicolas Martinelli <nim@odoo.com>
Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
Co-authored-by: wan <wan@odoo.com>
Co-authored-by: Xavier Morel <xmo@odoo.com>
Co-authored-by: Tiffany Chang (tic) <tic@odoo.com>
This commit is contained in:
Victor Feyens 2021-05-11 12:57:39 +02:00 committed by vfe-odoo
parent e9d06b7881
commit d0c2cb17bc
97 changed files with 6185 additions and 39 deletions

View File

@ -5,6 +5,7 @@ Tutorials
.. toctree::
:titlesonly:
howtos/rdtraining
howtos/themes
howtos/website
howtos/backend

View File

@ -0,0 +1,74 @@
:show-content:
.. _howto/rdtraining:
===============
Getting Started
===============
Welcome to the Getting Started Odoo tutorial! If you reached this page that means you are
interested in the development of your own Odoo module. It might also mean that you recently
joined the Odoo company for a rather technical position. In any case, your journey to the
technical side of Odoo starts here.
This training is split in two parts. The first part is the 'Core Training': its objective is to
give you an insight of the most important parts of the Odoo development framework.
The chapters should be followed in their given order since they cover the development of a new
Odoo application from scratch in an incremental way. In other words, each chapter depends on the
previous one.
The second part covers a set of 'Advanced' topics. Each topic can be followed independently, but
requires the 'Core Training'. Note that some advanced topics cover basic features
of an Odoo module, so it's a good idea to give them a quick look.
All topics are built around a business case we will enhance along the way. The reader is expected
to actively take part in the training by writing the solution for each exercise.
Ready? Let's get started!
Core Training
=============
.. toctree::
:caption: Advanced Topics
:titlesonly:
:glob:
rdtraining/0*
rdtraining/1*
* :doc:`rdtraining/01_architecture`
* :doc:`rdtraining/02_setup`
* :doc:`rdtraining/03_newapp`
* :doc:`rdtraining/04_basicmodel`
* :doc:`rdtraining/05_securityintro`
* :doc:`rdtraining/06_firstui`
* :doc:`rdtraining/07_basicviews`
* :doc:`rdtraining/08_relations`
* :doc:`rdtraining/09_compute_onchange`
* :doc:`rdtraining/10_actions`
* :doc:`rdtraining/11_constraints`
* :doc:`rdtraining/12_sprinkles`
* :doc:`rdtraining/13_inheritance`
* :doc:`rdtraining/14_other_module`
* :doc:`rdtraining/15_qwebintro`
* :doc:`rdtraining/16_guidelines_pr`
Advanced topics
===============
.. toctree::
:caption: Advanced Topics
:titlesonly:
rdtraining/B_acl_irrules
rdtraining/C_data
rdtraining/E_unittest
rdtraining/J_reports
rdtraining/K_dashboard
* :doc:`rdtraining/B_acl_irrules`
* :doc:`rdtraining/C_data`
* :doc:`rdtraining/E_unittest`
* :doc:`rdtraining/J_reports`
* :doc:`rdtraining/K_dashboard`

View File

@ -0,0 +1,131 @@
.. _howto/rdtraining/01_architecture:
================================
Chapter 1: Architecture Overview
================================
Multitier application
=====================
Odoo follows a `multitier architecture`_, meaning that the presentation, the business
logic and the data storage are separated. More specifically, it uses a three-tier architecture
(image from Wikipedia):
.. image:: 01_architecture/media/three_tier.svg
:align: center
:alt: Three-tier architecture
The presentation tier is a combination of HTML5, JavaScript and CSS. The logic tier is exclusively
written in Python, while the data tier only supports PostgreSQL as an RDBMS.
Depending on the scope of your module, Odoo development can be done in any of these tiers.
Therefore, before going any further, it may be a good idea to refresh your memory if you don't have
an intermediate level in these topics.
In order to go through this tutorial, you will need a very basic knowledge of HTML and an intermediate
level of Python. Advanced topics will require more knowledge in the other subjects. There are
plenty of tutorials freely accessible, so we cannot recommend one over another since it depends
on your background.
For reference this is the official `Python tutorial`_.
Odoo modules
============
Both server and client extensions are packaged as *modules* which are
optionally loaded in a *database*. A module is a collection of functions and data that target a
single purpose.
Odoo modules can either add brand new business logic to an Odoo system or
alter and extend existing business logic. One module can be created to add your
country's accounting rules to Odoo's generic accounting support, while
a different module can add support for real-time visualisation of a bus fleet.
Everything in Odoo starts and ends with modules.
Terminology: developers group their business features in Odoo *modules*. The main user-facing
modules are flagged and exposed as *Apps*, but a majority of the modules aren't Apps. *Modules*
may also be referred to as *addons* and the directories where the Odoo server finds them
form the ``addons_path``.
Composition of a module
-----------------------
An Odoo module **can** contain a number of elements:
:ref:`Business objects <reference/orm>`
A business object (e.g. an invoice) is declared as a Python class. The fields defined in
these classes are automatically mapped to database columns thanks to the
:abbr:`ORM (Object-Relational Mapping)` layer.
:ref:`Object views <reference/views>`
Define UI display
:ref:`Data files <reference/data>`
XML or CSV files declaring the model data:
* :ref:`views <reference/views>` or :ref:`reports <reference/reports>`,
* configuration data (modules parametrization, :ref:`security rules <reference/security>`),
* demonstration data
* and more
:ref:`Web controllers <reference/controllers>`
Handle requests from web browsers
Static web data
Images, CSS or JavaScript files used by the web interface or website
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 <howto/rdtraining/G_website>` and
:ref:`static web data <howto/rdtraining/I_jswidget>` are advanced topics.
Module structure
----------------
Each module is a directory within a *module directory*. Module directories
are specified by using the :option:`--addons-path <odoo-bin --addons-path>`
option.
An Odoo module is declared by its :ref:`manifest <reference/module/manifest>`.
When an Odoo module includes business objects (i.e. Python files), they are organized as a
`Python package <https://docs.python.org/3/tutorial/modules.html#packages>`_
with a ``__init__.py`` file. This file contains import instructions for various Python
files in the module.
Here is a simplified module directory:
.. code-block:: bash
module
├── models
│ ├── *.py
│ └── __init__.py
├── data
│ └── *.xml
├── __init__.py
└── __manifest__.py
Odoo Editions
=============
Odoo is available in `two versions`_: Odoo Enterprise (licensed & shared sources) and Odoo Community
(open-source). In addition to services such as support or upgrades, the Enterprise version provides extra
functionalities to Odoo. From a technical point-of-view, these functionalities are simply
new modules installed on top of the modules provided by the Community version.
Ready to start? Before writing actual code, let's go to the
:ref:`next chapter <howto/rdtraining/02_setup>` to review the Odoo installation process. Even if
Odoo is already running on your system, we strongly suggest you go through this chapter
to make sure we start on the same page during the development of our new application.
.. _multitier architecture:
https://en.wikipedia.org/wiki/Multitier_architecture
.. _Python tutorial:
https://docs.python.org/3.6/tutorial/
.. _two versions:
https://www.odoo.com/page/editions

View File

@ -0,0 +1,253 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="592.55548" height="530.32971" id="svg2" sodipodi:version="0.32" inkscape:version="0.46" sodipodi:docname="Overview_of_a_three-tier_application_vectorVersion.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" version="1.0">
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" gridtolerance="10000" guidetolerance="10" objecttolerance="10" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1" inkscape:cx="274.60069" inkscape:cy="311.51845" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="1680" inkscape:window-height="994" inkscape:window-x="12" inkscape:window-y="42" showguides="true" inkscape:guide-bbox="true"/>
<defs id="defs4">
<linearGradient id="linearGradient5132">
<stop id="stop5134" offset="0" style="stop-color: rgb(168, 173, 129); stop-opacity: 0.327434;"/>
<stop id="stop5136" offset="1" style="stop-color: rgb(0, 0, 0); stop-opacity: 0;"/>
</linearGradient>
<linearGradient id="linearGradient5120">
<stop style="stop-color: rgb(168, 173, 129); stop-opacity: 0.610619;" offset="0" id="stop5122"/>
<stop style="stop-color: rgb(0, 0, 0); stop-opacity: 0;" offset="1" id="stop5124"/>
</linearGradient>
<marker style="overflow: visible;" id="Arrow2Lend" refX="0" refY="0" orient="auto" inkscape:stockid="Arrow2Lend">
<path transform="matrix(-1.1, 0, 0, -1.1, -1.1, 0)" d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.97309,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z" style="font-size: 12px; fill-rule: evenodd; stroke-width: 0.625; stroke-linejoin: round;" id="path5691"/>
</marker>
<marker style="overflow: visible;" id="Arrow1Lend" refX="0" refY="0" orient="auto" inkscape:stockid="Arrow1Lend">
<path transform="matrix(-0.8, 0, 0, -0.8, -10, 0)" style="fill-rule: evenodd; stroke: rgb(0, 0, 0); stroke-width: 1pt; marker-start: none;" d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z" id="path5673"/>
</marker>
<marker style="overflow: visible;" id="Arrow1Lstart" refX="0" refY="0" orient="auto" inkscape:stockid="Arrow1Lstart">
<path transform="matrix(0.8, 0, 0, 0.8, 10, 0)" style="fill-rule: evenodd; stroke: rgb(0, 0, 0); stroke-width: 1pt; marker-start: none;" d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z" id="path5670"/>
</marker>
<linearGradient id="linearGradient3694">
<stop id="stop3696" offset="0" style="stop-color: rgb(250, 251, 230); stop-opacity: 1;"/>
<stop style="stop-color: rgb(225, 227, 187); stop-opacity: 0.74902;" offset="0.25" id="stop3795"/>
<stop style="stop-color: rgb(200, 203, 145); stop-opacity: 0.498039;" offset="0.5" id="stop3702"/>
<stop id="stop3698" offset="1" style="stop-color: rgb(250, 251, 230); stop-opacity: 0;"/>
</linearGradient>
<linearGradient id="linearGradient3375">
<stop id="stop3377" offset="0" style="stop-color: rgb(130, 131, 36); stop-opacity: 0.389381;"/>
<stop id="stop3379" offset="1" style="stop-color: rgb(0, 0, 0); stop-opacity: 0;"/>
</linearGradient>
<linearGradient id="linearGradient3367">
<stop id="stop3369" offset="0" style="stop-color: rgb(154, 154, 154); stop-opacity: 1;"/>
<stop id="stop3371" offset="1" style="stop-color: rgb(0, 0, 0); stop-opacity: 0;"/>
</linearGradient>
<inkscape:perspective id="perspective10" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" inkscape:vp_z="477.48915 : -25.408568 : 0" inkscape:vp_y="605.9884 : 795.47349 : 0" inkscape:vp_x="184.81274 : 367.45922 : 0" sodipodi:type="inkscape:persp3d"/>
<inkscape:perspective sodipodi:type="inkscape:persp3d" inkscape:vp_x="0 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="744.09448 : 526.18109 : 1" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" id="perspective2410"/>
<inkscape:perspective sodipodi:type="inkscape:persp3d" inkscape:vp_x="0 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="744.09448 : 526.18109 : 1" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" id="perspective2463"/>
<inkscape:perspective sodipodi:type="inkscape:persp3d" inkscape:vp_x="0 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="744.09448 : 526.18109 : 1" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" id="perspective2476"/>
<radialGradient r="10.66466" fy="547.66473" fx="105.4473" cy="547.66473" cx="105.4473" gradientUnits="userSpaceOnUse" id="radialGradient3397" xlink:href="#linearGradient3375" inkscape:collect="always"/>
<radialGradient r="4.4774756" fy="545.5434" fx="100.93949" cy="545.5434" cx="100.93949" gradientUnits="userSpaceOnUse" id="radialGradient3399" xlink:href="#linearGradient3367" inkscape:collect="always"/>
<radialGradient r="10.66466" fy="547.66473" fx="105.4473" cy="547.66473" cx="105.4473" gradientUnits="userSpaceOnUse" id="radialGradient3447" xlink:href="#linearGradient3375" inkscape:collect="always"/>
<radialGradient r="4.4774756" fy="545.5434" fx="100.93949" cy="545.5434" cx="100.93949" gradientUnits="userSpaceOnUse" id="radialGradient3449" xlink:href="#linearGradient3367" inkscape:collect="always"/>
<radialGradient r="4.4774756" fy="545.5434" fx="100.93949" cy="545.5434" cx="100.93949" gradientUnits="userSpaceOnUse" id="radialGradient3469" xlink:href="#linearGradient3367" inkscape:collect="always"/>
<radialGradient r="10.66466" fy="547.66473" fx="105.4473" cy="547.66473" cx="105.4473" gradientUnits="userSpaceOnUse" id="radialGradient3471" xlink:href="#linearGradient3375" inkscape:collect="always"/>
<radialGradient r="10.66466" fy="547.66473" fx="105.4473" cy="547.66473" cx="105.4473" gradientUnits="userSpaceOnUse" id="radialGradient3487" xlink:href="#linearGradient3375" inkscape:collect="always"/>
<radialGradient r="4.4774756" fy="545.5434" fx="100.93949" cy="545.5434" cx="100.93949" gradientUnits="userSpaceOnUse" id="radialGradient3489" xlink:href="#linearGradient3367" inkscape:collect="always"/>
<inkscape:perspective sodipodi:type="inkscape:persp3d" inkscape:vp_x="0 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="744.09448 : 526.18109 : 1" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" id="perspective3560"/>
<linearGradient spreadMethod="reflect" gradientUnits="userSpaceOnUse" y2="420.9158" x2="436.07776" y1="420.9158" x1="409.62192" id="linearGradient3700" xlink:href="#linearGradient3694" inkscape:collect="always"/>
<linearGradient spreadMethod="reflect" y2="420.9158" x2="436.07776" y1="420.9158" x1="409.62192" gradientTransform="matrix(-1, 0, 0, -1, 801.161, 848.543)" gradientUnits="userSpaceOnUse" id="linearGradient3793" xlink:href="#linearGradient3694" inkscape:collect="always"/>
<linearGradient y2="334.11847" x2="451.48767" y1="334.11847" x1="389.26227" spreadMethod="reflect" gradientUnits="userSpaceOnUse" id="linearGradient5289" xlink:href="#linearGradient3694" inkscape:collect="always"/>
<linearGradient gradientTransform="translate(103.75, -6.75)" y2="354.61218" x2="387.75" y1="354.61218" x1="323.75" spreadMethod="reflect" gradientUnits="userSpaceOnUse" id="linearGradient5309" xlink:href="#linearGradient3694" inkscape:collect="always"/>
<marker style="overflow: visible;" id="Arrow2Lends" refX="0" refY="0" orient="auto" inkscape:stockid="Arrow2Lends">
<path transform="matrix(-1.1, 0, 0, -1.1, -1.1, 0)" d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.97309,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z" style="font-size: 12px; fill: rgb(0, 0, 0); fill-rule: evenodd; stroke: rgb(0, 0, 0); stroke-width: 0.625; stroke-linejoin: round;" id="path8066"/>
</marker>
<pattern id="pattern2768" patternTransform="translate(135.53, 367.117)" height="124.591" width="471.03999" patternUnits="userSpaceOnUse">
<path style="fill: rgb(255, 73, 69); fill-opacity: 1; fill-rule: evenodd; stroke: black; stroke-width: 0.886228; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 0.44311386,124.14788 L 470.59687,0.44311386" id="path1876" inkscape:connector-type="polyline"/>
</pattern>
<inkscape:perspective id="perspective3896" inkscape:persp3d-origin="170.9816 : 44.63 : 1" inkscape:vp_z="341.9632 : 66.945 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_x="0 : 66.945 : 1" sodipodi:type="inkscape:persp3d"/>
<inkscape:perspective id="perspective5024" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" inkscape:vp_z="744.09448 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_x="0 : 526.18109 : 1" sodipodi:type="inkscape:persp3d"/>
<filter inkscape:collect="always" id="filter5116">
<feGaussianBlur inkscape:collect="always" stdDeviation="0.69889934" id="feGaussianBlur5118"/>
</filter>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3375" id="radialGradient5234" gradientUnits="userSpaceOnUse" cx="105.4473" cy="547.66473" fx="105.4473" fy="547.66473" r="10.66466"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3367" id="radialGradient5236" gradientUnits="userSpaceOnUse" cx="100.93949" cy="545.5434" fx="100.93949" fy="545.5434" r="4.4774756"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3375" id="radialGradient5238" gradientUnits="userSpaceOnUse" cx="105.4473" cy="547.66473" fx="105.4473" fy="547.66473" r="10.66466"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3367" id="radialGradient5240" gradientUnits="userSpaceOnUse" cx="100.93949" cy="545.5434" fx="100.93949" fy="545.5434" r="4.4774756"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3694" id="linearGradient5246" gradientUnits="userSpaceOnUse" spreadMethod="reflect" x1="323.75" y1="354.61218" x2="387.75" y2="354.61218" gradientTransform="translate(76.5051, 478.056)"/>
<inkscape:perspective id="perspective5297" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" inkscape:vp_z="744.09448 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_x="0 : 526.18109 : 1" sodipodi:type="inkscape:persp3d"/>
<inkscape:perspective id="perspective5310" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" inkscape:vp_z="744.09448 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_x="0 : 526.18109 : 1" sodipodi:type="inkscape:persp3d"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3694" id="linearGradient3261" gradientUnits="userSpaceOnUse" spreadMethod="reflect" x1="389.26227" y1="334.11847" x2="451.48767" y2="334.11847"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3694" id="linearGradient3263" gradientUnits="userSpaceOnUse" gradientTransform="translate(103.75, -6.75)" spreadMethod="reflect" x1="323.75" y1="354.61218" x2="387.75" y2="354.61218"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient5120" id="radialGradient3265" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.855164, 0.518357, -0.68892, 1.13655, 798.093, -526.435)" cx="725.97113" cy="332.63196" fx="725.97113" fy="332.63196" r="74.028908"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient5120" id="radialGradient3267" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.855164, 0.518357, -0.68892, 1.13655, 924.971, -531.625)" cx="725.97113" cy="332.63196" fx="725.97113" fy="332.63196" r="74.028908"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient5132" id="radialGradient3269" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.855164, 0.518357, -0.68892, 1.13655, 335.632, -479.206)" cx="725.97113" cy="332.63196" fx="725.97113" fy="332.63196" r="74.028908"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3375" id="radialGradient3271" gradientUnits="userSpaceOnUse" cx="105.4473" cy="547.66473" fx="105.4473" fy="547.66473" r="10.66466"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3367" id="radialGradient3273" gradientUnits="userSpaceOnUse" cx="100.93949" cy="545.5434" fx="100.93949" fy="545.5434" r="4.4774756"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3375" id="radialGradient3275" gradientUnits="userSpaceOnUse" cx="105.4473" cy="547.66473" fx="105.4473" fy="547.66473" r="10.66466"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3367" id="radialGradient3277" gradientUnits="userSpaceOnUse" cx="100.93949" cy="545.5434" fx="100.93949" fy="545.5434" r="4.4774756"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3375" id="radialGradient3279" gradientUnits="userSpaceOnUse" cx="105.4473" cy="547.66473" fx="105.4473" fy="547.66473" r="10.66466"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3367" id="radialGradient3281" gradientUnits="userSpaceOnUse" cx="100.93949" cy="545.5434" fx="100.93949" fy="545.5434" r="4.4774756"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3375" id="radialGradient3283" gradientUnits="userSpaceOnUse" cx="105.4473" cy="547.66473" fx="105.4473" fy="547.66473" r="10.66466"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3367" id="radialGradient3285" gradientUnits="userSpaceOnUse" cx="100.93949" cy="545.5434" fx="100.93949" fy="545.5434" r="4.4774756"/>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient5132" id="radialGradient3287" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.855164, 0.518357, -0.68892, 1.13655, 335.632, -479.206)" cx="725.97113" cy="332.63196" fx="725.97113" fy="332.63196" r="74.028908"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3694" id="linearGradient3656" gradientUnits="userSpaceOnUse" gradientTransform="translate(1069.93, 613.577)" spreadMethod="reflect" x1="323.75" y1="354.61218" x2="387.75" y2="354.61218"/>
</defs>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1" transform="translate(-1000, -522.032)">
<rect style="opacity: 1; fill: rgb(246, 246, 240); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" id="rect5770" width="592.55499" height="207.88939" x="1000" y="844.47278" rx="7.1512814" ry="3.4265683"/>
<rect style="opacity: 1; fill: rgb(237, 237, 224); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" id="rect5774" width="592.55499" height="165.56349" x="1000" y="672.89893" rx="7.1684713" ry="3.6468365"/>
<rect style="opacity: 1; fill: rgb(246, 246, 240); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" id="rect5768" width="592.55548" height="145.56349" x="1000" y="522.03247" rx="7.1171522" ry="3.2063"/>
<path style="fill: none; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 1434.4671,927.26494 C 1434.4671,897.74323 1434.2903,897.74323 1434.2903,897.74323 C 1487.1465,871.22673 1487.1465,871.22673 1487.1465,871.22673" id="path5758"/>
<path transform="translate(1005.2, 661.107)" d="M 451.48767,334.11847 A 31.112698,9.0156116 0 1 1 389.26227,334.11847 A 31.112698,9.0156116 0 1 1 451.48767,334.11847 z" sodipodi:ry="9.0156116" sodipodi:rx="31.112698" sodipodi:cy="334.11847" sodipodi:cx="420.37497" id="path5548" style="fill: rgb(253, 253, 253); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" sodipodi:type="arc"/>
<rect ry="3.4265683" rx="7.1171522" y="938.68878" x="1394.1827" height="59" width="63" id="rect5550" style="fill: url(#linearGradient3656) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;"/>
<path transform="translate(1005.59, 606.859)" d="M 451.48767,334.11847 A 31.112698,9.0156116 0 1 1 389.26227,334.11847 A 31.112698,9.0156116 0 1 1 451.48767,334.11847 z" sodipodi:ry="9.0156116" sodipodi:rx="31.112698" sodipodi:cy="334.11847" sodipodi:cx="420.37497" id="path5552" style="fill: rgb(253, 253, 253); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" sodipodi:type="arc"/>
<path transform="matrix(0.991965, 0, 0, 0.991965, 1008.69, 662.755)" d="M 451.48767,334.11847 A 31.112698,9.0156116 0 1 1 389.26227,334.11847 A 31.112698,9.0156116 0 1 1 451.48767,334.11847 z" sodipodi:ry="9.0156116" sodipodi:rx="31.112698" sodipodi:cy="334.11847" sodipodi:cx="420.37497" id="path5554" style="fill: rgb(253, 253, 253); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" sodipodi:type="arc"/>
<path transform="matrix(0.991965, 0, 0, 0.991965, 1008.44, 662.755)" d="M 451.48767,334.11847 A 31.112698,9.0156116 0 1 1 389.26227,334.11847 A 31.112698,9.0156116 0 1 1 451.48767,334.11847 z" sodipodi:ry="9.0156116" sodipodi:rx="31.112698" sodipodi:cy="334.11847" sodipodi:cx="420.37497" id="path5556" style="fill: url(#linearGradient3261) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" sodipodi:type="arc"/>
<g id="g5558" transform="translate(1072.85, 613.702)">
<rect style="opacity: 1; fill: url(#linearGradient3263) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5560" width="58.5" height="33" x="432.5" y="344.36218" rx="0.52616197" ry="3.4265683"/>
<path style="fill: rgb(253, 253, 253); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(145, 145, 124); stroke-width: 0.970822px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;" d="M 433.1835,344.1737 C 436.00677,336.63379 438.12809,336.81226 438.12809,336.81226 L 486.97826,336.95258 C 489.15613,337.1119 489.06591,341.96918 490.36369,344.32875 L 433.1835,344.1737 z" id="path5562" sodipodi:nodetypes="ccccc"/>
<rect style="opacity: 1; fill: rgb(78, 219, 55); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5564" width="8" height="5" x="439" y="367.86218" rx="0.52616197" ry="3.4265683"/>
</g>
<g id="g5582" transform="translate(894.433, 518.95)">
<path style="fill: none; fill-rule: evenodd; stroke: rgb(134, 134, 134); stroke-width: 2.3; stroke-linecap: butt; stroke-linejoin: miter; marker-start: none; marker-end: none; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 564.36816,457.7548 C 605.49316,457.7548 605.49316,457.7548 605.49316,457.7548" id="path5584"/>
<path sodipodi:nodetypes="ccccc" id="path5586" d="M 608.57301,457.98055 L 597.97215,461.85992 L 599.33893,457.71598 L 598.01712,453.65159 L 608.57301,457.98055 z" style="fill: rgb(134, 134, 134); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(134, 134, 134); stroke-width: 0.276426px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;"/>
</g>
<g id="g5588" transform="matrix(-1, 0, 0, -1, 2067.91, 1425.91)">
<path style="fill: none; fill-rule: evenodd; stroke: rgb(134, 134, 134); stroke-width: 2.3; stroke-linecap: butt; stroke-linejoin: miter; marker-start: none; marker-end: none; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 564.36816,457.7548 C 605.49316,457.7548 605.49316,457.7548 605.49316,457.7548" id="path5590"/>
<path sodipodi:nodetypes="ccccc" id="path5592" d="M 608.57301,457.98055 L 597.97215,461.85992 L 599.33893,457.71598 L 598.01712,453.65159 L 608.57301,457.98055 z" style="fill: rgb(134, 134, 134); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(134, 134, 134); stroke-width: 0.276426px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;"/>
</g>
<path style="fill: rgb(255, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 0.307256px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;" d="M 1414.9178,926.08546 L 1419.2299,914.30229 L 1414.6237,915.8215 L 1410.1061,914.35228 L 1414.9178,926.08546 z" id="path5742" sodipodi:nodetypes="ccccc"/>
<text xml:space="preserve" style="font-size: 14px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1390.14" y="1020.54" id="text3009"><tspan y="1020.54" x="1390.14" sodipodi:role="line" id="tspan3011"><tspan x="1390.14" y="1020.54" id="tspan3013">Database</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 14px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1505.38" y="1007.08" id="text3015"><tspan y="1007.08" x="1505.38" sodipodi:role="line" id="tspan3017"><tspan x="1505.38" y="1007.08" id="tspan3019">Storage</tspan></tspan></text>
<path style="fill: none; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 1344.5761,787.61135 C 1344.7529,878.47458 1344.7529,878.47458 1344.7529,878.47458" id="path5750"/>
<path style="fill: none; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 1504.5591,785.84358 C 1504.7359,876.70681 1504.7359,876.70681 1504.7359,876.70681" id="path5748"/>
<path style="fill: url(#radialGradient3265) rgb(0, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(104, 105, 48); stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: round; stroke-opacity: 1; filter: url(#filter5116);" d="M 1105.2895,149.56928 C 1104.94,183.08983 1106.2895,282.56928 1106.2895,282.56928 L 1106.2895,282.56928 C 1252.2895,282.56928 1252.2895,282.56928 1252.2895,282.56928 C 1252.2895,149.56928 1251.7895,149.56928 1251.7895,149.56928 C 1235.2895,135.06928 1227.7895,136.06928 1227.7895,136.06928 C 1133.7895,134.06928 1131.2895,136.56928 1131.2895,136.56928 C 1131.2895,136.56928 1105.3804,140.84909 1105.2895,149.56928 z" id="path5034" sodipodi:nodetypes="cccccccs" transform="matrix(0.660912, 0, 0, 0.660912, 567.035, 450.586)"/>
<rect style="fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(47, 47, 47); stroke-width: 1.5201; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" id="rect5030" width="77.987648" height="62.786667" x="1306.9241" y="551.53455" rx="4.7038136" ry="2.2646611"/>
<path style="fill: url(#radialGradient3267) rgb(0, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(104, 105, 48); stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: round; stroke-opacity: 1; filter: url(#filter5116);" d="M 1232.1672,144.37863 C 1231.8177,177.89918 1233.1672,277.37863 1233.1672,277.37863 L 1233.1672,277.37863 C 1379.1672,277.37863 1379.1672,277.37863 1379.1672,277.37863 C 1379.1672,144.37863 1378.6672,144.37863 1378.6672,144.37863 C 1362.1672,129.87862 1354.6672,130.87862 1354.6672,130.87862 C 1260.6672,128.87862 1258.1672,131.37862 1258.1672,131.37862 C 1258.1672,131.37862 1232.2582,135.65843 1232.1672,144.37863 z" id="path5148" sodipodi:nodetypes="cccccccs" transform="matrix(0.670464, 0, 0, 0.670464, 629.406, 452.538)"/>
<rect style="fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(47, 47, 47); stroke-width: 1.54207; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" id="rect5150" width="79.114761" height="63.694084" x="1465.0555" y="551.46503" rx="4.7717948" ry="2.2973909"/>
<path style="fill: url(#radialGradient3269) rgb(0, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: none; stroke-width: 1.0007; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1; filter: url(#filter5116);" d="M 642.82808,196.7977 C 642.47856,230.31825 643.82808,329.7977 643.82808,329.7977 L 643.82808,329.7977 C 789.82808,329.7977 789.82808,329.7977 789.82808,329.7977 C 789.82808,196.7977 789.32808,196.7977 789.32808,196.7977 C 772.82808,182.2977 765.32808,183.2977 765.32808,183.2977 C 671.32808,181.2977 668.82808,183.7977 668.82808,183.7977 C 668.82808,183.7977 642.91901,188.07751 642.82808,196.7977 z" id="path5152" sodipodi:nodetypes="cccccccs" transform="matrix(0.420735, 0, 0, 0.411447, 1212.42, 479.614)"/>
<path style="fill: none; fill-rule: evenodd; stroke: rgb(255, 255, 255); stroke-width: 0.636053px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;" d="M 1471.0818,589.82652 C 1538.6654,589.82652 1538.6654,589.45689 1538.6654,589.45689" id="path5182"/>
<g id="g5316" transform="matrix(0.536791, 0.0577592, -0.0577592, 0.536791, 1208.09, 505.119)">
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5318" width="7.230413" height="72.96875" x="65.939308" y="460.66718" rx="1.6370747" ry="3.6790967" transform="matrix(0.943099, -0.332513, 0.332513, 0.943099, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5320" width="7.230413" height="72.96875" x="-501.26709" y="32.945621" rx="1.6370747" ry="3.6790967" transform="matrix(-0.332513, -0.943099, 0.943099, -0.332513, 0, 0)" inkscape:transform-center-x="-22.797407" inkscape:transform-center-y="-26.781993"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5322" width="7.230413" height="72.96875" x="-304.91528" y="364.952" rx="1.6370747" ry="3.6790967" transform="matrix(0.435242, -0.900314, 0.900314, 0.435242, 0, 0)" inkscape:transform-center-x="-0.054580855" inkscape:transform-center-y="-0.11405819"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5324" width="7.230413" height="72.96875" x="-405.55185" y="-337.909" rx="1.6370747" ry="3.6790967" transform="matrix(-0.900314, -0.435242, 0.435242, -0.900314, 0, 0)" inkscape:transform-center-x="-0.45083121" inkscape:transform-center-y="0.21580039"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5326" width="7.2304077" height="72.96875" x="-129.78862" y="448.93118" rx="1.6370734" ry="3.6790941" transform="matrix(0.744062, -0.66811, 0.66811, 0.744062, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5328" width="7.2304068" height="72.96875" x="-488.87778" y="-162.5117" rx="1.6370732" ry="3.6790936" transform="matrix(-0.66811, -0.744062, 0.744062, -0.66811, 0, 0)" inkscape:transform-center-x="-10.812994" inkscape:transform-center-y="-33.467625"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5330" width="7.2304125" height="72.96875" x="-434.65988" y="219.1375" rx="1.6370746" ry="3.6790965" transform="matrix(0.0575757, -0.998341, 0.998341, 0.0575757, 0, 0)" inkscape:transform-center-x="-0.0067134245" inkscape:transform-center-y="-0.12624831"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5332" width="7.2304125" height="72.96875" x="-260.01053" y="-468.30582" rx="1.6370746" ry="3.6790965" transform="matrix(-0.998341, -0.0575757, 0.0575757, -0.998341, 0, 0)" inkscape:transform-center-y="0.026899316" inkscape:transform-center-x="-0.49908664"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.912671; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5334" width="7.474649" height="43.927952" x="-377.0932" y="-400.33444" rx="1.6923734" ry="2.2148545" transform="matrix(-0.925212, -0.379451, 0.379451, -0.925212, 0, 0)" inkscape:transform-center-x="-4.2716058" inkscape:transform-center-y="-7.6715504"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.912671; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5336" width="7.474649" height="43.927952" x="-382.27505" y="351.12729" rx="1.6923734" ry="2.2148545" transform="matrix(0.379451, -0.925212, 0.925212, 0.379451, 0, 0)" inkscape:transform-center-x="-7.671692" inkscape:transform-center-y="4.2715427"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.912671; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5338" width="7.474649" height="43.927948" x="-535.38159" y="-25.514301" rx="1.6923734" ry="2.2148545" transform="matrix(-0.385911, -0.922536, 0.922536, -0.385911, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.912671; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5340" width="7.474649" height="43.927948" x="-7.163105" y="509.94055" rx="1.6923734" ry="2.2148545" transform="matrix(0.922536, -0.385911, 0.385911, 0.922536, 0, 0)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5342" sodipodi:cx="116.31907" sodipodi:cy="545.80859" sodipodi:rx="14.495689" sodipodi:ry="14.495689" d="M 130.81476,545.80859 A 14.495689,14.495689 0 1 1 101.82338,545.80859 A 14.495689,14.495689 0 1 1 130.81476,545.80859 z" transform="matrix(0.960398, -0.401749, 0.401749, 0.960398, -129.006, 14.0137)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: url(#radialGradient3271) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(190, 190, 179); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5344" sodipodi:cx="105.4473" sodipodi:cy="547.66473" sodipodi:rx="10.16466" sodipodi:ry="10.16466" d="M 115.61196,547.66473 A 10.16466,10.16466 0 1 1 95.282643,547.66473 A 10.16466,10.16466 0 1 1 115.61196,547.66473 z" transform="matrix(0.960398, -0.401749, 0.401749, 0.960398, -119.396, 7.89883)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: url(#radialGradient3273) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5346" sodipodi:cx="100.93949" sodipodi:cy="545.5434" sodipodi:rx="3.9774756" sodipodi:ry="3.9774756" d="M 104.91697,545.5434 A 3.9774756,3.9774756 0 1 1 96.962016,545.5434 A 3.9774756,3.9774756 0 1 1 104.91697,545.5434 z" transform="matrix(0.960398, -0.401749, 0.401749, 0.960398, -114.285, 7.95537)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.519676; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5348" sodipodi:cx="116.31907" sodipodi:cy="545.80859" sodipodi:rx="14.495689" sodipodi:ry="14.495689" d="M 130.81476,545.80859 A 14.495689,14.495689 0 1 1 101.82338,545.80859 A 14.495689,14.495689 0 1 1 130.81476,545.80859 z" transform="matrix(1.96359, -0.692314, 0.692314, 1.96359, -375.858, -545.202)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: url(#radialGradient3275) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(190, 190, 179); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5350" sodipodi:cx="105.4473" sodipodi:cy="547.66473" sodipodi:rx="10.16466" sodipodi:ry="10.16466" d="M 115.61196,547.66473 A 10.16466,10.16466 0 1 1 95.282643,547.66473 A 10.16466,10.16466 0 1 1 115.61196,547.66473 z" transform="matrix(1.55459, -0.548111, 0.548111, 1.55459, -233.943, -347.364)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: url(#radialGradient3277) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5352" sodipodi:cx="100.93949" sodipodi:cy="545.5434" sodipodi:rx="3.9774756" sodipodi:ry="3.9774756" d="M 104.91697,545.5434 A 3.9774756,3.9774756 0 1 1 96.962016,545.5434 A 3.9774756,3.9774756 0 1 1 104.91697,545.5434 z" transform="matrix(1.30537, -0.460241, 0.460241, 1.30537, -152.884, -220.026)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5354" width="7.1152625" height="4.7729135" x="65.982826" y="465.99457" rx="1.6576668" ry="2.3864567" transform="matrix(0.943099, -0.332513, 0.332513, 0.943099, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5356" width="7.1152625" height="4.7729135" x="65.960732" y="523.66797" rx="1.6576668" ry="2.3864567" transform="matrix(0.943099, -0.332513, 0.332513, 0.943099, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5358" width="7.1152625" height="4.7729135" x="-501.19525" y="95.592987" rx="1.6576668" ry="2.3864567" transform="matrix(-0.332513, -0.943099, 0.943099, -0.332513, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5360" width="7.1152625" height="4.7729135" x="-501.19519" y="37.786999" rx="1.6576668" ry="2.3864567" transform="matrix(-0.332513, -0.943099, 0.943099, -0.332513, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5362" width="7.1152625" height="4.7729135" x="-306.59344" y="427.05988" rx="1.6576668" ry="2.3864567" transform="matrix(0.431749, -0.901994, 0.901994, 0.431749, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5364" width="7.1152625" height="4.7729135" x="-306.31866" y="369.14117" rx="1.6576668" ry="2.3864567" transform="matrix(0.431749, -0.901994, 0.901994, 0.431749, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5366" width="7.1152625" height="4.7729135" x="-404.36612" y="-276.25415" rx="1.6576668" ry="2.3864567" transform="matrix(-0.901994, -0.431749, 0.431749, -0.901994, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5368" width="7.1152625" height="4.7729135" x="-404.18933" y="-334.01593" rx="1.6576668" ry="2.3864567" transform="matrix(-0.901994, -0.431749, 0.431749, -0.901994, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5370" width="7.1152616" height="4.7729135" x="-129.78484" y="454.45309" rx="1.6576666" ry="2.3864567" transform="matrix(0.744062, -0.66811, 0.66811, 0.744062, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5372" width="7.1152616" height="4.7729135" x="-129.78716" y="512.4054" rx="1.6576666" ry="2.3864567" transform="matrix(0.744062, -0.66811, 0.66811, 0.744062, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5374" width="7.1152616" height="4.7729135" x="-488.91299" y="-100.00322" rx="1.6576666" ry="2.3864567" transform="matrix(-0.66811, -0.744062, 0.744062, -0.66811, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5376" width="7.1152616" height="4.7729135" x="-488.91293" y="-157.80919" rx="1.6576666" ry="2.3864567" transform="matrix(-0.66811, -0.744062, 0.744062, -0.66811, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5378" width="7.1152616" height="4.7729135" x="-435.77966" y="280.63101" rx="1.6576666" ry="2.3864567" transform="matrix(0.0537059, -0.998557, 0.998557, 0.0537059, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5380" width="7.1152616" height="4.7729135" x="-435.53876" y="222.79398" rx="1.6576666" ry="2.3864567" transform="matrix(0.0537059, -0.998557, 0.998557, 0.0537059, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5382" width="7.1152616" height="4.7729135" x="-258.33395" y="-406.56134" rx="1.6576666" ry="2.3864567" transform="matrix(-0.998557, -0.0537059, 0.0537059, -0.998557, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5384" width="7.1152616" height="4.7729135" x="-258.15717" y="-464.32318" rx="1.6576666" ry="2.3864567" transform="matrix(-0.998557, -0.0537059, 0.0537059, -0.998557, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5386" width="6.75" height="3.4375" x="390.19818" y="340.07739" rx="0.47016668" ry="1.71875" transform="matrix(0.901994, 0.431749, -0.431749, 0.901994, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5388" width="6.7500005" height="3.4375002" x="373.94806" y="386.9249" rx="0.47016671" ry="1.7187501" transform="matrix(0.921316, 0.388814, -0.388814, 0.921316, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5390" width="6.7500005" height="3.4375002" x="361.05246" y="-402.72012" rx="0.47016671" ry="1.7187501" transform="matrix(-0.412213, 0.911088, -0.911088, -0.412213, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5392" width="6.7500005" height="3.4375002" x="393.76926" y="-339.68652" rx="0.47016671" ry="1.7187501" transform="matrix(-0.329492, 0.944158, -0.944158, -0.329492, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5394" width="6.7500005" height="3.4375002" x="527.9704" y="-20.811844" rx="0.47016671" ry="1.7187501" transform="matrix(0.372629, 0.92798, -0.92798, 0.372629, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5397" width="6.7500005" height="3.4375002" x="528.52869" y="8.4513111" rx="0.47016671" ry="1.7187501" transform="matrix(0.372629, 0.92798, -0.92798, 0.372629, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5399" width="6.75" height="3.4375" x="36.094131" y="-545.8858" rx="0.47016668" ry="1.71875" transform="matrix(-0.895012, 0.446042, -0.446042, -0.895012, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5401" width="6.75" height="3.4375" x="34.23085" y="-517.57141" rx="0.47016668" ry="1.71875" transform="matrix(-0.895012, 0.446042, -0.446042, -0.895012, 0, 0)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: none; fill-opacity: 1; fill-rule: nonzero; stroke: rgb(239, 239, 235); stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 0.418269;" id="path5403" sodipodi:cx="171.87114" sodipodi:cy="489.99133" sodipodi:rx="29.74268" sodipodi:ry="29.74268" d="M 201.61382,489.99133 A 29.74268,29.74268 0 1 1 142.12846,489.99133 A 29.74268,29.74268 0 1 1 201.61382,489.99133 z" transform="matrix(0.901994, 0.431749, -0.431749, 0.901994, 287.033, -70.2209)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: none; fill-opacity: 1; fill-rule: nonzero; stroke: rgb(239, 239, 235); stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 0.418269;" id="path5405" sodipodi:cx="152.11635" sodipodi:cy="538.56073" sodipodi:rx="14.407301" sodipodi:ry="14.407301" d="M 166.52365,538.56073 A 14.407301,14.407301 0 1 1 137.70905,538.56073 A 14.407301,14.407301 0 1 1 166.52365,538.56073 z" transform="matrix(0.901994, 0.431749, -0.431749, 0.901994, 297.439, -60.2057)"/>
</g>
<g id="g5415" transform="matrix(0.536791, 0.0577592, -0.0577592, 0.536791, 1368.72, 499.781)">
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5418" width="7.230413" height="72.96875" x="65.939308" y="460.66718" rx="1.6370747" ry="3.6790967" transform="matrix(0.943099, -0.332513, 0.332513, 0.943099, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5420" width="7.230413" height="72.96875" x="-501.26709" y="32.945621" rx="1.6370747" ry="3.6790967" transform="matrix(-0.332513, -0.943099, 0.943099, -0.332513, 0, 0)" inkscape:transform-center-x="-22.797407" inkscape:transform-center-y="-26.781993"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5422" width="7.230413" height="72.96875" x="-304.91528" y="364.952" rx="1.6370747" ry="3.6790967" transform="matrix(0.435242, -0.900314, 0.900314, 0.435242, 0, 0)" inkscape:transform-center-x="-0.054580855" inkscape:transform-center-y="-0.11405819"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5424" width="7.230413" height="72.96875" x="-405.55185" y="-337.909" rx="1.6370747" ry="3.6790967" transform="matrix(-0.900314, -0.435242, 0.435242, -0.900314, 0, 0)" inkscape:transform-center-x="-0.45083121" inkscape:transform-center-y="0.21580039"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5426" width="7.2304077" height="72.96875" x="-129.78862" y="448.93118" rx="1.6370734" ry="3.6790941" transform="matrix(0.744062, -0.66811, 0.66811, 0.744062, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5428" width="7.2304068" height="72.96875" x="-488.87778" y="-162.5117" rx="1.6370732" ry="3.6790936" transform="matrix(-0.66811, -0.744062, 0.744062, -0.66811, 0, 0)" inkscape:transform-center-x="-10.812994" inkscape:transform-center-y="-33.467625"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5430" width="7.2304125" height="72.96875" x="-434.65988" y="219.1375" rx="1.6370746" ry="3.6790965" transform="matrix(0.0575757, -0.998341, 0.998341, 0.0575757, 0, 0)" inkscape:transform-center-x="-0.0067134245" inkscape:transform-center-y="-0.12624831"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1.15691; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5432" width="7.2304125" height="72.96875" x="-260.01053" y="-468.30582" rx="1.6370746" ry="3.6790965" transform="matrix(-0.998341, -0.0575757, 0.0575757, -0.998341, 0, 0)" inkscape:transform-center-y="0.026899316" inkscape:transform-center-x="-0.49908664"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.912671; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5434" width="7.474649" height="43.927952" x="-377.0932" y="-400.33444" rx="1.6923734" ry="2.2148545" transform="matrix(-0.925212, -0.379451, 0.379451, -0.925212, 0, 0)" inkscape:transform-center-x="-4.2716058" inkscape:transform-center-y="-7.6715504"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.912671; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5436" width="7.474649" height="43.927952" x="-382.27505" y="351.12729" rx="1.6923734" ry="2.2148545" transform="matrix(0.379451, -0.925212, 0.925212, 0.379451, 0, 0)" inkscape:transform-center-x="-7.671692" inkscape:transform-center-y="4.2715427"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.912671; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5438" width="7.474649" height="43.927948" x="-535.38159" y="-25.514301" rx="1.6923734" ry="2.2148545" transform="matrix(-0.385911, -0.922536, 0.922536, -0.385911, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.912671; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5440" width="7.474649" height="43.927948" x="-7.163105" y="509.94055" rx="1.6923734" ry="2.2148545" transform="matrix(0.922536, -0.385911, 0.385911, 0.922536, 0, 0)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5442" sodipodi:cx="116.31907" sodipodi:cy="545.80859" sodipodi:rx="14.495689" sodipodi:ry="14.495689" d="M 130.81476,545.80859 A 14.495689,14.495689 0 1 1 101.82338,545.80859 A 14.495689,14.495689 0 1 1 130.81476,545.80859 z" transform="matrix(0.960398, -0.401749, 0.401749, 0.960398, -129.006, 14.0137)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: url(#radialGradient3279) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(190, 190, 179); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5444" sodipodi:cx="105.4473" sodipodi:cy="547.66473" sodipodi:rx="10.16466" sodipodi:ry="10.16466" d="M 115.61196,547.66473 A 10.16466,10.16466 0 1 1 95.282643,547.66473 A 10.16466,10.16466 0 1 1 115.61196,547.66473 z" transform="matrix(0.960398, -0.401749, 0.401749, 0.960398, -119.396, 7.89883)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: url(#radialGradient3281) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5446" sodipodi:cx="100.93949" sodipodi:cy="545.5434" sodipodi:rx="3.9774756" sodipodi:ry="3.9774756" d="M 104.91697,545.5434 A 3.9774756,3.9774756 0 1 1 96.962016,545.5434 A 3.9774756,3.9774756 0 1 1 104.91697,545.5434 z" transform="matrix(0.960398, -0.401749, 0.401749, 0.960398, -114.285, 7.95537)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(156, 156, 142); stroke-width: 0.519676; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5448" sodipodi:cx="116.31907" sodipodi:cy="545.80859" sodipodi:rx="14.495689" sodipodi:ry="14.495689" d="M 130.81476,545.80859 A 14.495689,14.495689 0 1 1 101.82338,545.80859 A 14.495689,14.495689 0 1 1 130.81476,545.80859 z" transform="matrix(1.96359, -0.692314, 0.692314, 1.96359, -375.858, -545.202)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: url(#radialGradient3283) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(190, 190, 179); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5450" sodipodi:cx="105.4473" sodipodi:cy="547.66473" sodipodi:rx="10.16466" sodipodi:ry="10.16466" d="M 115.61196,547.66473 A 10.16466,10.16466 0 1 1 95.282643,547.66473 A 10.16466,10.16466 0 1 1 115.61196,547.66473 z" transform="matrix(1.55459, -0.548111, 0.548111, 1.55459, -233.943, -347.364)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: url(#radialGradient3285) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(145, 145, 124); stroke-width: 1; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="path5452" sodipodi:cx="100.93949" sodipodi:cy="545.5434" sodipodi:rx="3.9774756" sodipodi:ry="3.9774756" d="M 104.91697,545.5434 A 3.9774756,3.9774756 0 1 1 96.962016,545.5434 A 3.9774756,3.9774756 0 1 1 104.91697,545.5434 z" transform="matrix(1.30537, -0.460241, 0.460241, 1.30537, -152.884, -220.026)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5454" width="7.1152625" height="4.7729135" x="65.982826" y="465.99457" rx="1.6576668" ry="2.3864567" transform="matrix(0.943099, -0.332513, 0.332513, 0.943099, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5456" width="7.1152625" height="4.7729135" x="65.960732" y="523.66797" rx="1.6576668" ry="2.3864567" transform="matrix(0.943099, -0.332513, 0.332513, 0.943099, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5458" width="7.1152625" height="4.7729135" x="-501.19525" y="95.592987" rx="1.6576668" ry="2.3864567" transform="matrix(-0.332513, -0.943099, 0.943099, -0.332513, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5460" width="7.1152625" height="4.7729135" x="-501.19519" y="37.786999" rx="1.6576668" ry="2.3864567" transform="matrix(-0.332513, -0.943099, 0.943099, -0.332513, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5462" width="7.1152625" height="4.7729135" x="-306.59344" y="427.05988" rx="1.6576668" ry="2.3864567" transform="matrix(0.431749, -0.901994, 0.901994, 0.431749, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5464" width="7.1152625" height="4.7729135" x="-306.31866" y="369.14117" rx="1.6576668" ry="2.3864567" transform="matrix(0.431749, -0.901994, 0.901994, 0.431749, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5466" width="7.1152625" height="4.7729135" x="-404.36612" y="-276.25415" rx="1.6576668" ry="2.3864567" transform="matrix(-0.901994, -0.431749, 0.431749, -0.901994, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5468" width="7.1152625" height="4.7729135" x="-404.18933" y="-334.01593" rx="1.6576668" ry="2.3864567" transform="matrix(-0.901994, -0.431749, 0.431749, -0.901994, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5470" width="7.1152616" height="4.7729135" x="-129.78484" y="454.45309" rx="1.6576666" ry="2.3864567" transform="matrix(0.744062, -0.66811, 0.66811, 0.744062, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5472" width="7.1152616" height="4.7729135" x="-129.78716" y="512.4054" rx="1.6576666" ry="2.3864567" transform="matrix(0.744062, -0.66811, 0.66811, 0.744062, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5474" width="7.1152616" height="4.7729135" x="-488.91299" y="-100.00322" rx="1.6576666" ry="2.3864567" transform="matrix(-0.66811, -0.744062, 0.744062, -0.66811, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5476" width="7.1152616" height="4.7729135" x="-488.91293" y="-157.80919" rx="1.6576666" ry="2.3864567" transform="matrix(-0.66811, -0.744062, 0.744062, -0.66811, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5478" width="7.1152616" height="4.7729135" x="-435.77966" y="280.63101" rx="1.6576666" ry="2.3864567" transform="matrix(0.0537059, -0.998557, 0.998557, 0.0537059, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5480" width="7.1152616" height="4.7729135" x="-435.53876" y="222.79398" rx="1.6576666" ry="2.3864567" transform="matrix(0.0537059, -0.998557, 0.998557, 0.0537059, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5482" width="7.1152616" height="4.7729135" x="-258.33395" y="-406.56134" rx="1.6576666" ry="2.3864567" transform="matrix(-0.998557, -0.0537059, 0.0537059, -0.998557, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5484" width="7.1152616" height="4.7729135" x="-258.15717" y="-464.32318" rx="1.6576666" ry="2.3864567" transform="matrix(-0.998557, -0.0537059, 0.0537059, -0.998557, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5486" width="6.75" height="3.4375" x="390.19818" y="340.07739" rx="0.47016668" ry="1.71875" transform="matrix(0.901994, 0.431749, -0.431749, 0.901994, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5488" width="6.7500005" height="3.4375002" x="373.94806" y="386.9249" rx="0.47016671" ry="1.7187501" transform="matrix(0.921316, 0.388814, -0.388814, 0.921316, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5490" width="6.7500005" height="3.4375002" x="361.05246" y="-402.72012" rx="0.47016671" ry="1.7187501" transform="matrix(-0.412213, 0.911088, -0.911088, -0.412213, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5492" width="6.7500005" height="3.4375002" x="393.76926" y="-339.68652" rx="0.47016671" ry="1.7187501" transform="matrix(-0.329492, 0.944158, -0.944158, -0.329492, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5494" width="6.7500005" height="3.4375002" x="527.9704" y="-20.811844" rx="0.47016671" ry="1.7187501" transform="matrix(0.372629, 0.92798, -0.92798, 0.372629, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5496" width="6.7500005" height="3.4375002" x="528.52869" y="8.4513111" rx="0.47016671" ry="1.7187501" transform="matrix(0.372629, 0.92798, -0.92798, 0.372629, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5498" width="6.75" height="3.4375" x="36.094131" y="-545.8858" rx="0.47016668" ry="1.71875" transform="matrix(-0.895012, 0.446042, -0.446042, -0.895012, 0, 0)"/>
<rect style="opacity: 1; fill: rgb(250, 251, 230); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;" id="rect5500" width="6.75" height="3.4375" x="34.23085" y="-517.57141" rx="0.47016668" ry="1.71875" transform="matrix(-0.895012, 0.446042, -0.446042, -0.895012, 0, 0)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: none; fill-opacity: 1; fill-rule: nonzero; stroke: rgb(239, 239, 235); stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 0.418269;" id="path5502" sodipodi:cx="171.87114" sodipodi:cy="489.99133" sodipodi:rx="29.74268" sodipodi:ry="29.74268" d="M 201.61382,489.99133 A 29.74268,29.74268 0 1 1 142.12846,489.99133 A 29.74268,29.74268 0 1 1 201.61382,489.99133 z" transform="matrix(0.901994, 0.431749, -0.431749, 0.901994, 287.033, -70.2209)"/>
<path sodipodi:type="arc" style="opacity: 1; fill: none; fill-opacity: 1; fill-rule: nonzero; stroke: rgb(239, 239, 235); stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 0.418269;" id="path5504" sodipodi:cx="152.11635" sodipodi:cy="538.56073" sodipodi:rx="14.407301" sodipodi:ry="14.407301" d="M 166.52365,538.56073 A 14.407301,14.407301 0 1 1 137.70905,538.56073 A 14.407301,14.407301 0 1 1 166.52365,538.56073 z" transform="matrix(0.901994, 0.431749, -0.431749, 0.901994, 297.439, -60.2057)"/>
</g>
<path style="fill: none; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 1414.4913,917.89578 C 1414.4913,897.74323 1414.4913,897.74323 1414.4913,897.74323 C 1365.8777,873.52482 1365.8777,873.52482 1365.8777,873.52482" id="path5756"/>
<g id="g5514" transform="matrix(0.713522, 0, 0, 0.713522, 1092.85, 388.956)">
<path id="path5516" d="M 327.75998,612.9113 C 327.75998,700.20702 327.75998,700.20702 327.75998,700.20702 C 396.31737,700.20702 396.31737,700.20702 396.31737,700.20702 C 396.31737,630.43917 396.31737,630.43917 396.31737,630.43917 C 376.17621,612.9113 376.17621,612.9113 376.17621,612.9113 C 328.92196,613.25498 327.75998,612.9113 327.75998,612.9113 z" style="fill: rgb(253, 253, 253); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(190, 190, 190); stroke-width: 1.03197px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;"/>
<path id="path5518" d="M 376.56353,613.59867 L 376.56353,629.40812 C 395.93003,630.09549 395.5427,630.09549 395.5427,630.09549" style="fill: none; fill-rule: evenodd; stroke: rgb(138, 138, 138); stroke-width: 1.03197px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;"/>
</g>
<g id="g5528" transform="matrix(0.713522, 0, 0, 0.713522, 1247.63, 390.042)">
<path id="path5530" d="M 327.75998,612.9113 C 327.75998,700.20702 327.75998,700.20702 327.75998,700.20702 C 396.31737,700.20702 396.31737,700.20702 396.31737,700.20702 C 396.31737,630.43917 396.31737,630.43917 396.31737,630.43917 C 376.17621,612.9113 376.17621,612.9113 376.17621,612.9113 C 328.92196,613.25498 327.75998,612.9113 327.75998,612.9113 z" style="fill: rgb(253, 253, 253); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(190, 190, 190); stroke-width: 1.03197px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;"/>
<path id="path5532" d="M 376.56353,613.59867 L 376.56353,629.40812 C 395.93003,630.09549 395.5427,630.09549 395.5427,630.09549" style="fill: none; fill-rule: evenodd; stroke: rgb(138, 138, 138); stroke-width: 1.03197px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;"/>
</g>
<path style="fill: rgb(255, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 0.307256px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;" d="M 1504.5791,643.21934 L 1508.8912,655.00251 L 1504.285,653.4833 L 1499.7674,654.95252 L 1504.5791,643.21934 z" id="path5726" sodipodi:nodetypes="ccccc"/>
<path style="fill: rgb(255, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 0.307256px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;" d="M 1504.7204,778.89134 L 1509.0325,790.67451 L 1504.4263,789.1553 L 1499.9087,790.62452 L 1504.7204,778.89134 z" id="path5738" sodipodi:nodetypes="ccccc"/>
<path style="fill: rgb(255, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 0.307256px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;" d="M 1345.2679,741.53059 L 1349.58,729.74742 L 1344.9738,731.26663 L 1340.4562,729.79741 L 1345.2679,741.53059 z" id="path5740" sodipodi:nodetypes="ccccc"/>
<path style="fill: none; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 1344.6645,641.94735 C 1344.8413,732.81058 1344.8413,732.81058 1344.8413,732.81058" id="path5744"/>
<path style="fill: none; fill-rule: evenodd; stroke: rgb(255, 0, 0); stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" d="M 1504.4907,652.58241 C 1504.6675,743.44564 1504.6675,743.44564 1504.6675,743.44564" id="path5746"/>
<path style="fill: url(#radialGradient3287) rgb(0, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: none; stroke-width: 1.0007; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1; filter: url(#filter5116);" d="M 642.82808,196.7977 C 642.47856,230.31825 643.82808,329.7977 643.82808,329.7977 L 643.82808,329.7977 C 789.82808,329.7977 789.82808,329.7977 789.82808,329.7977 C 789.82808,196.7977 789.32808,196.7977 789.32808,196.7977 C 772.82808,182.2977 765.32808,183.2977 765.32808,183.2977 C 671.32808,181.2977 668.82808,183.7977 668.82808,183.7977 C 668.82808,183.7977 642.91901,188.07751 642.82808,196.7977 z" id="path5128" sodipodi:nodetypes="cccccccs" transform="matrix(0.414741, 0, 0, 0.405585, 1057.88, 480.707)"/>
<text xml:space="preserve" style="font-size: 7.93095px; font-style: normal; font-weight: bold; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1312.74" y="565.631" id="text2773"><tspan y="565.631" x="1312.74" sodipodi:role="line" id="tspan2775"><tspan x="1312.74" y="565.631" id="tspan2777">&gt;GET SALES </tspan><tspan dx="0" x="1370.27" y="565.631" id="tspan2779"/></tspan><tspan y="575.545" x="1312.74" sodipodi:role="line" id="tspan2781"><tspan x="1312.74" y="575.545" id="tspan2783"> TOTAL</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 8.04557px; font-style: normal; font-weight: bold; fill: rgb(153, 153, 153); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1470.95" y="565.765" id="text2785"><tspan y="565.765" x="1470.95" sodipodi:role="line" id="tspan2787"><tspan x="1470.95" y="565.765" id="tspan2789" style="fill: rgb(153, 153, 153);">&gt;GET SALES </tspan><tspan dx="0" x="1529.31" y="565.765" id="tspan2791" style="fill: rgb(153, 153, 153);"/></tspan><tspan y="575.822" x="1470.95" sodipodi:role="line" id="tspan2793"><tspan x="1470.95" y="575.822" id="tspan2795" style="fill: rgb(153, 153, 153);"> TOTAL</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 10px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1331.43" y="755.035" id="text2943"><tspan y="755.035" x="1331.43" sodipodi:role="line" id="tspan2945"><tspan x="1331.43" y="755.035" id="tspan2947">GET LIST OF ALL</tspan><tspan dx="0" x="1423.63" y="755.035" id="tspan2949"/></tspan><tspan y="767.535" x="1331.43" sodipodi:role="line" id="tspan2951"><tspan x="1331.43" y="767.535" id="tspan2953">SALES MADE</tspan><tspan dx="0" x="1403.09" y="767.535" id="tspan2955"/></tspan><tspan y="780.035" x="1331.43" sodipodi:role="line" id="tspan2957"><tspan x="1331.43" y="780.035" id="tspan2959">LAST YEAR</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 10px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1493.37" y="757.841" id="text2961"><tspan y="757.841" x="1493.37" sodipodi:role="line" id="tspan2963"><tspan x="1493.37" y="757.841" id="tspan2965">ADD ALL SALES</tspan><tspan dx="0" x="1580.48" y="757.841" id="tspan2967"/></tspan><tspan y="770.341" x="1493.37" sodipodi:role="line" id="tspan2969"><tspan x="1493.37" y="770.341" id="tspan2971">TOGETHER</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 8.04557px; font-style: normal; font-weight: bold; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1470.64" y="588.715" id="text2973"><tspan y="588.715" x="1470.64" sodipodi:role="line" id="tspan2975"><tspan x="1470.64" y="588.715" id="tspan2977">4 TOTAL SALES</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 10px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1332.85" y="860.491" id="text2979"><tspan y="860.491" x="1332.85" sodipodi:role="line" id="tspan2981"><tspan x="1332.85" y="860.491" id="tspan2983">QUERY</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 9px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1487.72" y="847.577" id="text2985"><tspan y="847.577" x="1487.72" sodipodi:role="line" id="tspan2987"><tspan x="1487.72" y="847.577" id="tspan2989">SALE 1</tspan><tspan dx="0" x="1522.44" y="847.577" id="tspan2991"/></tspan><tspan y="858.827" x="1487.72" sodipodi:role="line" id="tspan2993"><tspan x="1487.72" y="858.827" id="tspan2995">SALE 2</tspan><tspan dx="0" x="1522.44" y="858.827" id="tspan2997"/></tspan><tspan y="870.077" x="1487.72" sodipodi:role="line" id="tspan2999"><tspan x="1487.72" y="870.077" id="tspan3001">SALE 3</tspan><tspan dx="0" x="1522.44" y="870.077" id="tspan3003"/></tspan><tspan y="881.327" x="1487.72" sodipodi:role="line" id="tspan3005"><tspan x="1487.72" y="881.327" id="tspan3007">SALE 4</tspan></tspan></text>
<rect ry="0.022097087" rx="0" y="460.38123" x="164.5349" height="0.044194173" width="0" id="rect5413" style="opacity: 1; fill: rgb(78, 219, 55); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.082; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 3.1; stroke-dasharray: none; stroke-opacity: 1;"/>
<flowRoot xml:space="preserve" id="flowRoot5253" style="font-size:12px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"><flowRegion id="flowRegion5255"><rect id="rect5257" width="62.225395" height="33.941124" x="330.21887" y="640.82605"/></flowRegion><flowPara id="flowPara5259"/></flowRoot> <flowRoot xml:space="preserve" id="flowRoot5631" style="font-size:12px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"><flowRegion id="flowRegion5633"><rect id="rect5636" width="226.27417" height="67.17514" x="839.33575" y="169.18582"/></flowRegion><flowPara id="flowPara5638"/></flowRoot> <rect style="opacity: 1; fill: rgb(246, 246, 240); fill-opacity: 0; fill-rule: nonzero; stroke: none; stroke-width: 2.4; stroke-linecap: butt; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1;" id="rect5762" width="595.38391" height="154.14928" x="0" y="524.8609" rx="7.1171522" ry="3.4265683"/>
<flowRoot xml:space="preserve" id="flowRoot2676" style="font-size:12px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"><flowRegion id="flowRegion2678"><rect id="rect2680" width="220" height="45" x="14" y="-59.669922"/></flowRegion><flowPara id="flowPara2682"/></flowRoot> <flowRoot xml:space="preserve" id="flowRoot2684" style="font-size:12px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"><flowRegion id="flowRegion2686"><rect id="rect2688" width="337" height="117" x="-27" y="-211.66992"/></flowRegion><flowPara id="flowPara2690"/></flowRoot> <flowRoot xml:space="preserve" id="flowRoot2692" style="font-size:12px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"><flowRegion id="flowRegion2694"><rect id="rect2696" width="123" height="189" x="120" y="-349.66992"/></flowRegion><flowPara id="flowPara2698"/></flowRoot> <flowRoot xml:space="preserve" id="flowRoot2728" style="font-size:12px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"><flowRegion id="flowRegion2730"><rect id="rect2732" width="248.19447" height="79.549515" x="22.98097" y="54.800766"/></flowRegion><flowPara id="flowPara2734"/></flowRoot> <flowRoot xml:space="preserve" id="flowRoot2798" style="font-size:10px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"><flowRegion id="flowRegion2800"><rect id="rect2802" width="8.485281" height="12.727922" x="101.82338" y="423.55695"/></flowRegion><flowPara id="flowPara2804"/></flowRoot> <flowRoot xml:space="preserve" id="flowRoot2806" style="font-size:10px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"><flowRegion id="flowRegion2808"><rect id="rect2810" width="451.13412" height="149.90663" x="21.213203" y="623.66815"/></flowRegion><flowPara id="flowPara2812"/></flowRoot> <text xml:space="preserve" style="font-size: 14px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1018.53" y="879.429" id="text2737"><tspan y="879.429" x="1018.53" sodipodi:role="line" id="tspan2739"><tspan x="1018.53" y="879.429" style="font-size: 16px;" id="tspan2741">Data tier</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 14px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1018.53" y="558.691" id="text2694"><tspan y="558.691" x="1018.53" sodipodi:role="line" id="tspan2696"><tspan x="1018.53" y="558.691" style="font-size: 16px;" id="tspan2698">Presentation tier</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 14px; font-style: normal; font-weight: bold; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1018.53" y="702.198" id="text2700"><tspan y="702.198" x="1018.53" sodipodi:role="line" id="tspan2702"><tspan x="1018.53" y="702.198" style="font-size: 16px;" id="tspan2704">Logic tier</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 12px; font-style: normal; font-weight: normal; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1020.04" y="725.487" id="text3427"><tspan sodipodi:role="line" id="tspan3429" x="1020.04" y="725.487"><tspan x="1020.04" y="725.487" id="tspan3431">This layer coordinates the </tspan></tspan><tspan sodipodi:role="line" id="tspan3433" x="1020.04" y="740.487"><tspan x="1020.04" y="740.487" id="tspan3435">application, processes commands, </tspan></tspan><tspan sodipodi:role="line" id="tspan3437" x="1020.04" y="755.487"><tspan x="1020.04" y="755.487" id="tspan3439">makes logical decisions and </tspan></tspan><tspan sodipodi:role="line" id="tspan3441" x="1020.04" y="770.487"><tspan x="1020.04" y="770.487" id="tspan3443">evaluations, and performs </tspan></tspan><tspan sodipodi:role="line" id="tspan3445" x="1020.04" y="785.487"><tspan x="1020.04" y="785.487" id="tspan3447">calculations. It also moves and </tspan></tspan><tspan sodipodi:role="line" id="tspan3449" x="1020.04" y="800.487"><tspan x="1020.04" y="800.487" id="tspan3451">processes data between the two </tspan></tspan><tspan sodipodi:role="line" id="tspan3453" x="1020.04" y="815.487"><tspan x="1020.04" y="815.487" id="tspan3455">surrounding layers.</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 12px; font-style: normal; font-weight: normal; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1019.72" y="903.985" id="text3457"><tspan sodipodi:role="line" id="tspan3459" x="1019.72" y="903.985"><tspan x="1019.72" y="903.985" id="tspan3461">Here information is stored and retrieved </tspan></tspan><tspan sodipodi:role="line" id="tspan3463" x="1019.72" y="918.985"><tspan x="1019.72" y="918.985" id="tspan3465">from a database or file system. The </tspan></tspan><tspan sodipodi:role="line" id="tspan3467" x="1019.72" y="933.985"><tspan x="1019.72" y="933.985" id="tspan3469">information is then passed back to the </tspan></tspan><tspan sodipodi:role="line" id="tspan3471" x="1019.72" y="948.985"><tspan x="1019.72" y="948.985" id="tspan3473">logic tier for processing, and then </tspan></tspan><tspan sodipodi:role="line" id="tspan3475" x="1019.72" y="963.985"><tspan x="1019.72" y="963.985" id="tspan3477">eventually back to the user.</tspan></tspan></text>
<text xml:space="preserve" style="font-size: 12px; font-style: normal; font-weight: normal; fill: rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1; font-family: Bitstream Vera Sans;" x="1020.04" y="579.832" id="text3714"><tspan sodipodi:role="line" id="tspan3716"><tspan x="1020.04" y="579.832" id="tspan3718">The top-most level of the application</tspan><tspan dx="0" x="1241.47" y="579.832" id="tspan3720"/></tspan><tspan sodipodi:role="line" id="tspan3722"><tspan x="1020.04" y="594.832" id="tspan3724">is the user interface. The main function</tspan><tspan dx="0" x="1257.22" y="594.832" id="tspan3726"/></tspan><tspan sodipodi:role="line" id="tspan3728"><tspan x="1020.04" y="609.832" id="tspan3730">of the interface is to translate tasks </tspan><tspan dx="0" x="1238.45" y="609.832" id="tspan3732"/></tspan><tspan sodipodi:role="line" id="tspan3734"><tspan x="1020.04" y="624.832" id="tspan3736">and results to something the user can </tspan><tspan dx="0" x="1252.98" y="624.832" id="tspan3738"/></tspan><tspan sodipodi:role="line" id="tspan3740"><tspan x="1020.04" y="639.832" id="tspan3742">understand.</tspan></tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -0,0 +1,452 @@
.. _howto/rdtraining/02_setup:
=========================================
Chapter 2: Development Environment Set-up
=========================================
There are multiple ways to install Odoo depending on the intended use case.
This document attempts to describe the installation options for an internal Odoo R&D developer. We
assume that you are installing your development environment on a standard Odoo laptop with Linux
Mint installed and up-to-date. At the time of writing, we are using a vanilla Linux Mint 20
(Ubuntu 20.04) as a starting point.
If you are using another environment, you can refer to :ref:`setup/install/source`.
Fetch the sources & configure git
=================================
Install and configure git
-------------------------
The very first step of the installation process is to install the `git version control system <https://git-scm.com/>`__
because the Odoo source code is managed on GitHub. Once installed, you can set your name and email:
.. code-block:: console
$ sudo apt install git
$ git config --global user.name "Your full name"
$ git config --global user.email "xyz@odoo.com"
Configure GitHub
----------------
To fetch the sources and contribute to Odoo's development you will need a GitHub account. We
recommend using your trigram (xyz) followed by '-odoo' as your username: 'xyz-odoo'. If you prefer,
you can also use your personal GitHub account.
The easiest way to authenticate with GitHub is to use an SSH connection. Using the SSH
authentication will allow you to connect to GitHub without supplying your username and
password every time.
The following instructions are based on the official `GitHub documentation <https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh>`__.
Here is a step-by-step procedure:
- Generate a new SSH key, add it to the ssh-agent and copy the SSH key to your clipboard.
.. code-block:: console
$ ssh-keygen -t ed25519 -C "xyz@odoo.com"
$ ssh-add ~/.ssh/id_ed25519
$ sudo apt-get install xclip
$ xclip -sel clip < ~/.ssh/id_ed25519.pub
In Github:
- In the upper-right corner of any page, click your profile photo, then click Settings
.. image:: 02_setup/media/userbar-account-settings.png
- In the user settings sidebar, click SSH and GPG keys.
.. image:: 02_setup/media/settings-sidebar-ssh-keys.png
- Click New SSH key or Add SSH key.
.. image:: 02_setup/media/ssh-add-ssh-key.png
- In the "Title" field, add a descriptive label for the new key.
- Paste your key into the "Key" field.
.. image:: 02_setup/media/ssh-key-paste.png
- Click Add SSH key.
Fetch the sources
-----------------
All the Odoo sources will be located in `$HOME/src/`
.. code-block:: console
$ mkdir -p $HOME/src
$ cd $HOME/src
$ git clone git@github.com:odoo/odoo.git
$ git clone git@github.com:odoo/enterprise.git
.. tip:: Cloning the repositories will take a while, enjoy a cup of coffee while you wait.
.. tip:: You may need to ask your manager for read rights to fetch the enterprise repository.
.. _howto/rdtraining/02_setup/development_repository:
Configure development repository
--------------------------------
To contribute to Odoo's development you will need to
`fork the repository <https://guides.github.com/activities/forking/>`__, create a branch containing
your code in the fork and submit a
`Pull Request <https://docs.github.com/en/github/getting-started-with-github/github-glossary#pull-request>`__
to the Odoo repository.
If you are lucky enough to work at Odoo, the forks already exist. They are called
`odoo-dev/odoo` and `odoo-dev/enterprise`.
.. code-block:: console
$ cd $HOME/src/odoo
$ git remote add odoo-dev git@github.com:odoo-dev/odoo.git #add odoo-dev as a new remote
$ git remote rename origin odoo #change the name of origin (the odoo repository) to odoo
$ git remote set-url --push odoo no_push #remove the possibility to push to odoo (you can only push to odoo-dev)
$ cd $HOME/src/enterprise
$ git remote add enterprise-dev git@github.com:odoo-dev/enterprise.git
$ git remote rename origin enterprise
$ git remote set-url --push enterprise no_push
Useful git commands
-------------------
Here are some useful git commands for your day-to-day work.
* Change branch:
When you change branches, both repositories (odoo and enterprise) must be synchronized, i.e. both
need to be in the same branch.
.. code-block:: console
$ cd $HOME/src/odoo
$ git checkout 14.0
$ cd $HOME/src/enterprise
$ git checkout 14.0
* Fetch and rebase:
.. code-block:: console
$ cd $HOME/src/odoo
$ git fetch --all --prune
$ git rebase --autostash odoo/14.0
$ cd $HOME/src/enterprise
$ git fetch --all --prune
$ git rebase --autostash enterprise/14.0
Install the dependencies
========================
Python
------
Odoo requires Python 3.6 or later, if your computer is up-to-date you should already be at this
version or higher.
You can check your Python version with:
.. code-block:: console
$ python3 --version
Install pip3 and libraries
--------------------------
For libraries using native code, installation of development tools and native dependencies is
required before installing the Python dependencies of Odoo.
.. code-block:: console
$ sudo apt install python3-pip python3-dev libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev libssl-dev libpq-dev libjpeg-dev
Install odoo requirements
-------------------------
.. code-block:: console
$ cd $HOME/src/odoo
$ pip3 install -r requirements.txt
.. _howto/rdtraining/02_setup/install-wkhtmltopdf:
Install wkhtmltopdf
-------------------
wkhtmltopdf is a library to render HTML into PDF. Odoo uses it to create PDF reports. wkhtmltopdf
is not installed through pip and must be installed manually in version 0.12.5 to support
headers and footers.
.. code-block:: console
$ cd /tmp/
$ sudo wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.focal_amd64.deb
$ sudo gdebi --n wkhtmltox_0.12.5-1.focal_amd64.deb
$ sudo ln -s /usr/local/bin/wkhtmltopdf /usr/bin
$ sudo ln -s /usr/local/bin/wkhtmltoimage /usr/bin
Right-to-left interface support
-------------------------------
In order to support right-to-left (RTL) languages, we need `rtlcss` to convert the CSS files:
.. code-block:: console
$ sudo apt-get install nodejs npm
$ sudo npm install -g rtlcss
Install PostgreSQL
------------------
As seen in :ref:`howto/rdtraining/01_architecture`, Odoo uses PostgreSQL as a RDBMS. In the context of a
development machine, the easiest approach is to install it locally. Then we can create a PostgreSQL user
corresponding to our current user:
.. code-block:: console
$ sudo apt install postgresql postgresql-client
$ sudo -u postgres createuser -s $USER
Some useful SQL commands:
.. code-block:: console
$ createdb $DB_NAME #Create a database
$ dropdb $DB_NAME #Drop a database
$ psql $DB_NAME #Connect to a database
\l #List all the available databases
\dt #List all the tables of the $DB_NAME database
\d $TABLE_NAME #Show the structure of the table $TABLE_NAME
\q #Quit the psql environment (ctrl + d)
Run the server
==============
Running odoo-bin
----------------
Once all dependencies are set up, Odoo can be launched by running odoo-bin, the command-line interface of the server.
.. code-block:: console
$ cd $HOME/src/odoo/
$ ./odoo-bin --addons-path="addons/,../enterprise/" -d rd-demo
There are multiple :ref:`command-line arguments <reference/cmdline/server>` that you can use to
configure the server. In this training you will only need some of them.
.. option:: -d <database>
The database that is going to be used.
.. option:: --addons-path <directories>
A comma-separated list of directories in which modules are stored. These directories are scanned
for modules.
.. option:: --limit-time-cpu <limit>
Prevents the worker from using more than <limit> CPU seconds for each request.
.. option:: --limit-time-real <limit>
Prevents the worker from taking longer than <limit> seconds to process a request.
The last two can be used to prevent the worker from being killed when debugging the source code.
.. tip:: You may face an error similar to `AttributeError: module '$MODULE_NAME' has no attribute '$ATTRIBUTE'`
In this case you may need to re-install the module with `$ pip install --upgrade --force-reinstall $MODULE_NAME`
If this error occurs with more than one module then you may need to re-install all the
requirements with `$ pip3 install --upgrade --force-reinstall -r requirements.txt`
You can also clear the python cache to solve the issue
.. code-block:: console
$ cd $HOME/.local/lib/python3.8/site-packages/
$ find -name '*.pyc' -type f -delete
Log in to Odoo
--------------
Open `http://localhost:8069/` on your browser. We recommend you use:
`Firefox <https://www.mozilla.org/fr/firefox/new/>`__,
`Chrome <https://www.google.com/intl/fr/chrome/>`__
(`Chromium <https://www.chromium.org/Home>`__ the open source equivalent) or any other browser with
development tools.
To log in as the administrator user, you can use the following credentials:
* email = `admin`
* password = `admin`
The developer mode
==================
The Developer or Debug Mode gives you access to additional (advanced) tools.
This is useful for training and we assume that the user is in developer mode for the rest of the tutorials.
To activate the developer or debug mode you can follow the steps `here <https://www.odoo.com/documentation/user/general/developer_mode/activate.html>`__.
Extra tools
===========
Code Editor
-----------
If you are working at Odoo, many of your colleagues are using `VSCode`_ (`VSCodium`_ the open source
equivalent), `Sublime Text`_, `Atom`_ or `PyCharm`_. However you are free to
choose your preferred editor.
Don't forget to configure your linters correctly. Using a linter can help you by showing syntax and semantic
warnings or errors. Odoo source code tries to respect Python and JavaScript standards, but some of
them can be ignored.
For Python, we use PEP8 with these options ignored:
- E501: line too long
- E301: expected 1 blank line, found 0
- E302: expected 2 blank lines, found 1
For JavaScript, we use ESLint and you can find a `configuration file example here`_.
.. _configuration file example here: https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines#use-a-linter
.. _VSCode: https://code.visualstudio.com/
.. _VSCodium: https://vscodium.com/
.. _Sublime Text: https://www.sublimetext.com/
.. _PyCharm: https://www.jetbrains.com/pycharm/download/#section=linux
.. _Atom: https://atom.io/
Administrator tools for PostgreSQL
-----------------------------------
You can manage your PostgreSQL databases using the command line as demonstrated earlier or using
a GUI application such as `pgAdmin <https://www.pgadmin.org/download/pgadmin-4-apt/>`__ or `DBeaver <https://dbeaver.io/>`__.
To connect the GUI application to your database we recommend you connect using the Unix socket.
* Host name/address = /var/run/postgresql
* Port = 5432
* Username = $USER
Python Debugging
----------------
When facing a bug or trying to understand how the code works, simply printing things out can
go a long way, but a proper debugger can save a lot of time.
You can use a classic Python library debugger (`pdb <https://docs.python.org/3/library/pdb.html>`__,
`pudb <https://pypi.org/project/pudb/>`__ or `ipdb <https://pypi.org/project/ipdb/>`__) or you can
use your editor's debugger. To avoid difficult configurations in the beginning, it is
easier if you use a library debugger.
In the following example we use ipdb, but the process is similar with other libraries.
- Install the library:
.. code-block:: console
pip3 install ipdb
- Trigger (breakpoint):
.. code-block:: console
import ipdb; ipdb.set_trace()
Example:
.. code-block:: python
:emphasize-lines: 2
def copy(self, default=None):
import ipdb; ipdb.set_trace()
self.ensure_one()
chosen_name = default.get('name') if default else ''
new_name = chosen_name or _('%s (copy)') % self.name
default = dict(default or {}, name=new_name)
return super(Partner, self).copy(default)
Here is a list of commands:
.. option:: h(elp) [command]
Without an argument, print the list of available commands. With a command as an argument, print help
about that command.
.. option:: pp expression
The value of the ``expression`` is pretty-printed using the ``pprint`` module.
.. option:: w(here)
Print a stack trace, with the most recent frame at the bottom.
.. option:: d(own)
Move the current frame one level down in the stack trace (to a newer frame).
.. option:: u(p)
Move the current frame one level up in the stack trace (to an older frame).
.. option:: n(ext)
Continue the execution until the next line in the current function is reached or it returns.
.. option:: c(ontinue)
Continue the execution and only stop when a breakpoint is encountered.
.. option:: s(tep)
Execute the current line, stop at the first possible occasion (either in a function that is
called or on the next line in the current function).
.. option:: q(uit)
Quit the debugger. The program being executed is aborted.
.. tip::
To avoid killing the worker when debugging, you can add these arguments when launching the
server: `--limit-time-cpu=9999 --limit-time-real=9999`
Another solution is to add them directly in the `~/.odoorc` file:
.. code-block:: console
$ cat ~/.odoorc
[options]
limit_time_cpu = 9999
limit_time_real = 9999
Now that your server is running, it's time to start
:ref:`writing your own application <howto/rdtraining/03_newapp>`!

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,104 @@
.. _howto/rdtraining/03_newapp:
============================
Chapter 3: A New Application
============================
The purpose of this chapter is to lay the foundation for the creation of a completely new Odoo module.
We will start from scratch with the minimum needed to have our module recognized by Odoo.
In the upcoming chapters, we will progressively add features to build a realistic business case.
The Real Estate Advertisement module
====================================
Our new module will cover a business area which is very specific and therefore not included in the
standard set of modules: real estate. It is worth noting that before
developing a new module, it is good practice to verify that Odoo doesn't already provide a way
to answer the specific business case.
Here is an overview of the main list view containing some advertisements:
.. image:: 03_newapp/media/overview_list_view_01.png
:align: center
:alt: List view 01
The top area of the form view summarizes important information for the property, such as the name,
the property type, the postcode and so on. The first tab contains information describing the
property: bedrooms, living area, garage, garden...
.. image:: 03_newapp/media/overview_form_view_01.png
:align: center
:alt: Form view 01
The second tab lists the offers for the property. We can see here that potential buyers can make
offers above or below the expected selling price. It is up to the seller to accept an offer.
.. image:: 03_newapp/media/overview_form_view_02.png
:align: center
:alt: Form view 02
Here is a quick video showing the workflow of the module.
Hopefully, this video will be recorded soon :-)
Prepare the addon directory
===========================
**Reference**: the documentation related to this topic can be found in
:ref:`manifest <reference/module/manifest>`.
.. note::
**Goal**: the goal of this section is to have Odoo recognize our new module, which will
be an empty shell for now. It will be listed in the Apps:
.. image:: 03_newapp/media/app_in_list.png
:align: center
:alt: The new module appears in the list
The first step of module creation is to create a new directory. To ease the development, we
suggest you first create the directory ``/home/$USER/src/custom``. In this directory we add
another directory ``estate``, which is our module.
A module must contain at least 2 files: the ``__manifest__.py`` file and a ``__init__.py`` file.
The ``__init__.py`` file can remain empty for now and we'll come back to it in the next chapter.
On the other hand, the ``__manifest__.py`` file must describe our module and cannot remain empty.
Its only required field is the ``name``, but it usually contains much more information.
Take a look at the
`CRM file <https://github.com/odoo/odoo/blob/fc92728fb2aa306bf0e01a7f9ae1cfa3c1df0e10/addons/crm/__manifest__.py#L1-L67>`__
as an example. In addition to providing the description of the module (``name``, ``category``,
``summary``, ``website``...), it lists its dependencies (``depends``). A dependency means that the
Odoo framework will ensure that these modules are installed before our module is installed. Moreover, if
one of these dependencies is uninstalled, then our module and **any other that depends on it will also
be uninstalled**. Think about your favorite Linux distribution package manager
(``apt``, ``dnf``, ``pacman``...): Odoo works in the same way.
.. exercise:: Create the required addon files.
Create the following folders and files:
- ``/home/$USER/src/custom/estate/__init__.py``
- ``/home/$USER/src/custom/estate/__manifest__.py``
The ``__manifest__.py`` file should only define the name and the dependencies of our modules.
Two framework modules are necessary: ``base`` and ``web``.
Restart the Odoo server and add the ``custom`` folder to the ``addons-path``:
.. code-block:: console
$ ./odoo-bin --addons-path=../custom,../enterprise/,addons
Go to Apps, click on Update Apps List, search for ``estate`` and... tadaaa, your module appears!
Did it not appear? Maybe try removing the default 'Apps' filter ;-)
.. exercise:: Make your module an 'App'.
Add the appropriate key to your ``__manifest__.py`` so that the module appears when the 'Apps'
filter is on.
You can even install the module! But obviously it's an empty shell, so no menu will appear.
All good? If yes, then let's :ref:`create our first model <howto/rdtraining/04_basicmodel>`!

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,298 @@
.. _howto/rdtraining/04_basicmodel:
==================================
Chapter 4: Models And Basic Fields
==================================
At the end of the :ref:`previous chapter <howto/rdtraining/03_newapp>`, we were able to create an Odoo
module. However, at this point it is still an empty shell which doesn't allow us to store any data.
In our real estate module, we want to store the information related to the properties
(name, description, price, living area...) in a database. The Odoo framework provides tools to
facilitate database interactions.
Before moving forward in the exercise, make sure the ``estate`` module is installed, i.e. it
must appear as 'Installed' in the Apps list.
Object-Relational Mapping
=========================
**Reference**: the documentation related to this topic can be found in the
:ref:`reference/orm/model` API.
.. note::
**Goal**: at the end of this section, the table ``estate_property`` should be created:
.. code-block:: text
$ psql -d rd-demo
rd-demo=# SELECT COUNT(*) FROM estate_property;
count
-------
0
(1 row)
A key component of Odoo is the `ORM`_ layer.
This layer avoids having to manually write most `SQL`_
and provides extensibility and security services\ [#rawsql]_.
Business objects are declared as Python classes extending
:class:`~odoo.models.Model`, which integrates them into the automated
persistence system.
Models can be configured by setting attributes in their
definition. The most important attribute is
:attr:`~odoo.models.Model._name`, which is required and defines the name for
the model in the Odoo system. Here is a minimum definition of a
model::
from odoo import models
class TestModel(models.Model):
_name = "test.model"
This definition is enough for the ORM to generate a database table named ``test_model``. The
``.`` in the model ``_name`` is automatically converted into a ``_`` by the ORM. By convention all
models are located in a ``models`` directory and each model is defined in its own Python
file.
Take a look at how the ``crm_recurring_plan`` table is defined and how the corresponding Python
file is imported:
1. The model is defined in the file ``crm/models/crm_recurring_plan.py``
(see `here <https://github.com/odoo/odoo/blob/e80911aaead031e7523173789e946ac1fd27c7dc/addons/crm/models/crm_recurring_plan.py#L1-L9>`__)
2. The file ``crm_recurring_plan.py`` is imported in ``crm/models/__init__.py``
(see `here <https://github.com/odoo/odoo/blob/e80911aaead031e7523173789e946ac1fd27c7dc/addons/crm/models/__init__.py#L15>`__)
3. The folder ``models`` is imported in ``crm/__init__.py``
(see `here <https://github.com/odoo/odoo/blob/e80911aaead031e7523173789e946ac1fd27c7dc/addons/crm/__init__.py#L5>`__)
.. exercise:: Define the real estate properties model.
Based on example given in the CRM module, create the appropriate files and folder for the
``estate_property`` table.
When the files are created, add a minimum definition for the
``estate.property`` model.
Any modification of the Python files requires a restart of the Odoo server. When we restart
the server, we will add the parameters ``-d`` and ``-u``:
.. code-block:: console
$ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate
``-u estate`` means we want to upgrade the ``estate`` module, i.e. the ORM will
apply database schema changes. In this case it creates a new table. ``-d rd-demo`` means
that the upgrade should be performed on the ``rd-demo`` database. ``-u`` should always be used in
combination with ``-d``.
During the startup you should see the following warnings:
.. code-block:: text
...
WARNING rd-demo odoo.models: The model estate.property has no _description
...
WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules, consider adding one...
...
If this is the case, then you should be good! To be sure, double check with ``psql`` as demonstrated in
the **Goal**.
.. exercise:: Add a description.
Add a ``_description`` to your model to get rid of one of the warnings.
Model fields
============
**Reference**: the documentation related to this topic can be found in the
:ref:`reference/orm/fields` API.
Fields are used to define what the model can store and where they are stored. Fields are
defined as attributes in the model class::
from odoo import fields, models
class TestModel(models.Model):
_name = "test.model"
_description = "Test Model"
name = fields.Char()
The ``name`` field is a :class:`~odoo.fields.Char` which will be represented as a Python
unicode ``str`` and a SQL ``VARCHAR``.
Types
-----
.. note::
**Goal**: at the end of this section, several basic fields should have been added to the table
``estate_property``:
.. code-block:: text
$ psql -d rd-demo
rd-demo=# \d estate_property;
Table "public.estate_property"
Column | Type | Collation | Nullable | Default
--------------------+-----------------------------+-----------+----------+---------------------------------------------
id | integer | | not null | nextval('estate_property_id_seq'::regclass)
create_uid | integer | | |
create_date | timestamp without time zone | | |
write_uid | integer | | |
write_date | timestamp without time zone | | |
name | character varying | | |
description | text | | |
postcode | character varying | | |
date_availability | date | | |
expected_price | double precision | | |
selling_price | double precision | | |
bedrooms | integer | | |
living_area | integer | | |
facades | integer | | |
garage | boolean | | |
garden | boolean | | |
garden_area | integer | | |
garden_orientation | character varying | | |
Indexes:
"estate_property_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"estate_property_create_uid_fkey" FOREIGN KEY (create_uid) REFERENCES res_users(id) ON DELETE SET NULL
"estate_property_write_uid_fkey" FOREIGN KEY (write_uid) REFERENCES res_users(id) ON DELETE SET NULL
There are two broad categories of fields: 'simple' fields, which are atomic
values stored directly in the model's table, and 'relational' fields, which link
records (of the same or different models).
Simple field examples are :class:`~odoo.fields.Boolean`, :class:`~odoo.fields.Float`,
:class:`~odoo.fields.Char`, :class:`~odoo.fields.Text`, :class:`~odoo.fields.Date`
and :class:`~odoo.fields.Selection`.
.. exercise:: Add basic fields to the Real Estate Property table.
Add the following basic fields to the table:
========================= =========================
Field Type
========================= =========================
name Char
description Text
postcode Char
date_availability Date
expected_price Float
selling_price Float
bedrooms Integer
living_area Integer
facades Integer
garage Boolean
garden Boolean
garden_area Integer
garden_orientation Selection
========================= =========================
The ``garden_orientation`` field must have 4 possible values: 'North', 'South', 'East'
and 'West'. The selection list is defined as a list of tuples, see
`here <https://github.com/odoo/odoo/blob/b0e0035b585f976e912e97e7f95f66b525bc8e43/addons/crm/report/crm_activity_report.py#L31-L34>`__
for an example.
When the fields are added to the model, restart the server with ``-u estate``
.. code-block:: console
$ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate
Connect to ``psql`` and check the structure of the table ``estate_property``. You'll notice that
a couple of extra fields were also added to the table. We will revisit them later.
Common Attributes
-----------------
.. note::
**Goal**: at the end of this section, the columns ``name`` and ``expected_price`` should be
not nullable in the table ``estate_property``:
.. code-block:: console
rd-demo=# \d estate_property;
Table "public.estate_property"
Column | Type | Collation | Nullable | Default
--------------------+-----------------------------+-----------+----------+---------------------------------------------
...
name | character varying | | not null |
...
expected_price | double precision | | not null |
...
Much like the model itself, fields can be configured by passing
configuration attributes as parameters::
name = field.Char(required=True)
Some attributes are available on all fields, here are the most common ones:
:attr:`~odoo.fields.Field.string` (``str``, default: field's name)
The label of the field in UI (visible by users).
:attr:`~odoo.fields.Field.required` (``bool``, default: ``False``)
If ``True``, the field can not be empty. It must either have a default
value or always be given a value when creating a record.
:attr:`~odoo.fields.Field.help` (``str``, default: ``''``)
Provides long-form help tooltip for users in the UI.
:attr:`~odoo.fields.Field.index` (``bool``, default: ``False``)
Requests that Odoo create a `database index`_ on the column.
.. exercise:: Set attributes for existing fields.
Add the following attributes:
========================= =========================
Field Attribute
========================= =========================
name required
expected_price required
========================= =========================
After restarting the server, both fields should be not nullable.
Automatic Fields
----------------
**Reference**: the documentation related to this topic can be found in
:ref:`reference/fields/automatic`.
You may have noticed your model has a few fields you never defined.
Odoo creates a few fields in all models\ [#autofields]_. These fields are
managed by the system and can't be written to, but they can be read if
useful or necessary:
:attr:`~odoo.fields.Model.id` (:class:`~odoo.fields.Id`)
The unique identifier for a record of the model.
:attr:`~odoo.fields.Model.create_date` (:class:`~odoo.fields.Datetime`)
Creation date of the record.
:attr:`~odoo.fields.Model.create_uid` (:class:`~odoo.fields.Many2one`)
User who created the record.
:attr:`~odoo.fields.Model.write_date` (:class:`~odoo.fields.Datetime`)
Last modification date of the record.
:attr:`~odoo.fields.Model.write_uid` (:class:`~odoo.fields.Many2one`)
User who last modified the record.
Now that we have created our first model, let's
:ref:`add some security <howto/rdtraining/05_securityintro>`!
.. [#autofields] it is possible to :ref:`disable the automatic creation of some
fields <reference/fields/automatic/log_access>`
.. [#rawsql] writing raw SQL queries is possible, but requires caution as this
bypasses all Odoo authentication and security mechanisms.
.. _database index:
https://use-the-index-luke.com/sql/preface
.. _ORM:
https://en.wikipedia.org/wiki/Object-relational_mapping
.. _SQL:
https://en.wikipedia.org/wiki/SQL

View File

@ -0,0 +1,126 @@
.. _howto/rdtraining/05_securityintro:
==========================================
Chapter 5: Security - A Brief Introduction
==========================================
In the :ref:`previous chapter <howto/rdtraining/04_basicmodel>`, we created our first table intended
to store business data. In a business application such as Odoo, one of the first questions 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:`howto/rdtraining/B_acl_irrules`. This chapter
aims to cover the minimum required for our new module.
Data Files (CSV)
================
Odoo is a highly data driven system. Although behavior is customized using Python code, part of a
module's value is in the data it sets up when loaded. One way to load data is through a CSV
file. One example is the
`list of country states <https://github.com/odoo/odoo/blob/master/odoo/addons/base/data/res.country.state.csv>`__
which is loaded at installation of the ``base`` module.
.. code-block:: text
"id","country_id:id","name","code"
state_us_1,us,"Alabama","AL"
state_us_2,us,"Alaska","AK"
state_us_3,us,"Arizona","AZ"
state_us_4,us,"Arkansas","AR"
...
- ``id`` is an :term:`external identifier`. It can be used to refer to the record
(without knowing its in-database identifier).
- ``country_id:id`` refers to the country by using its :term:`external identifier`.
- ``name`` is the name of the state.
- ``code`` is the code of the state.
These three fields are
`defined <https://github.com/odoo/odoo/blob/2ad2f3d6567b6266fc42c6d2999d11f3066b282c/odoo/addons/base/models/res_country.py#L108-L111>`__
in the ``res.country.state`` model.
By convention, a file importing data is located in the ``data`` folder of a module. When the data
is related to security, it is located in the ``security`` folder. When the data is related to
views and actions (we will cover this later), it is located in the ``views`` folder.
Additionally, all of these files must be declared in the ``data``
list within the ``__manifest__.py`` file. Our example file is defined
`in the manifest of the base module <https://github.com/odoo/odoo/blob/e8697f609372cd61b045c4ee2c7f0fcfb496f58a/odoo/addons/base/__manifest__.py#L29>`__.
Also note that the content of the data files is only loaded when a module is installed or
updated.
.. warning::
The data files are sequentially loaded following their order in the ``__manifest__.py`` file.
This means that if data ``A`` refers to data ``B``, you must make sure that ``B``
is loaded before ``A``.
In the case of the country states, you will note that the
`list of countries <https://github.com/odoo/odoo/blob/e8697f609372cd61b045c4ee2c7f0fcfb496f58a/odoo/addons/base/__manifest__.py#L22>`__
is loaded **before** the
`list of country states <https://github.com/odoo/odoo/blob/e8697f609372cd61b045c4ee2c7f0fcfb496f58a/odoo/addons/base/__manifest__.py#L29>`__.
This is because the states refer to the countries.
Why is all this important for security? Because all the security configuration of a model is loaded through
data files, as we'll see in the next section.
Access Rights
=============
**Reference**: the documentation related to this topic can be found in
:ref:`reference/security/acl`.
.. note::
**Goal**: at the end of this section, the following warning should not appear anymore:
.. code-block:: text
WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules...
When no access rights are defined on a model, Odoo determines that no users can access the data.
It is even notified in the log:
.. code-block:: text
WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules, consider adding one. E.g. access_estate_property,access_estate_property,model_estate_property,base.group_user,1,0,0,0
Access rights are defined as records of the model ``ir.model.access``. Each
access right is associated with a model, a group (or no group for global
access) and a set of permissions: create, read, write and unlink\ [#unlink]_. Such access
rights are usually defined in a CSV file named
``ir.model.access.csv``.
Here is an example for our previous ``test.model``:
.. code-block:: text
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_test_model,access_test_model,model_test_model,base.group_user,1,0,0,0
- ``id`` is an :term:`external identifier`.
- ``name`` is the name of the ``ir.model.access``.
- ``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 <howto/rdtraining/N_security>` dedicated to the security.
- ``perm_read,perm_write,perm_create,perm_unlink``: read, write, create and unlink permissions
.. exercise:: Add access rights.
Create the ``ir.model.access.csv`` file in the appropriate folder and define it in the
``__manifest__.py`` file.
Give the read, write, create and unlink permissions to the group ``base.group_user``.
Tip: the warning message in the log gives you most of the solution ;-)
Restart the server and the warning message should have disappeared!
It's now time to finally :ref:`interact with the UI <howto/rdtraining/06_firstui>`!
.. [#who] meaning which Odoo user (or group of users)
.. [#unlink] 'unlink' is the equivalent of 'delete'

View File

@ -0,0 +1,293 @@
.. _howto/rdtraining/06_firstui:
========================================
Chapter 6: Finally, Some UI To Play With
========================================
Now that we've created our new :ref:`model <howto/rdtraining/04_basicmodel>` and its corresponding
:ref:`access rights <howto/rdtraining/05_securityintro>`, it is time to interact with
the user interface.
At the end of this chapter, we will have created a couple of menus in order to access a default list
and form view.
Data Files (XML)
================
**Reference**: the documentation related to this topic can be found in
:ref:`reference/data`.
In :ref:`howto/rdtraining/05_securityintro`, we added data through a CSV file. The CSV
format is convenient when the data to load has a simple format. When the format is more complex
(e.g. load the structure of a view or an email template), we use the XML format. For example,
this
`help field <https://github.com/odoo/odoo/blob/09c59012bf80d2ccbafe21c39e604d6cfda72924/addons/crm/views/crm_lost_reason_views.xml#L61-L69>`__
contains HTML tags. While it would be possible to load such data through a CSV file, it is more
convenient to use an XML file.
The XML files must be added to the same folders as the CSV files and defined similarly in the
``__manifest__.py``. The content of the data files is also sequentially loaded when a module is installed or
updated, therefore all remarks made for CSV files hold true for XML files.
When the data is linked to views, we add them to the ``views`` folder.
In this chapter we will load our first action and menus though an XML file. Actions and menus are
standard records in the database.
.. note::
When performance is important, the CSV format is preferred over the XML format. This is the case in Odoo
where loading a CSV file is faster than loading an XML file.
In Odoo, the user interface (actions, menus and views) is largely defined by creating
and composing records defined in an XML file. A common pattern is Menu > Action > View.
To access records the user navigates through several menu levels; the deepest level is an
action which triggers the opening of a list of the records.
Actions
=======
**Reference**: the documentation related to this topic can be found in
:ref:`reference/actions`.
.. note::
**Goal**: at the end of this section, an action should be loaded in the system. We won't see
anything yet in the UI, but the file should be loaded in the log:
.. code-block:: text
INFO rd-demo odoo.modules.loading: loading estate/data/estate_property_views.xml
Actions can be triggered in three ways:
1. by clicking on menu items (linked to specific actions)
2. by clicking on buttons in views (if these are connected to actions)
3. as contextual actions on object
We will only cover the first case in this chapter. The second case will be covered in a
:ref:`later chapter <howto/rdtraining/10_actions>` while the last is the focus of an advanced topic.
In our Real Estate example, we would like to link a menu to the ``estate.property`` model, so we
are able to create a new record. The action can be viewed as the link between the menu and
the model.
A basic action for our ``test.model`` is:
.. code-block:: xml
<record id="test_model_action" model="ir.actions.act_window">
<field name="name">Test action</field>
<field name="res_model">test.model</field>
<field name="view_mode">tree,form</field>
</record>
- ``id`` is an :term:`external identifier`. It can be used to refer to the record
(without knowing its in-database identifier).
- ``model`` has a fixed value of ``ir.actions.act_window`` (:ref:`reference/actions/window`).
- ``name`` is the name of the action.
- ``res_model`` is the model which the action applies to.
- ``view_mode`` are the views that will be available; in this case they are the list (tree) and form views.
We'll see :ref:`later <howto/rdtraining/15_qwebintro>` that there can be other view modes.
Examples can be found everywhere in Odoo, but
`this <https://github.com/odoo/odoo/blob/09c59012bf80d2ccbafe21c39e604d6cfda72924/addons/crm/views/crm_lost_reason_views.xml#L57-L70>`__
is a good example of a simple action. Pay attention to the structure of the XML data file since you will
need it in the following exercise.
.. exercise:: Add an action.
Create the ``estate_property_views.xml`` file in the appropriate folder and define it in the
``__manifest__.py`` file.
Create an action for the model ``estate.property``.
Restart the server and you should see the file loaded in the log.
Menus
=====
**Reference**: the documentation related to this topic can be found in
:ref:`reference/data/shortcuts`.
.. note::
**Goal**: at the end of this section, three menus should be created and the default view is
displayed:
.. image:: 06_firstui/media/estate_menu_root.png
:align: center
:alt: Root menus
.. image:: 06_firstui/media/estate_menu_action.png
:align: center
:alt: First level and action menus
.. image:: 06_firstui/media/estate_form_default.png
:align: center
:alt: Default form view
To reduce the complexity in declaring a menu (``ir.ui.menu``) and connecting it to the corresponding action,
we can use the ``<menuitem>`` shortcut .
A basic menu for our ``test_model_action`` is:
.. code-block:: xml
<menuitem id="test_model_menu_action" action="test_model_action"/>
The menu ``test_model_menu_action`` is linked to the action ``test_model_action``, and the action
is linked to the model ``test.model``. As previously mentioned, the action can be seen as the link
between the menu and the model.
However, menus always follow an architecture, and in practice there are three levels of menus:
1. The root menu, which is displayed in the App switcher (the Odoo Community App switcher is a
dropdown menu)
2. The first level menu, displayed in the top bar
3. The action menus
.. image:: 06_firstui/media/menu_01.png
:align: center
:alt: Root menus
.. image:: 06_firstui/media/menu_02.png
:align: center
:alt: First level and action menus
The easiest way to define the structure is to create it in the XML file. A basic
structure for our ``test_model_action`` is:
.. code-block:: xml
<menuitem id="test_menu_root" name="Test">
<menuitem id="test_first_level_menu" name="First Level">
<menuitem id="test_model_menu_action" action="test_model_action"/>
</menuitem>
</menuitem>
The name for the third menu is taken from the name of the ``action``.
.. exercise:: Add menus.
Create the ``estate_menus.xml`` file in the appropriate folder and define it in the
``__manifest__.py`` file. Remember the sequential loading of the data files ;-)
Create the three levels of menus for the ``estate.property`` action created in the previous
exercise. Refer to the **Goal** of this section for the expected result.
Restart the server and **refresh the browser**\ [#refresh]_. You should now see the menus,
and you'll even be able to create your first real estate property advertisement!
Fields, Attributes And View
===========================
.. note::
**Goal**: at the end of this section, the selling price should be read-only and the number
of bedrooms and the availability date should have default values. Additionally the selling price
and availability date values won't be copied when the record is duplicated.
.. image:: 06_firstui/media/attribute_and_default.gif
:align: center
:alt: Interaction between model and view
The reserved fields ``active`` and ``state`` are added to the ``estate.property`` model.
So far we have only used the generic view for our real estate property advertisements, but
in most cases we want to fine tune the view. There are many fine-tunings possible in Odoo, but
usually the first step is to make sure that:
- some fields have a default value
- some fields are read-only
- some fields are not copied when duplicating the record
In our real estate business case, we would like the following:
- The selling price should be read-only (it will be automatically filled in later)
- The availability date and the selling price should not be copied when duplicating a record
- The default number of bedrooms should be 2
- The default availability date should be in 3 months
Some New Attributes
-------------------
Before moving further with the view design, let's step back to our model definition. We saw that some
attributes, such as ``required=True``, impact the table schema in the database. Other attributes
will impact the view or provide default values.
.. exercise:: Add new attributes to the fields.
Find the appropriate attributes (see :class:`~odoo.fields.Field`) to:
- set the selling price as read-only
- prevent copying of the availability date and the selling price values
Restart the server and refresh the browser. You should not be able to set any selling prices. When
duplicating a record, the availability date should be empty.
Default Values
--------------
Any field can be given a default value. In the field definition, add the option
``default=X`` where ``X`` is either a Python literal value (boolean, integer,
float, string) or a function taking a model and returning a value::
name = fields.Char(default="Unknown")
last_seen = fields.Datetime("Last Seen", default=lambda self: fields.Datetime.now())
The ``name`` field will have the value 'Unknown' by default while the ``last_seen`` field will be
set as the current time.
.. exercise:: Set default values.
Add the appropriate default attributes so that:
- the default number of bedrooms is 2
- the default availability date is in 3 months
Tip: this might help you: :meth:`~odoo.fields.Date.today`
Check that the default values are set as expected.
Reserved Fields
---------------
**Reference**: the documentation related to this topic can be found in
:ref:`reference/orm/fields/reserved`.
A few field names are reserved for pre-defined behaviors. They should be defined on a
model when the related behavior is desired.
.. exercise:: Add active field.
Add the ``active`` field to the ``estate.property`` model.
Restart the server, create a new property, then come back to the list view... The property will
not be listed! ``active`` is an example of a reserved field with a specific behavior: when
a record has ``active=False``, it is automatically removed from any search. To display the
created property, you will need to specifically search for inactive records.
.. image:: 06_firstui/media/inactive.gif
:align: center
:alt: Inactive records
.. exercise:: Set a default value for active field.
Set the appropriate default value for the ``active`` field so it doesn't disappear anymore.
Note that the default ``active=False`` value was assigned to all existing records.
.. exercise:: Add state field.
Add a ``state`` field to the ``estate.property`` model. Five values are possible: New,
Offer Received, Offer Accepted, Sold and Canceled. It must be required, should not be copied
and should have its default value set to 'New'.
Make sure to use the correct type!
The ``state`` will be used later on for several UI enhancements.
Now that we are able to interact with the UI thanks to the default views, the next step is
obvious: we want to define :ref:`our own views <howto/rdtraining/07_basicviews>`.
.. [#refresh] A refresh is needed since the web client keeps a cache of the various menus
and views for performance reasons.

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,231 @@
.. _howto/rdtraining/07_basicviews:
======================
Chapter 7: Basic Views
======================
We have seen in the :ref:`previous chapter <howto/rdtraining/06_firstui>` that Odoo is able to
generate default views for a given model. In practice, the default view is **never** acceptable
for a business application. Instead, we should at least organize the various fields in a logical
manner.
Views are defined in XML files with actions and menus. They are instances of the
``ir.ui.view`` model.
In our real estate module, we need to organize the fields in a logical way:
- in the list (tree) view, we want to display more than just the name.
- in the form view, the fields should be grouped.
- in the search view, we must be able to search on more than just the name. Specifically, we want a
filter for the 'Available' properties and a shortcut to group by postcode.
List
====
**Reference**: the documentation related to this topic can be found in
:ref:`reference/views/list`.
.. note::
**Goal**: at the end of this section, the list view should look like this:
.. image:: 07_basicviews/media/list.png
:align: center
:alt: List view
List views, also called tree views, display records in a tabular form.
Their root element is ``<tree>``. The most basic version of this view simply
lists all the fields to display in the table (where each field is a column):
.. code-block:: xml
<tree string="Tests">
<field name="name"/>
<field name="last_seen"/>
</tree>
A simple example can be found
`here <https://github.com/odoo/odoo/blob/6da14a3aadeb3efc40f145f6c11fc33314b2f15e/addons/crm/views/crm_lost_reason_views.xml#L46-L54>`__.
.. exercise:: Add a custom list view.
Define a list view for the ``estate.property`` model in the appropriate XML file. Check the
**Goal** of this section for the fields to display.
Tips:
- do not add the ``editable="bottom"`` attribute that you can find in the example above. We'll
come back to it later.
- some field labels may need to be adapted to match the reference.
As always, you need to restart the server (do not forget the ``-u`` option) and refresh the browser
to see the result.
.. warning::
You will probably use some copy-paste in this chapter, therefore always make sure that the ``id``
remains unique for each view!
Form
====
**Reference**: the documentation related to this topic can be found in
:ref:`reference/views/form`.
.. note::
**Goal**: at the end of this section, the form view should look like this:
.. image:: 07_basicviews/media/form.png
:align: center
:alt: Form view
Forms are used to create and edit single records.
Their root element is ``<form>``. They are composed of high-level structure
elements (groups and notebooks) and interactive elements (buttons and fields):
.. code-block:: xml
<form string="Test">
<sheet>
<group>
<group>
<field name="name"/>
</group>
<group>
<field name="last_seen"/>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
</notebook>
</group>
</sheet>
</form>
It is possible to use regular HTML tags such as ``div`` and ``h1`` as well as the the ``class`` attribute
(Odoo provides some built-in classes) to fine-tune the look.
A simple example can be found
`here <https://github.com/odoo/odoo/blob/6da14a3aadeb3efc40f145f6c11fc33314b2f15e/addons/crm/views/crm_lost_reason_views.xml#L16-L44>`__.
.. exercise:: Add a custom form view.
Define a form view for the ``estate.property`` model in the appropriate XML file. Check the
**Goal** of this section for the expected final design of the page.
This might require some trial and error before you get to the expected result ;-) It is advised
that you add the fields and the tags one at a time to help understand how it works.
In order to avoid relaunching the server every time you do a modification to the view, it can
be convenient to use the ``--dev xml`` parameter when launching the server:
.. code-block:: console
$ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate --dev xml
This parameter allows you to just refresh the page to view your view modifications.
Search
======
**Reference**: the documentation related to this topic can be found in
:ref:`reference/views/search`.
.. note::
**Goal**: at the end of this section, the search view should look like this:
.. image:: 07_basicviews/media/search_01.png
:align: center
:alt: Search fields
.. image:: 07_basicviews/media/search_02.png
:align: center
:alt: Filter
.. image:: 07_basicviews/media/search_03.png
:align: center
:alt: Group By
Search views are slightly different from the list and form views since they don't display
*content*. Although they apply to a specific model, they are used to filter
other views' content (generally aggregated views such as :ref:`reference/views/list`).
Beyond the difference in use case, they are defined the same way.
Their root element is ``<search>``. The most basic version of this view simply
lists all the fields for which a shortcut is desired:
.. code-block:: xml
<search string="Tests">
<field name="name"/>
<field name="last_seen"/>
</search>
The default search view generated by Odoo provides a shortcut to filter by ``name``. It is very
common to add the fields which the user is likely to filter on in a customized search view.
.. exercise:: Add a custom search view.
Define a search view for the ``estate.property`` model in the appropriate XML file. Check the
first image of this section's **Goal** for the list of fields.
After restarting the server, it should be possible to filter on the given fields.
Search views can also contain ``<filter>`` elements, which act as toggles for
predefined searches. Filters must have one of the following attributes:
- ``domain``: adds the given domain to the current search
- ``context``: adds some context to the current search; uses the key ``group_by`` to group
results on the given field name
A simple example can be found
`here <https://github.com/odoo/odoo/blob/715a24333bf000d5d98b9ede5155d3af32de067c/addons/delivery/views/delivery_view.xml#L30-L44>`__.
Before going further in the exercise, it is necessary to introduce the 'domain' concept.
Domains
-------
**Reference**: the documentation related to this topic can be found in
:ref:`reference/orm/domains`.
In Odoo, a domain encodes conditions on
records: a domain is a list of criteria used to select a subset of a model's
records. Each criterion is a triplet with a *field name*, an *operator* and a *value*.
A record satisfies a criterion if the specified field meets the condition of the operator applied to the value.
For instance, when used on the *Product* model the following domain selects
all *services* with a unit price greater than *1000*::
[('product_type', '=', 'service'), ('unit_price', '>', 1000)]
By default criteria are combined with an implicit AND, meaning *every* criterion
needs to be satisfied for a record to match a domain. The logical operators
``&`` (AND), ``|`` (OR) and ``!`` (NOT) can be used to explicitly combine
criteria. They are used in prefix position (the operator is inserted before
its arguments rather than between). For instance, to select products 'which are
services *OR* have a unit price which is *NOT* between 1000 and 2000'::
['|',
('product_type', '=', 'service'),
'!', '&',
('unit_price', '>=', 1000),
('unit_price', '<', 2000)]
.. exercise:: Add filter and Group By.
The following should be added to the previously created search view:
- a filter which displays available properties, i.e. the state should be 'New' or
'Offer Received'.
- the ability to group results by postcode.
Looking good? At this point we are already able to create models and design a user interface which
makes sense business-wise. However, a key component is still missing: the
:ref:`link between models <howto/rdtraining/08_relations>`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,266 @@
.. _howto/rdtraining/08_relations:
===================================
Chapter 8: Relations Between Models
===================================
The :ref:`previous chapter <howto/rdtraining/07_basicviews>` covered the creation of custom views
for a model containing basic fields. However, in any real business scenario we need more than
one model. Moreover, links between models are necessary. One can easily imagine one model containing
the customers and another one containing the list of users. You might need to refer to a customer
or a user on any existing business model.
In our real estate module, we want the following information for a property:
- the customer who bought the property
- the real restate agent who sold the property
- the property type: house, apartment, penthouse, castle...
- a list of tags characterizing the property: cozy, renovated...
- a list of the offers received
Many2one
========
**Reference**: the documentation related to this topic can be found in
:class:`~odoo.fields.Many2one`.
.. note::
**Goal**: at the end of this section:
- a new ``estate.property.type`` model should be created with the corresponding menu, action and views.
.. image:: 08_relations/media/property_type.png
:align: center
:alt: Property type
- three Many2one fields should be added to the ``estate.property`` model: property type, buyer and seller.
.. image:: 08_relations/media/property_many2one.png
:align: center
:alt: Property
In our real estate module, we want to define the concept of property type. A property type
is, for example, a house or an apartment. It is a standard business need to categorize
properties according to their type, especially to refine filtering.
A property can have **one** type, but the same type can be assigned to **many** properties.
This is supported by the **many2one** concept.
A many2one is a simple link to another object. For example, in order to define a link to the
``res.partner`` in our test model, we can write::
partner_id = fields.Many2one("res.partner", string="Partner")
By convention, many2one fields have the ``_id`` suffix. Accessing the data in the partner
can then be easily done with::
print(my_test_object.partner_id.name)
.. seealso::
`foreign keys <https://www.postgresql.org/docs/current/tutorial-fk.html>`_
In practice a many2one can be seen as a dropdown list in a form view.
.. exercise:: Add the Real Estate Property Type table.
- Create the ``estate.property.type`` model and add the following field:
========================= ========================= =========================
Field Type Attributes
========================= ========================= =========================
name Char required
========================= ========================= =========================
- Add the menus as displayed in this section's **Goal**
- Add the field ``property_type_id`` into your ``estate.property`` model and its form, tree
and search views
This exercise is a good recap of the previous chapters: you need to create a
:ref:`model <howto/rdtraining/04_basicmodel>`, set the
:ref:`model <howto/rdtraining/05_securityintro>`, add an
:ref:`action and a menu <howto/rdtraining/06_firstui>`, and
:ref:`create a view <howto/rdtraining/07_basicviews>`.
Tip: do not forget to import any new Python files in ``__init__.py``, add new data files in
``__manifest.py__`` or add the access rights ;-)
Once again, restart the server and refresh to see the results!
In the real estate module, there are still two missing pieces of information we want on a property:
the buyer and the salesperson. The buyer can be any individual, but on the other hand the
salesperson must be an employee of the real estate agency (i.e. an Odoo user).
In Odoo, there are two models which we commonly refer to:
- ``res.partner``: a partner is a physical or legal entity. It can be a company, an individual or
even a contact address.
- ``res.users``: the users of the system. Users can be 'internal', i.e. they have
access to the Odoo backend. Or they can be 'portal', i.e. they cannot access the backend, only the
frontend (e.g. to access their previous orders in eCommerce).
.. exercise:: Add the buyer and the salesperson.
Add a buyer and a salesperson to the ``estate.property`` model using the two common models
mentioned above. They should be added in a new tab of the form view, as depicted in this section's **Goal**.
The default value for the salesperson must be the current user. The buyer should not be copied.
Tip: to get the default value, check the note below or look at an example
`here <https://github.com/odoo/odoo/blob/5bb8b927524d062be32f92eb326ef64091301de1/addons/crm/models/crm_lead.py#L92>`__.
.. note::
The object ``self.env`` gives access to request parameters and other useful
things:
- ``self.env.cr`` or ``self._cr`` is the database *cursor* object; it is
used for querying the database
- ``self.env.uid`` or ``self._uid`` is the current user's database id
- ``self.env.user`` is the current user's record
- ``self.env.context`` or ``self._context`` is the context dictionary
- ``self.env.ref(xml_id)`` returns the record corresponding to an XML id
- ``self.env[model_name]`` returns an instance of the given model
Now let's have a look at other types of links.
Many2many
=========
**Reference**: the documentation related to this topic can be found in
:class:`~odoo.fields.Many2many`.
.. note::
**Goal**: at the end of this section:
- a new ``estate.property.tag`` model should be created with the corresponding menu and action.
.. image:: 08_relations/media/property_tag.png
:align: center
:alt: Property tag
- tags should be added to the ``estate.property`` model:
.. image:: 08_relations/media/property_many2many.png
:align: center
:alt: Property
In our real estate module, we want to define the concept of property tags. A property tag
is, for example, a property which is 'cozy' or 'renovated'.
A property can have **many** tags and a tag can be assigned to **many** properties.
This is supported by the **many2many** concept.
A many2many is a bidirectional multiple relationship: any record on one side can be related to any
number of records on the other side. For example, in order to define a link to the
``account.tax`` model on our test model, we can write::
tax_ids = fields.Many2many("account.tax", string="Taxes")
By convention, many2many fields have the ``_ids`` suffix. This means that several taxes can be
added to our test model. It behaves as a list of records, meaning that accessing the data must be
done in a loop::
for tax in my_test_object.tax_ids:
print(tax.name)
A list of records is known as a *recordset*, i.e. an ordered collection of records. It supports
standard Python operations on collections, such as ``len()`` and ``iter()``, plus extra set
operations like ``recs1 | recs2``.
.. exercise:: Add the Real Estate Property Tag table.
- Create the ``estate.property.tag`` model and add the following field:
========================= ========================= =========================
Field Type Attributes
========================= ========================= =========================
name Char required
========================= ========================= =========================
- Add the menus as displayed in this section's **Goal**
- Add the field ``tag_ids`` to your ``estate.property`` model and in its form and tree views
Tip: in the view, use the ``widget="many2many_tags"`` attribute as demonstrated
`here <https://github.com/odoo/odoo/blob/5bb8b927524d062be32f92eb326ef64091301de1/addons/crm_iap_lead_website/views/crm_reveal_views.xml#L36>`__.
The ``widget`` attribute will be explained in detail in :ref:`a later chapter of the training <howto/rdtraining/12_sprinkles>`.
For now, you can try to adding and removing it and see the result ;-)
One2many
========
**Reference**: the documentation related to this topic can be found in
:class:`~odoo.fields.One2many`.
.. note::
**Goal**: at the end of this section:
- a new ``estate.property.offer`` model should be created with the corresponding form and tree view.
- offers should be added to the ``estate.property`` model:
.. image:: 08_relations/media/property_offer.png
:align: center
:alt: Property offers
In our real estate module, we want to define the concept of property offers. A property offer
is an amount a potential buyer offers to the seller. The offer can be lower or higher than the
expected price.
An offer applies to **one** property, but the same property can have **many** offers.
The concept of **many2one** appears once again. However, in this case we want to display the list
of offers for a given property so we will use the **one2many** concept.
A one2many is the inverse of a many2one. For example, we defined
on our test model a link to the ``res.partner`` model thanks to the field ``partner_id``.
We can define the inverse relation, i.e. the list of test models linked to our partner::
test_ids = fields.One2many("test.model", "partner_id", string="Tests")
The first parameter is called the ``comodel`` and the second parameter is the field we want to
inverse.
By convention, one2many fields have the ``_ids`` suffix. They behave as a list of records, meaning
that accessing the data must be done in a loop::
for test in partner.test_ids:
print(test.name)
.. danger::
Because a :class:`~odoo.fields.One2many` is a virtual relationship,
there *must* be a :class:`~odoo.fields.Many2one` field defined in the comodel.
.. exercise:: Add the Real Estate Property Offer table.
- Create the ``estate.property.offer`` model and add the following fields:
========================= ================================ ============= =================
Field Type Attributes Values
========================= ================================ ============= =================
price Float
status Selection no copy Accepted, Refused
partner_id Many2one (``res.partner``) required
property_id Many2one (``estate.property``) required
========================= ================================ ============= =================
- Create a tree view and a form view with the ``price``, ``partner_id`` and ``status`` fields. No
need to create an action or a menu.
- Add the field ``offer_ids`` to your ``estate.property`` model and in its form view as
depicted in this section's **Goal**.
There are several important things to notice here. First, we don't need an action or a menu for all
models. Some models are intended to be accessed only through another model. This is the case in our
exercise: an offer is always accessed through a property.
Second, despite the fact that the ``property_id`` field is required, we did not include it in the
views. How does Odoo know which property our offer is linked to? Well that's part of the
magic of using the Odoo framework: sometimes things are defined implicitly. When we create
a record through a one2many field, the corresponding many2one is populated automatically
for convenience.
Still alive? This chapter is definitely not the easiest one. It introduced a couple of new concepts
while relying on everything that was introduced before. The
:ref:`next chapter <howto/rdtraining/09_compute_onchange>` will be lighter, don't worry ;-)

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,305 @@
.. _howto/rdtraining/09_compute_onchange:
========================================
Chapter 9: Computed Fields And Onchanges
========================================
The :ref:`relations between models <howto/rdtraining/08_relations>` are a key component of any Odoo
module. They are necessary for the modelization of any business case.
However, we may want links between the fields within a given model. Sometimes the
value of one field is determined from the values of other fields and other times we want to help the
user with data entry.
These cases are supported by the concepts of computed fields and onchanges. Although this chapter is
not technically complex, the semantics of both concepts is very important.
This is also the first time we will write Python logic. Until now we haven't written anything
other than class definitions and field declarations.
Computed Fields
===============
**Reference**: the documentation related to this topic can be found in
:ref:`reference/fields/compute`.
.. note::
**Goal**: at the end of this section:
- In the property model, the total area and the best offer should be computed:
.. image:: 09_compute_onchange/media/compute.gif
:align: center
:alt: Compute fields
- In the property offer model, the validity date should be computed and can be updated:
.. image:: 09_compute_onchange/media/compute_inverse.gif
:align: center
:alt: Compute field with inverse
In our real estate module, we have defined the living area as well as the garden area. It is then
natural to define the total area as the sum of both fields. We will use the concept of a computed
field for this, i.e. the value of a given field will be computed from the value of other fields.
So far fields have been stored directly in and retrieved directly from the
database. Fields can also be *computed*. In this case, the field's value is not
retrieved from the database but computed on-the-fly by calling a method of the
model.
To create a computed field, create a field and set its attribute
:attr:`~odoo.fields.Field.compute` to the name of a method. The computation
method should set the value of the computed field for every record in
``self``.
By convention, :attr:`~odoo.fields.Field.compute` methods are private, meaning that they cannot
be called from the presentation tier, only from the business tier (see
:ref:`howto/rdtraining/01_architecture`). Private methods have a name starting with an underscore ``_``.
Dependencies
------------
The value of a computed field usually depends on the values of other fields in
the computed record. The ORM expects the developer to specify those dependencies
on the compute method with the decorator :func:`~odoo.api.depends`.
The given dependencies are used by the ORM to trigger the recomputation of the
field whenever some of its dependencies have been modified::
from odoo import api, fields, models
class TestComputed(models.Model):
_name = "test.computed"
total = fields.Float(compute="_compute_total")
amount = fields.Float()
@api.depends("amount")
def _compute_total(self):
for record in self:
record.total = 2.0 * record.amount
.. note:: ``self`` is a collection.
:class: aphorism
The object ``self`` is a *recordset*, i.e. an ordered collection of
records. It supports the standard Python operations on collections, e.g.
``len(self)`` and ``iter(self)``, plus extra set operations such as ``recs1 |
recs2``.
Iterating over ``self`` gives the records one by one, where each record is
itself a collection of size 1. You can access/assign fields on single
records by using the dot notation, e.g. ``record.name``.
Many examples of computed fields can be found in Odoo.
`Here <https://github.com/odoo/odoo/blob/713dd3777ca0ce9d121d5162a3d63de3237509f4/addons/account/models/account_move.py#L3420-L3423>`__
is a simple one.
.. exercise:: Compute the total area.
- Add the ``total_area`` field to ``estate.property``. It is defined as the sum of the
``living_area`` and the ``garden_area``.
- Add the field in the form view as depicted on the first image of this section's **Goal**.
For relational fields it's possible to use paths through a field as a dependency::
description = fields.Char(compute="_compute_description")
partner_id = fields.Many2one("res.partner")
@api.depends("partner_id.name")
def _compute_description(self):
for record in self:
record.description = "Test for partner %s" % record.partner_id.name
The example is given with a :class:`~odoo.fields.Many2one`, but it is valid for
:class:`~odoo.fields.Many2many` or a :class:`~odoo.fields.One2many`. An example can be found
`here <https://github.com/odoo/odoo/blob/713dd3777ca0ce9d121d5162a3d63de3237509f4/addons/account/models/account_reconcile_model.py#L248-L251>`__.
Let's try it in our module with the following exercise!
.. exercise:: Compute the best offer.
- Add the ``best_price`` field to ``estate.property``. It is defined as the highest (i.e. maximum) of the
offers' ``price``.
- Add the field to the form view as depicted in the first image of this section's **Goal**.
Tip: you might want to try using the :meth:`~odoo.models.BaseModel.mapped` method. See
`here <https://github.com/odoo/odoo/blob/f011c9aacf3a3010c436d4e4f408cd9ae265de1b/addons/account/models/account_payment.py#L686>`__
for a simple example.
Inverse Function
----------------
You might have noticed that computed fields are read-only by default. This is expected since the
user is not supposed to set a value.
In some cases, it might be useful to still be able to set a value directly. In our real estate example,
we can define a validity duration for an offer and set a validity date. We would like to be able
to set either the duration or the date with one impacting the other.
To support this Odoo provides the ability to use an ``inverse`` function::
from odoo import api, fields, models
class TestComputed(models.Model):
_name = "test.computed"
total = fields.Float(compute="_compute_total", inverse="_inverse_total")
amount = fields.Float()
@api.depends("amount")
def _compute_total(self):
for record in self:
record.total = 2.0 * record.amount
def _inverse_total(self):
for record in self:
record.amount = record.total / 2.0
An example can be found
`here <https://github.com/odoo/odoo/blob/2ccf0bd0dcb2e232ee894f07f24fdc26c51835f7/addons/crm/models/crm_lead.py#L308-L317>`__.
A compute method sets the field while an inverse method sets the field's
dependencies.
Note that the ``inverse`` method is called when saving the record, while the
``compute`` method is called at each change of its dependencies.
.. exercise:: Compute a validity date for offers.
- Add the following fields to the ``estate.property.offer`` model:
========================= ========================= =========================
Field Type Default
========================= ========================= =========================
validity Integer 7
date_deadline Date
========================= ========================= =========================
Where ``date_deadline`` is a computed field which is defined as the sum of two fields from
the offer: the ``create_date`` and the ``validity``. Define an appropriate inverse function
so that the user can set either the date or the validity.
Tip: the ``create_date`` is only filled in when the record is created, therefore you will
need a fallback to prevent crashing at time of creation.
- Add the fields in the form view and the list view as depicted on the second image of this section's **Goal**.
Additional Information
----------------------
Computed fields are **not stored** in the database by default. Therefore it is **not
possible** to search on a computed field unless a ``search`` method is defined. This topic is beyond the scope
of this training, so we won't cover it. An example can be found
`here <https://github.com/odoo/odoo/blob/f011c9aacf3a3010c436d4e4f408cd9ae265de1b/addons/event/models/event_event.py#L188>`__.
Another solution is to store the field with the ``store=True`` attribute. While this is
usually convenient, pay attention to the potential computation load added to your model. Lets re-use
our example::
description = fields.Char(compute="_compute_description", store=True)
partner_id = fields.Many2one("res.partner")
@api.depends("partner_id.name")
def _compute_description(self):
for record in self:
record.description = "Test for partner %s" % record.partner_id.name
Every time the partner ``name`` is changed, the ``description`` is automatically recomputed for
**all the records** referring to it! This can quickly become prohibitive to recompute when
millions of records need recomputation.
It is also worth noting that a computed field can depend on another computed field. The ORM is
smart enough to correctly recompute all the dependencies in the right order... but sometimes at the
cost of degraded performance.
In general performance must always be kept in mind when defining computed fields. The more
complex is your field to compute (e.g. with a lot of dependencies or when a computed field
depends on other computed fields), the more time it will take to compute. Always take some time to
evaluate the cost of a computed field beforehand. Most of the time it is only when your code
reaches a production server that you realize it slows down a whole process. Not cool :-(
Onchanges
=========
**Reference**: the documentation related to this topic can be found in
:func:`~odoo.api.onchange`:
.. note::
**Goal**: at the end of this section, enabling the garden will set a default area of 10 and
an orientation to North.
.. image:: 09_compute_onchange/media/onchange.gif
:align: center
:alt: Onchange
In our real estate module, we also want to help the user with data entry. When the 'garden'
field is set, we want to give a default value for the garden area as well as the orientation.
Additionally, when the 'garden' field is unset we want the garden area to reset to zero and the
orientation to be removed. In this case, the value of a given field modifies the value of
other fields.
The 'onchange' mechanism provides a way for the client interface to update a
form without saving anything to the database whenever the user has filled in
a field value. To achieve this, we define a method where ``self`` represents
the record in the form view and decorate it with :func:`~odoo.api.onchange`
to specify which field it is triggered by. Any change you make on
``self`` will be reflected on the form::
from odoo import api, fields, models
class TestOnchange(models.Model):
_name = "test.onchange"
name = fields.Char(string="Name")
description = fields.Char(string="Description")
partner_id = fields.Many2one("res.partner", string="Partner")
@api.onchange("partner_id")
def _onchange_partner_id(self):
self.name = "Document for %s" % (self.partner_id.name)
self.description = "Default description for %s" % (self.partner_id.name)
In this example, changing the partner will also change the name and the description values. It is up to
the user whether or not to change the name and description values afterwards. Also note that we do not
loop on ``self``, this is because the method is only triggered in a form view, where ``self`` is always
a single record.
.. exercise:: Set values for garden area and orientation.
Create an ``onchange`` in the ``estate.property`` model in order to set values for the
garden area (10) and orientation (North) when garden is set to True. When unset, clear the fields.
Additional Information
----------------------
Onchanges methods can also return a non-blocking warning message
(`example <https://github.com/odoo/odoo/blob/cd9af815ba591935cda367d33a1d090f248dd18d/addons/payment_authorize/models/payment.py#L34-L36>`__).
How to use them?
================
There is no strict rule for the use of computed fields and onchanges.
In many cases, both computed fields and onchanges may be used to achieve the same result. Always
prefer computed fields since they are also triggered outside of the context of a form view. Never
ever use an onchange to add business logic to your model. This is a **very bad** idea since
onchanges are not automatically triggered when creating a record programmatically; they are only
triggered in the form view.
The usual pitfall of computed fields and onchanges is trying to be 'too smart' by adding too much
logic. This can have the opposite result of what was expected: the end user is confused from
all the automation.
Computed fields tend to be easier to debug: such a field is set by a given method, so it's easy to
track when the value is set. Onchanges, on the other hand, may be confusing: it is very difficult to
know the extent of an onchange. Since several onchange methods may set the same fields, it
easily becomes difficult to track where a value is coming from.
When using stored computed fields, pay close attention to the dependencies. When computed fields
depend on other computed fields, changing a value can trigger a large number of recomputations.
This leads to poor performance.
In the :ref:`next chapter<howto/rdtraining/10_actions>`, we'll see how we can trigger some business
logic when buttons are clicked.

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,139 @@
.. _howto/rdtraining/10_actions:
==================================
Chapter 10: Ready For Some Action?
==================================
So far we have mostly built our module by declaring fields and views. We just introduced business
logic in the :ref:`previous chapter <howto/rdtraining/09_compute_onchange>` thanks to computed fields
and onchanges. In any real business scenario, we would want to link some business logic to action buttons.
In our real estate example, we would like to be able to:
- cancel or set a property as sold
- accept or refuse an offer
One could argue that we can already do these things by changing the state manually, but
this is not really convenient. Moreover, we want to add some extra processing: when an offer is
accepted we want to set the selling price and the buyer for the property.
Action Type
===========
**Reference**: the documentation related to this topic can be found in
:ref:`reference/actions` and :ref:`reference/exceptions`.
.. note::
**Goal**: at the end of this section:
- You should be able to cancel or set a property as sold:
.. image:: 10_actions/media/property.gif
:align: center
:alt: Cancel and set to sold
A canceled property cannot be sold and a sold property cannot be canceled. For the sake of
clarity, the ``state`` field has been added on the view.
- You should be able to accept or refuse an offer:
.. image:: 10_actions/media/offer_01.gif
:align: center
:alt: Accept or refuse an offer
- Once an offer is accepted, the selling price and the buyer should be set:
.. image:: 10_actions/media/offer_02.gif
:align: center
:alt: Accept an offer
In our real estate module, we want to link business logic with some buttons. The most common way to
do this is to:
- Add a button in the view, for example in the ``header`` of the view:
.. code-block:: xml
<form>
<header>
<button name="action_do_something" type="object" string="Do Something"/>
</header>
<sheet>
<field name="name"/>
</sheet>
</form>
- and link this button to business logic:
.. code-block:: python
from odoo import fields, models
class TestAction(models.Model):
_name = "test.action"
name = fields.Char()
def action_do_something(self):
for record in self:
record.name = "Something"
return True
By assigning ``type="object"`` to our button, the Odoo framework will execute a Python method
with ``name="action_do_something"`` on the given model.
The first important detail to note is that our method name isn't prefixed with an underscore
(``_``). This makes our method a **public** method, which can be called directly from the Odoo
interface (through an RPC call). Until now, all methods we created (compute, onchange) were called
internally, so we used **private** methods prefixed by an underscore. You should always define your
methods as private unless they need to be called from the user interface.
Also note that we loop on ``self``. Always assume that a method can be called on multiple records; it's
better for reusability.
Finally, a public method should always return something so that it can be called through XML-RPC.
When in doubt, just ``return True``.
There are hundreds of examples in the Odoo source code. One example is this
`button in a view <https://github.com/odoo/odoo/blob/cd9af815ba591935cda367d33a1d090f248dd18d/addons/crm/views/crm_lead_views.xml#L9-L11>`__
and its
`corresponding Python method <https://github.com/odoo/odoo/blob/cd9af815ba591935cda367d33a1d090f248dd18d/addons/crm/models/crm_lead.py#L746-L760>`__
.. exercise:: Cancel and set a property as sold.
- Add the buttons 'Cancel' and 'Sold' to the ``estate.property`` model. A canceled property
cannot be set as sold, and a sold property cannot be canceled.
Refer to the first image of the **Goal** for the expected result.
Tip: in order to raise an error, you can use the :ref:`UserError<reference/exceptions>`
function. There are plenty of examples in the Odoo source code ;-)
- Add the buttons 'Accept' and 'Refuse' to the ``estate.property.offer`` model.
Refer to the second image of the **Goal** for the expected result.
Tip: to use an icon as a button, have a look
`at this example <https://github.com/odoo/odoo/blob/cd9af815ba591935cda367d33a1d090f248dd18d/addons/event/views/event_views.xml#L521>`__.
- When an offer is accepted, set the buyer and the selling price for the corresponding property.
Refer to the third image of the **Goal** for the expected result.
Pay attention: in real life only one offer can be accepted for a given property!
Object Type
===========
In :ref:`howto/rdtraining/06_firstui`, we created an action that was linked to a menu.
You may be wondering if it is possible to link an action to a button. Good news, it is! One
way to do it is:
.. code-block:: xml
<button type="action" name="%(test.test_model_action)d" string="My Action"/>
We use ``type="action"`` and we refer to the :term:`external identifier` in the ``name``.
In the :ref:`next chapter <howto/rdtraining/11_constraints>` we'll see how we can prevent encoding
incorrect data in Odoo.

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -0,0 +1,129 @@
.. _howto/rdtraining/11_constraints:
=======================
Chapter 11: Constraints
=======================
The :ref:`previous chapter <howto/rdtraining/10_actions>` introduced the ability to add some
business logic to our model. We can now link buttons to business code, but how can we prevent
users from entering incorrect data? For example, in our real estate module nothing prevents
users from setting a negative expected price.
Odoo provides two ways to set up automatically verified invariants:
:func:`Python constraints <odoo.api.constrains>` and
:attr:`SQL constraints <odoo.models.Model._sql_constraints>`.
SQL
===
**Reference**: the documentation related to this topic can be found in
:ref:`reference/orm/models` and in the `PostgreSQL's documentation`_.
.. note::
**Goal**: at the end of this section:
- Amounts should be (strictly) positive
.. image:: 11_constraints/media/sql_01.gif
:align: center
:alt: Constraints on amounts
- Property types and tags should have a unique name
.. image:: 11_constraints/media/sql_02.gif
:align: center
:alt: Constraints on names
SQL constraints are defined through the model attribute
:attr:`~odoo.models.Model._sql_constraints`. This attribute is assigned a list
of triples containing strings ``(name, sql_definition, message)``, where ``name`` is a
valid SQL constraint name, ``sql_definition`` is a table_constraint_ expression
and ``message`` is the error message.
You can find a simple example
`here <https://github.com/odoo/odoo/blob/24b0b6f07f65b6151d1d06150e376320a44fd20a/addons/analytic/models/analytic_account.py#L20-L23>`__.
.. exercise:: Add SQL constraints.
Add the following constraints to their corresponding models:
- A property expected price must be strictly positive
- A property selling price must be positive
- An offer price must be strictly positive
- A property tag name and property type name must be unique
Tip: search for the ``unique`` keyword in the Odoo codebase for examples of unique names.
Restart the server with the ``-u estate`` option to see the result. Note that you might have data
that prevents a SQL constraint from being set. An error message similar to the following might pop up:
.. code-block:: text
ERROR rd-demo odoo.schema: Table 'estate_property_offer': unable to add constraint 'estate_property_offer_check_price' as CHECK(price > 0)
For example, if some offers have a price of zero, then the constraint can't be applied. You can delete
the problematic data in order to apply the new constraints.
Python
======
**Reference**: the documentation related to this topic can be found in
:func:`~odoo.api.constrains`.
.. note::
**Goal**: at the end of this section, it will not possible to accept an offer
lower than 90% of the expected price.
.. image:: 11_constraints/media/python.gif
:align: center
:alt: Python constraint
SQL constraints are an efficient way of ensuring data consistency. However it may be necessary
to make more complex checks which require Python code. In this case we need a Python constraint.
A Python constraint is defined as a method decorated with
:func:`~odoo.api.constrains` and is invoked on a recordset. The decorator
specifies which fields are involved in the constraint. The constraint is automatically evaluated
when any of these fields are modified . The method is expected to
raise an exception if its invariant is not satisfied::
from odoo.exceptions import ValidationError
...
@api.constrains('date_end')
def _check_date_end(self):
for record in self:
if record.date_end < fields.Date.today():
raise ValidationError("The end date cannot be set in the past")
# all records passed the test, don't return anything
A simple example can be found
`here <https://github.com/odoo/odoo/blob/3783654b87851bdeb11e32da78bb5b62865b869a/addons/account/models/account_payment_term.py#L104-L108>`__.
.. exercise:: Add Python constraints.
Add a constraint so that the selling price cannot be lower than 90% of the expected price.
Tip: the selling price is zero until an offer is validated. You will need to fine tune your
check to take this into account.
.. warning::
Always use the :meth:`~odoo.tools.float_utils.float_compare` and
:meth:`~odoo.tools.float_utils.float_is_zero` methods when working with floats!
Ensure the constraint is triggered every time the selling price or the expected price is changed!
SQL constraints are usually more efficient than Python constraints. When performance matters, always
prefer SQL over Python constraints.
Our real estate module is starting to look good. We added some business logic, and now we make sure
the data is consistent. However, the user interface is still a bit rough. Let's see how we can
improve it in the :ref:`next chapter <howto/rdtraining/12_sprinkles>`.
.. _PostgreSQL's documentation:
.. _table_constraint:
https://www.postgresql.org/docs/current/ddl-constraints.html

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@ -0,0 +1,523 @@
.. _howto/rdtraining/12_sprinkles:
=============================
Chapter 12: Add The Sprinkles
=============================
Our real estate module now makes sense from a business perspective. We created
:ref:`specific views <howto/rdtraining/07_basicviews>`, added several
:ref:`action buttons <howto/rdtraining/10_actions>` and
:ref:`constraints <howto/rdtraining/11_constraints>`. However our user interface is still a bit
rough. We would like to add some colors to the list views and make some fields and buttons conditionally
disappear. For example, the 'Sold' and 'Cancel' buttons should disappear when the property
is sold or canceled since it is no longer allowed to change the state at this point.
This chapter covers a very small subset of what can be done in the views. Do not hesitate to
read the reference documentation for a more complete overview.
**Reference**: the documentation related to this chapter can be found in
:ref:`reference/views`.
Inline Views
============
.. note::
**Goal**: at the end of this section, a specific list of properties should be added to the property
type view:
.. image:: 12_sprinkles/media/inline_view.png
:align: center
:alt: Inline list view
In the real estate module we added a list of offers for a property. We simply added the field
``offer_ids`` with:
.. code-block:: xml
<field name="offer_ids"/>
The field uses the specific view for ``estate.property.offer``. In some cases we want to define
a specific list view which is only used in the context of a form view. For example, we would like
to display the list of properties linked to a property type. However, we only want to display 3
fields for clarity: name, expected price and state.
To do this, we can define *inline* list views. An inline list view is defined directly inside
a form view. For example:
.. code-block:: python
from odoo import fields, models
class TestModel(models.Model):
_name = "test.model"
_description = "Test Model"
description = fields.Char()
line_ids = fields.One2many("test.model.line", "model_id")
class TestModelLine(models.Model):
_name = "test.model.line"
_description = "Test Model Line"
model_id = fields.Many2one("test.model")
field_1 = fields.Char()
field_2 = fields.Char()
field_3 = fields.Char()
.. code-block:: xml
<form>
<field name="description"/>
<field name="line_ids">
<tree>
<field name="field_1"/>
<field name="field_2"/>
</tree>
</field>
</form>
In the form view of the ``test.model``, we define a specific list view for ``test.model.line``
with fields ``field_1`` and ``field_2``.
An example can be found
`here <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/event/views/event_tag_views.xml#L27-L33>`__.
.. exercise:: Add an inline list view.
- Add the ``One2many`` field ``property_ids`` to the ``estate.property.type`` model.
- Add the field in the ``estate.property.type`` form view as depicted in the **Goal** of this
section.
Widgets
=======
**Reference**: the documentation related to this section can be found in
:ref:`reference/js/widgets`.
.. note::
**Goal**: at the end of this section, the state of the property should be displayed using a
specific widget:
.. image:: 12_sprinkles/media/widget.png
:align: center
:alt: Statusbar widget
Four states are displayed: New, Offer Received, Offer Accepted and Sold.
Whenever we've added fields to our models, we've (almost) never had to worry about how
these fields would look like in the user interface. For example, a date picker is provided
for a ``Date`` field and a ``One2many`` field is automatically displayed as a list. Odoo
chooses the right 'widget' depending on the field type.
However, in some cases, we want a specific representation of a field which can be done thanks to
the ``widget`` attribute. We already used it for the ``tag_ids`` field when we used the
``widget="many2many_tags"`` attribute. If we hadn't used it, then the field would have displayed as a
list.
Each field type has a set of widgets which can be used to fine tune its display. Some widgets also
take extra options. An exhaustive list can be found in :ref:`reference/js/widgets`.
.. exercise:: Use the status bar widget.
Use the ``statusbar`` widget in order to display the ``state`` of the ``estate.property`` as
depicted in the **Goal** of this section.
Tip: a simple example can be found
`here <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/account/views/account_bank_statement_views.xml#L136>`__.
.. warning:: Same field multiple times in a view
Add a field only **once** to a list or a form view. Adding it multiple times is
not supported.
List Order
==========
**Reference**: the documentation related to this section can be found in
:ref:`reference/orm/models`.
.. note::
**Goal**: at the end of this section, all lists should display by default in a deterministic
order. Property types can be ordered manually.
During the previous exercises, we created several list views. However, at no point did we specify
which order the records had to be listed in by default. This is a very important thing for many business
cases. For example, in our real estate module we would want to display the highest offers on top of the
list.
Model
-----
Odoo provides several ways to set a default order. The most common way is to define
the ``_order`` attribute directly in the model. This way, the retrieved records will follow
a deterministic order which will be consistent in all views including when records are searched
programmatically. By default there is no order specified, therefore the records will be
retrieved in a non-deterministic order depending on PostgreSQL.
The ``_order`` attribute takes a string containing a list of fields which will be used for sorting.
It will be converted to an order_by_ clause in SQL. For example:
.. code-block:: python
from odoo import fields, models
class TestModel(models.Model):
_name = "test.model"
_description = "Test Model"
_order = "id desc"
description = fields.Char()
Our records are ordered by descending ``id``, meaning the highest comes first.
.. exercise:: Add model ordering.
Define the following orders in their corresponding models:
=================================== ===================================
Model Order
=================================== ===================================
``estate.property`` Descending ID
``estate.property.offer`` Descending Price
``estate.property.tag`` Name
``estate.property.type`` Name
=================================== ===================================
View
----
Ordering is possible at the model level. This has the advantage of a consistent order everywhere
a list of records is retrieved. However, it is also possible to define a specific order directly
in a view thanks to the ``default_order`` attribute
(`example <https://github.com/odoo/odoo/blob/892dd6860733c46caf379fd36f57219082331b66/addons/crm/report/crm_activity_report_views.xml#L30>`__).
Manual
------
Both model and view ordering allow flexibility when sorting records, but there is still one case
we need to cover: the manual ordering. A user may want to sort records depending on the business
logic. For example, in our real estate module we would like to sort the property types manually.
It is indeed useful to have the most used types appear at the top of the list. If our real estate
agency mainly sells houses, it is more convenient to have 'House' appear before 'Apartment'.
To do so, a ``sequence`` field is used in combination with the ``handle`` widget. Obviously
the ``sequence`` field must be the first field in the ``_order`` attribute.
.. exercise:: Add manual ordering.
- Add the following field:
=================================== ======================= =======================
Model Field Type
=================================== ======================= =======================
``estate.property.type`` Sequence Integer
=================================== ======================= =======================
- Add the sequence to the ``estate.property.type`` list view with the correct widget.
Tip: you can find an example here:
`model <https://github.com/odoo/odoo/blob/892dd6860733c46caf379fd36f57219082331b66/addons/crm/models/crm_stage.py#L36>`__
and
`view <https://github.com/odoo/odoo/blob/892dd6860733c46caf379fd36f57219082331b66/addons/crm/views/crm_stage_views.xml#L23>`__.
Attributes and options
======================
It would be prohibitive to detail all the available features which allow fine tuning of the look of a
view. Therefore, we'll stick to the most common ones.
Form
----
.. note::
**Goal**: at the end of this section, the property form view will have:
- Conditional display of buttons and fields
- Tag colors
.. image:: 12_sprinkles/media/form.gif
:align: center
:alt: Form view with sprinkles
In our real estate module, we want to modify the behavior of some fields. For example, we don't
want to be able to create or edit a property type from the form view. Instead we expect the
types to be handled in their appropriate menu. We also want to give tags a color. In order to add these
behavior customizations, we can add the ``options`` attribute to several field widgets.
.. exercise:: Add widget options.
- Add the appropriate option to the ``property_type_id`` field to prevent the creation and the
editing of a property type from the property form view. Have a look at the
:ref:`Many2one widget documentation <reference/js/widgets>` for more info.
- Add the following field:
=================================== ======================= =======================
Model Field Type
=================================== ======================= =======================
``estate.property.tag`` Color Integer
=================================== ======================= =======================
Then add the appropriate option to the ``tag_ids`` field to add a color picker on the tags.
Have a look at the :ref:`FieldMany2ManyTags widget documentation <reference/js/widgets>`
for more info.
In :ref:`howto/rdtraining/06_firstui`, we saw that reserved fields were used for
specific behaviors. For example, the ``active`` field is used to automatically filter out
inactive records. We added the ``state`` as a reserved field as well. It's now time to use it!
A ``state`` field is used in combination with a ``states`` attribute in the view to display
buttons conditionally.
.. exercise:: Add conditional display of buttons.
Use the ``states`` attribute to display the header buttons conditionally as depicted
in this section's **Goal** (notice how the 'Sold' and 'Cancel' buttons change when the state is modified).
Tip: do not hesitate to search for ``states=`` in the Odoo XML files for some examples.
More generally, it is possible to make a field ``invisible``, ``readonly`` or ``required`` based
on the value of other fields thanks to the ``attrs`` attribute. Note that ``invisible`` can also be applied
to other elements of the view such as ``button`` or ``group``.
The ``attrs`` is a dictionary with the property as a key and a domain as a value. The domain gives
the conditon in which the property applies. For example:
.. code-block:: xml
<form>
<field name="description" attrs="{'invisible': [('is_partner', '=', False)]}"/>
<field name="is_partner" invisible="1"/>
</form>
This means that the ``description`` field is invisible when ``is_partner`` is ``False``. It is
important to note that a field used in an ``attrs`` **must** be present in the view. If it
should not be displayed to the user, we can use the ``invisible`` attribute to hide it.
.. exercise:: Use ``attrs``.
- Make the garden area and orientation invisible in the ``estate.property`` form view when
there is no garden.
- Make the 'Accept' and 'Refuse' buttons invisible once the offer state is set.
- Do not allow adding an offer when the property state is 'Offer Accepted', 'Sold' or
'Canceled'. To do this use the ``readonly`` ``attrs``.
.. warning::
Using a (conditional) ``readonly`` attribute in the view can be useful to prevent data entry
errors, but keep in mind that it doesn't provide any level of security! There is no check done
server-side, therefore it's always possible to write on the field through a RPC call.
List
----
.. note::
**Goal**: at the end of this section, the property and offer list views should have color decorations.
Additionally, offers and tags will be editable directly in the list, and the availability date will be
hidden by default.
.. image:: 12_sprinkles/media/decoration.png
:align: center
:alt: List view with decorations and optional field
.. image:: 12_sprinkles/media/editable_list.gif
:align: center
:alt: Editable list
When the model only has a few fields, it can be useful to edit records directly through the list
view and not have to open the form view. In the real estate example, there is no need to open a form view
to add an offer or create a new tag. This can be achieved thanks to the ``editable`` attribute.
.. exercise:: Make list views editable.
Make the ``estate.property.offer`` and ``estate.property.tag`` list views editable.
On the other hand, when a model has a lot of fields it can be tempting to add too many fields in the
list view and make it unclear. An alternative method is to add the fields, but make them optionally
hidden. This can be achieved thanks to the ``optional`` attribute.
.. exercise:: Make a field optional.
Make the field ``date_availability`` on the ``estate.property`` list view optional and hidden by
default.
Finally, color codes are useful to visually emphasize records. For example, in the real estate
module we would like to display refused offers in red and accepted offers in green. This can be achieved
thanks to the ``decoration-{$name}`` attribute (see :ref:`reference/js/widgets` for a
complete list):
.. code-block:: xml
<tree decoration-success="is_partner==True">
<field name="name">
<field name="is_partner" invisible="1">
</tree>
The records where ``is_partner`` is ``True`` will be displayed in green.
.. exercise:: Add some decorations.
On the ``estate.property`` list view:
- Properties with an offer received are green
- Properties with an offer accepted are green and bold
- Properties sold are muted
On the ``estate.property.offer`` list view:
- Refused offers are red
- Accepted offers are green
- The state should not be visible anymore
Tips:
- Keep in mind that **all** fields used in attributes must be in the view!
- If you want to test the color of the "Offer Received" and "Offer Accepted" states, add the
field in the form view and change it manually (we'll implement the business logic for this later).
Search
------
**Reference**: the documentation related to this section can be found in
:ref:`reference/views/search` and :ref:`reference/views/search/defaults`.
.. note::
**Goal**: at the end of this section, the available properties will be filtered by default,
and searching on the living area returns results where the area is larger than the given
number.
.. image:: 12_sprinkles/media/search.gif
:align: center
:alt: Default filters and domains
Last but not least, there are some tweaks we would like to apply when searching. First of all, we
want to have our 'Available' filter applied by default when we access the properties. To make this happen, we
need to use the ``search_default_{$name}`` action context, where ``{$name}`` is the filter name.
This means that we can define which filter(s) will be activated by default at the action level.
Here is an example of an
`action <https://github.com/odoo/odoo/blob/6decc32a889b46947db6dd4d42ef995935894a2a/addons/crm/report/crm_opportunity_report_views.xml#L115>`__
with its
`corresponding filter <https://github.com/odoo/odoo/blob/6decc32a889b46947db6dd4d42ef995935894a2a/addons/crm/report/crm_opportunity_report_views.xml#L68>`__.
.. exercise:: Add a default filter.
Make the 'Available' filter selected by default in the ``estate.property`` action.
Another useful improvement in our module would be the ability to search efficiently by living area.
In practice, a user will want to search for properties of 'at least' the given area. It is unrealistic
to expect users would want to find a property of an exact living area. It is always
possible to make a custom search, but that's inconvenient.
Search view ``<field>`` elements can have a ``filter_domain`` that overrides
the domain generated for searching on the given field. In the given domain,
``self`` represents the value entered by the user. In the example below, it is
used to search on both ``name`` and ``description`` fields.
.. code-block:: xml
<search string="Test">
<field name="description" string="Name and description"
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
</group>
</search>
.. exercise:: Change the living area search.
Add a ``filter_domain`` to the living area to include properties with an area equal to or
greater than the given value.
Stat Buttons
============
.. note::
**Goal**: at the end of this section, there will be a stat button on the property type form view
which shows the list of all offers related to properties of the given type when it is clicked on.
.. image:: 12_sprinkles/media/stat_button.gif
:align: center
:alt: Stat button
If you've already used some functional modules in Odoo, you've probably already encountered a 'stat
button'. These buttons are displayed on the top right of a form view and give a quick access to
linked documents. In our real estate module, we would like to have a quick link to the offers
related to a given property type as depicted in the **Goal** of this section.
At this point of the tutorial we have already seen most of the concepts to do this. However,
there is not a single solution and it can still be confusing if you don't know where to start from.
We'll describe a step-by-step solution in the exercise. It can always be useful to find some
examples in the Odoo codebase by looking for ``oe_stat_button``.
The following exercise might be a bit more difficult than the previous ones since it assumes you
are able to search for examples in the source code on your own. If you are stuck there is probably
someone nearby who can help you ;-)
The exercise introduces the concept of :ref:`reference/fields/related`. The easiest way to
understand it is to consider it as a specific case of a computed field. The following definition
of the ``description`` field:
.. code-block:: python
...
partner_id = fields.Many2one("res.partner", string="Partner")
description = fields.Char(related="partner_id.name")
is equivalent to:
.. code-block:: python
...
partner_id = fields.Many2one("res.partner", string="Partner")
description = fields.Char(compute="_compute_description")
@api.depends("partner_id.name")
def _compute_description(self):
for record in self:
record.description = record.partner_id.name
Every time the partner name is changed, the description is modified.
.. exercise:: Add a stat button to property type.
- Add the field ``property_type_id`` to ``estate.property.offer``. We can define it as a
related field on ``property_id.property_type_id`` and set it as stored.
Thanks to this field, an offer will be linked to a property type when it's created. You can add
the field to the list view of offers to make sure it works.
- Add the field ``offer_ids`` to ``estate.property.type`` which is the One2many inverse of
the field defined in the previous step.
- Add the field ``offer_count`` to ``estate.property.type``. It is a computed field that counts
the number of offers for a given property type (use ``offer_ids`` to do so).
At this point, you have all the information necessary to know how many offers are linked to
a property type. When in doubt, add ``offer_ids`` and ``offer_count`` directly to the view.
The next step is to display the list when clicking on the stat button.
- Create a stat button on ``estate.property.type`` pointing to the ``estate.property.offer``
action. This means you should use the ``type="action"`` attribute (go back to the end of
:ref:`howto/rdtraining/10_actions` if you need a refresher).
At this point, clicking on the stat button should display all offers. We still need to filter out the
offers.
- On the ``estate.property.offer`` action, add a domain that defines ``property_type_id``
as equal to the ``active_id`` (= the current record,
`here is an example <https://github.com/odoo/odoo/blob/df37ce50e847e3489eb43d1ef6fc1bac6d6af333/addons/event/views/event_views.xml#L162>`__)
Looking good? If not, don't worry, the :ref:`next chapter <howto/rdtraining/13_inheritance>` doesn't
require stat buttons ;-)
.. _order_by:
https://www.postgresql.org/docs/current/queries-order.html

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,244 @@
.. _howto/rdtraining/13_inheritance:
=======================
Chapter 13: Inheritance
=======================
A powerful aspect of Odoo is its modularity. A module is dedicated to a business need, but
modules can also interact with one another. This is useful for extending the functionality of an existing
module. For example, in our real estate scenario we want to display the list of a salesperson's properties
directly in the regular user view.
But before going through the specific Odoo module inheritance, let's see how we can alter the
behavior of the standard CRUD (Create, Retrieve, Update or Delete) methods.
Python Inheritance
==================
.. note::
**Goal**: at the end of this section:
- It should not be possible to delete a property which is not new or canceled.
.. image:: 13_inheritance/media/unlink.gif
:align: center
:alt: Unlink
- When an offer is created, the property state should change to 'Offer Received'
- It should not be possible to create an offer with a lower price than an existing offer
.. image:: 13_inheritance/media/create.gif
:align: center
:alt: Create
In our real estate module, we never had to develop anything specific to be able to do the
standard CRUD actions. The Odoo framework provides the necessary
tools to do them. In fact, such actions are already included in our model thanks to classical
Python inheritance::
from odoo import fields, models
class TestModel(models.Model):
_name = "test.model"
_description = "Test Model"
...
Our ``class TestModel`` inherits from :class:`~odoo.models.Model` which provides
:meth:`~odoo.models.Model.create`, :meth:`~odoo.models.Model.read`, :meth:`~odoo.models.Model.write`
and :meth:`~odoo.models.Model.unlink`.
These methods (and any other method defined on :class:`~odoo.models.Model`) can be extended to add
specific business logic::
from odoo import fields, models
class TestModel(models.Model):
_name = "test.model"
_description = "Test Model"
...
@api.model
def create(self, vals):
# Do some business logic, modify vals...
...
# Then call super to execute the parent method
return super().create(vals)
The decorator :func:`~odoo.api.model` is necessary for the :meth:`~odoo.models.Model.create`
method because the content of the recordset ``self`` is not relevant in the context of creation,
but it is not necessary for the other CRUD methods.
In Python 3, ``super()`` is equivalent to ``super(TestModel, self)``. The latter may be necessary
when you need to call the parent method with a modified recordset.
.. danger::
- It is very important to **always** call ``super()`` to avoid breaking the flow. There are
only a few very specific cases where you don't want to call it.
- Make sure to **always** return data consistent with the parent method. For example, if
the parent method returns a ``dict()``, your override must also return a ``dict()``.
.. exercise:: Add business logic to the CRUD methods.
- Prevent deletion of a property if its state is not 'New' or 'Canceled'
Tip: override :meth:`~odoo.models.Model.unlink` and remember that ``self`` can be a recordset
with more than one record.
- At offer creation, set the property state to 'Offer Received'. Also raise an error if the user
tries to create an offer with a lower amount than an existing offer.
Tip: the ``property_id`` field is available in the ``vals``, but it is an ``int``. To
instantiate an ``estate.property`` object, use ``self.env[model_name].browse(value)``
(`example <https://github.com/odoo/odoo/blob/136e4f66cd5cafe7df450514937c7218c7216c93/addons/gamification/models/badge.py#L57>`__)
Model Inheritance
=================
**Reference**: the documentation related to this topic can be found in
:ref:`reference/orm/inheritance`.
In our real estate module, we would like to display the list of properties linked to a salesperson
directly in the Settings / Users & Companies / Users form view. To do this, we need to add a field to
the ``res.users`` model and adapt its view to show it.
Odoo provides two *inheritance* mechanisms to extend an existing model in a modular way.
The first inheritance mechanism allows modules to modify the behavior of a model defined in an
another module by:
- adding fields to the model,
- overriding the definition of fields in the model,
- adding constraints to the model,
- adding methods to the model,
- overriding existing methods in the model.
The second inheritance mechanism (delegation) allows every record of a model to be linked
to a parent model's record and provides transparent access to the
fields of this parent record.
.. image:: 13_inheritance/media/inheritance_methods.png
:align: center
:alt: Inheritance Methods
In Odoo, the first mechanism is by far the most used. In our case, we want to add a field to an
existing model, which means we will use the first mechanism. For example::
from odoo import fields, models
class InheritedModel(models.Model):
_inherit = "inherited.model"
new_field = fields.Char(string="New Field")
A practical example where two fields are added to
a model can be found
`here <https://github.com/odoo/odoo/blob/60e9410e9aa3be4a9db50f6f7534ba31fea3bc29/addons/account_fleet/models/account_move.py#L39-L47>`__.
By convention, each inherited model is defined in its own Python file. In our example, it would be
``models/inherited_model.py``.
.. exercise:: Add a field to Users.
- Add the following field to ``res.users``:
===================== ======================================================
Field Type
===================== ======================================================
property_ids One2many inverse of ``user_id`` to ``estate.property``
===================== ======================================================
- Add a domain to the field so it only lists the available properties.
Now let's add the field to the view and check that everything is working well!
View Inheritance
================
**Reference**: the documentation related to this topic can be found in
:ref:`reference/views/inheritance`.
.. note::
**Goal**: at the end of this section, the list of available properties linked
to a salesperson should be displayed in their user form view
.. image:: 13_inheritance/media/users.png
:align: center
:alt: Users
Instead of modifying existing views in place (by overwriting them), Odoo
provides view inheritance where children 'extension' views are applied on top of
root views. These extension can both add and remove content from their parent view.
An extension view references its parent using the ``inherit_id`` field.
Instead of a single view, its ``arch`` field contains a number of
``xpath`` elements that select and alter the content of their parent view:
.. code-block:: xml
<record id="inherited_model_view_form" model="ir.ui.view">
<field name="name">inherited.model.form.inherit.test</field>
<field name="model">inherited.model</field>
<field name="inherit_id" ref="inherited.inherited_model_view_form"/>
<field name="arch" type="xml">
<!-- find field description and add the field
new_field after it -->
<xpath expr="//field[@name='description']" position="after">
<field name="new_field"/>
</xpath>
</field>
</record>
``expr``
An XPath_ expression selecting a single element in the parent view.
Raises an error if it matches no element or more than one
``position``
Operation to apply to the matched element:
``inside``
appends ``xpath``'s body to the end of the matched element
``replace``
replaces the matched element with the ``xpath``'s body, replacing any ``$0`` node occurrence
in the new body with the original element
``before``
inserts the ``xpath``'s body as a sibling before the matched element
``after``
inserts the ``xpaths``'s body as a sibling after the matched element
``attributes``
alters the attributes of the matched element using the special
``attribute`` elements in the ``xpath``'s body
When matching a single element, the ``position`` attribute can be set directly
on the element to be found. Both inheritances below have the same result.
.. code-block:: xml
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" />
</xpath>
<field name="description" position="after">
<field name="idea_ids" />
</field>
An example of a view inheritance extension can be found
`here <https://github.com/odoo/odoo/blob/691d1f087040f1ec7066e485d19ce3662dfc6501/addons/account_fleet/views/account_move_views.xml#L3-L17>`__.
.. exercise:: Add fields to the Users view.
Add the ``property_ids`` field to the ``base.view_users_form`` in a new notebook page.
Tip: an example an inheritance of the users' view can be found
`here <https://github.com/odoo/odoo/blob/691d1f087040f1ec7066e485d19ce3662dfc6501/addons/gamification/views/res_users_views.xml#L5-L14>`__.
Inheritance is extensively used in Odoo due to its modular concept. Do not hesitate to read
the corresponding documentation for more info!
In the :ref:`next chapter <howto/rdtraining/14_other_module>`, we will learn how to interact with
other modules.
.. _XPath: https://w3.org/TR/xpath

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,172 @@
.. _howto/rdtraining/14_other_module:
=======================================
Chapter 14: Interact With Other Modules
=======================================
In the :ref:`previous chapter <howto/rdtraining/13_inheritance>`, we used inheritance to modify
the behavior of a module. In our real estate scenario, we would like to go a step further
and be able to generate invoices for our customers. Odoo provides an Invoicing module, so it
would be neat to create an invoice directly from our real estate module, i.e. once a property
is set to 'Sold', an invoice is created in the Invoicing application.
Concrete Example: Account Move
==============================
.. note::
**Goal**: at the end of this section:
- A new module ``estate_account`` should be created
- When a property is sold, an invoice should be issued for the buyer
.. image:: 14_other_module/media/create_inv.gif
:align: center
:alt: Invoice creation
Any time we interact with another module, we need to keep in mind the modularity. If we intend
to sell our application to real estate agencies, some may want the invoicing feature but
others may not want it.
Link Module
-----------
The common approach for such use cases is to create a 'link' module. In our case, the module
would depend on ``estate`` and ``account`` and would include the invoice creation logic
of the estate property. This way the real estate and the accounting modules can be installed
independently. When both are installed, the link module provides the new feature.
.. exercise:: Create a link module.
Create the ``estate_account`` module, which depends on the ``estate`` and ``account`` modules.
For now, it will be an empty shell.
Tip: you already did this at the
:ref:`beginning of the tutorial <howto/rdtraining/03_newapp>`. The process is very similar.
When the ``estate_account`` module appears in the list, go ahead and install it! You'll notice that
the Invoicing application is installed as well. This is expected since your module depends on it.
If you uninstall the Invoicing application, your module will be uninstalled as well.
.. _howto/rdtraining/14_other_module/create:
Invoice Creation
----------------
It's now time to generate the invoice. We want to add functionality to the
``estate.property`` model, i.e. we want to add some extra logic for when a property is sold.
Does that sound familiar? If not, it's a good idea to go back to the
:ref:`previous chapter <howto/rdtraining/13_inheritance>` since you might have missed something ;-)
As a first step, we need to extend the action called when pressing the
:ref:`'Sold' button <howto/rdtraining/10_actions>` on a property. To do so, we need to create a
:ref:`model inheritance <howto/rdtraining/13_inheritance>` in the ``estate_account`` module
for the ``estate.property`` model. For now, the overridden action will simply return the ``super``
call. Maybe an example will make things clearer::
from odoo import models
class InheritedModel(models.Model):
_inherit = "inherited.model"
def inherited_action(self):
return super().inherited_action()
A practical example can be found
`here <https://github.com/odoo/odoo/blob/f1f48cdaab3dd7847e8546ad9887f24a9e2ed4c1/addons/event_sale/models/account_move.py#L7-L16>`__.
.. exercise:: Add the first step of invoice creation.
- Create a ``estate_property.py`` file in the correct folder of the ``estate_account`` module.
- ``_inherit`` the ``estate.property`` model.
- Override the ``action_sold`` method (you might have named it differently) to return the ``super``
call.
Tip: to make sure it works, add a ``print`` or a debugger breakpoint in the overridden method.
Is it working? If not, maybe check that all Python files are correctly imported.
If the override is working, we can move forward and create the invoice. Unfortunately, there
is no easy way to know how to create any given object in Odoo. Most of the time, it is necessary
to have a look at its model to find the required fields and provide appropriate values.
A good way to learn is to look at how other modules already do what you want to do. For example, one of
the basic flows of Sales is the creation of an invoice from a sales order. This looks like a good
starting point since it does exactly what we want to do. Take some time to read and understand the
`_create_invoices <https://github.com/odoo/odoo/blob/f1f48cdaab3dd7847e8546ad9887f24a9e2ed4c1/addons/sale/models/sale.py#L610-L717>`__
method. When you are done crying because this simple task looks awfully complex, we can move
forward in the tutorial.
To create an invoice, we need the following information:
- a ``partner_id``: the customer
- a ``move_type``: it has several `possible values <https://github.com/odoo/odoo/blob/f1f48cdaab3dd7847e8546ad9887f24a9e2ed4c1/addons/account/models/account_move.py#L138-L147>`__
- a ``journal_id``: the accounting journal
This is enough to create an empty invoice.
.. exercise:: Add the second step of invoice creation.
Create an empty ``account.move`` in the override of the ``action_sold`` method:
- the ``partner_id`` is taken from the current ``estate.property``
- the ``move_type`` should correspond to a 'Customer Invoice'
- the ``journal_id`` must be a ``sale`` journal (when in doubt, have a look
`here <https://github.com/odoo/odoo/blob/f1f48cdaab3dd7847e8546ad9887f24a9e2ed4c1/addons/sale/models/sale.py#L534>`__)
Tips:
- to create an object, use ``self.env[model_name].create(values)``, where ``values``
is a ``dict``.
- the ``create`` method doesn't accept recordsets as field values.
When a property is set to 'Sold', you should now have a new customer invoice created in
Invoicing / Customers / Invoices.
Obviously we don't have any invoice lines so far. To create an invoice line, we need the following
information:
- ``name``: a description of the line
- ``quantity``
- ``price_unit``
Moreover, an invoice line needs to be linked to an invoice. The easiest and most efficient way
to link a line to an invoice is to include all lines at invoice creation. To do this, the
``invoice_line_ids`` field is included in the ``account.move`` creation, which is a
:class:`~odoo.fields.One2many`. One2many and Many2many use special 'commands' described in
:ref:`reference/orm/models/crud`. This format is a list of triplets executed sequentially, where
each triplet is a command to execute on the set of records. Here is a simple example to include
a One2many field ``line_ids`` at creation of a ``test.model``::
def inherited_action(self):
self.env["test.model"].create(
{
"name": "Test",
"line_ids": [
(
0,
0,
{
"field_1": "value_1",
"field_2": "value_2",
},
)
],
}
)
return super().inherited_action()
.. exercise:: Add the third step of invoice creation.
Add two invoice lines during the creation of the ``account.move``. Each property sold will
be invoiced following these conditions:
- 6% of the selling price
- an additional 100.00 from administrative fees
Tip: Add the ``invoice_line_ids`` at creation following the example above.
For each line, we need a ``name``, ``quantity`` and ``price_unit``.
This chapter might be one of the most difficult that has been covered so far, but it is the closest
to what Odoo development will be in practice. In the :ref:`next chapter <howto/rdtraining/15_qwebintro>`,
we will introduce the templating mechanism used in Odoo.

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

View File

@ -0,0 +1,133 @@
.. _howto/rdtraining/15_qwebintro:
===================================
Chapter 15: A Brief History Of QWeb
===================================
So far the interface design of our real estate module has been rather limited. Building
a list view is straightforward since only the list of fields is necessary. The same holds true
for the form view: despite the use of a few tags such as ``<group>`` or ``<page>``, there
is very little to do in terms of design.
However, if we want to give a unique look to our application, it is necessary to go a step
further and be able to design new views. Moreover, other features such as PDF reports or
website pages need another tool to be created with more flexibility: a templating_ engine.
You might already be familiar with existing engines such as Jinja (Python), ERB (Ruby) or
Twig (PHP). Odoo comes with its own built-in engine: :ref:`reference/qweb`.
QWeb is the primary templating engine used by Odoo. It is an XML templating engine and used
mostly to generate HTML fragments and pages.
You probably already have come across the `kanban board`_ in Odoo where the records are
displayed in a card-like structure. We will build such a view for our real estate module.
Concrete Example: A Kanban View
===============================
**Reference**: the documentation related to this topic can be found in
:ref:`reference/views/kanban`.
.. note::
**Goal**: at the end of this section a Kanban view of the properties should be created:
.. image:: 15_qwebintro/media/kanban.png
:align: center
:alt: Kanban view
In our estate application, we would like to add a Kanban view to display our properties. Kanban
views are a standard Odoo view (like the form and list views), but their structure is much more
flexible. In fact, the structure of each card is a mix of form elements (including basic HTML)
and QWeb. The definition of a Kanban view is similar to the definition of the list and form
views, except that their root element is ``<kanban>``. In its simplest form, a Kanban view
looks like:
.. code-block:: xml
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="name"/>
</div>
</t>
</templates>
</kanban>
Let's break down this example:
- ``<templates>``: defines a list of :ref:`reference/qweb` templates. Kanban views *must* define at
least one root template ``kanban-box``, which will be rendered once for each record.
- ``<t t-name="kanban-box">``: ``<t>`` is a placeholder element for QWeb directives. In this case,
it is used to set the ``name`` of the template to ``kanban-box``
- ``<div class="oe_kanban_global_click">``: the ``oe_kanban_global_click`` makes the ``<div>``
clickable to open the record.
- ``<field name="name"/>``: this will add the ``name`` field to the view.
.. exercise:: Make a minimal kanban view.
Using the simple example provided, create a minimal Kanban view for the properties. The
only field to display is the ``name``.
Tip: you must add ``kanban`` in the ``view_mode`` of the corresponding
``ir.actions.act_window``.
Once the Kanban view is working, we can start improving it. If we want to display an element
conditionally, we can use the ``t-if`` directive (see :ref:`reference/qweb/conditionals`).
.. code-block:: xml
<kanban>
<field name="state"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="name"/>
</div>
<div t-if="record.state.raw_value == 'new'">
This is new!
</div>
</t>
</templates>
</kanban>
We added a few things:
- ``t-if``: the ``<div>`` element is rendered if the condition is true.
- ``record``: an object with all the requested fields as its attributes. Each field has
two attributes ``value`` and ``raw_value``. The former is formatted according to current
user parameters and the latter is the direct value from a :meth:`~odoo.models.Model.read`.
In the above example, the field ``name`` was added in the ``<templates>`` element, but ``state``
is outside of it. When we need the value of a field but don't want to display it in the view,
it is possible to add it outside of the ``<templates>`` element.
.. exercise:: Improve the Kanban view.
Add the following fields to the Kanban view: expected price, best price, selling price and
tags. Pay attention: the best price is only displayed when an offer is received, while the
selling price is only displayed when an offer is accepted.
Refer to the **Goal** of the section for a visual example.
Let's give the final touch to our view: the properties must be grouped by type by default. You
might want to have a look at the various options described in :ref:`reference/views/kanban`.
.. exercise:: Add default grouping.
Use the appropriate attribute to group the properties by type by default. You must also prevent
drag and drop.
Refer to the **Goal** of the section for a visual example.
Kanban views are a typical example of how it is always a good idea to start from an existing
view and fine tune it instead of starting from scratch. There are many options and classes
available, so... read and learn!
It is now time to add the
:ref:`final touches to our application and submit it on GitHub <howto/rdtraining/16_guidelines_pr>`!
.. _templating:
https://en.wikipedia.org/wiki/Template_processor
.. _kanban board:
https://en.wikipedia.org/wiki/Kanban_board

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,168 @@
.. _howto/rdtraining/16_guidelines_pr:
===============================================
Chapter 16: Polish Your Code And Submit Your PR
===============================================
In the previous chapters we saw how to create a fully functional business application. Now we will
prepare our code to be shared and learn how to share it.
Coding guidelines
=================
We will start refactoring the code to match to the Odoo coding guidelines. The guidelines aim
to improve the quality of the Odoo Apps code.
**Reference**: you will find the Odoo coding guidelines in :ref:`reference/guidelines`.
.. exercise:: Polish your code.
Refactor your code to respect the coding guidelines. Don't forget to respect the module
structure, the variable names, the method name convention, the model attribute order and the
xml ids.
Your first Pull Request (PR)
============================
**The following is intended to be used by Odoo staff members only. All repositories mentioned are
not accessible for third-parties.**
Now that your code respects the Odoo code guidelines and is polished up, let's share it with others.
Configure development repository
--------------------------------
We will start by creating a git development environment for the 'custom' folder, like we did in the
beginning for 'odoo' and 'enterprise'. The PR will target the `odoo/technical-training-sandbox` repository.
To do this the first step is to
`fork <https://guides.github.com/activities/forking/>`__ in GitHub
`this repository <https://github.com/odoo/technical-training-sandbox/>`__
to create your own development repository.
After you successfully created your development repository, we will configure your existing `$HOME/src/custom`
folder to be able to commit your work.
.. code-block:: console
$ cd $HOME/src/custom
$ git init
$ git remote add training git@github.com:odoo/technical-training-sandbox.git
$ git remote add training-dev git@github.com:xyz-odoo/technical-training-sandbox.git #Don't forget to change xyz-odoo to your own GitHub account
$ git remote set-url --push training no_push #Avoid pushing directly into the main repository
$ git fetch --all --prune
$ git branch master training/master
$ git checkout master #Two new files will be downloaded README.md and .gitignore
Note that if your work is already in a :ref:`Git and GitHub configured folder <howto/rdtraining/02_setup/development_repository>`
the previous steps are not needed. This is the case when you start working in the
'odoo' or 'enterprise' folders.
Branch, Commit & Push
---------------------
Before creating the PR, a new working branch has to be created and used to commit the code. Afterwards
the branch will be pushed to the development repository.
Create your working branch:
.. code-block:: console
$ git checkout -b master-my_first_branch-xyz
Your branch name must follow the following name strucutre : <targetVersion>-<feature>-<trigram>
Example: The branch master-sale-fixes-abc on odoo-dev/odoo is a branch containing fixes for the
sales app in the odoo/odoo repository, to be deployed in master and done by ABC.
Commit your code:
.. code-block:: console
$ git add .
$ git commit
**Everyone reads your commit messages!**
The commit message is very important, follow the :ref:`Developer guidelines <reference/guidelines/git>`.
Push your new branch to your development repository:
.. code-block:: console
$ git push -u master-my_first_branch-xyz
.. warning:: NEVER use `git push --force` (or `git push -f`) with a branch that does not belong to you
or in a stable branch. Never, never, never!
.. tip:: Remember that you can use `git status` at any point to check the current status of your branch
.. tip:: You can create git aliases to shorten some of these commands. Ask your colleagues
if they use git aliases and get inspired by their work.
Create your first PR
--------------------
After pushing your branch to your development repository you will see an output similar to:
.. code-block:: console
Enumerating objects: 24, done.
Counting objects: 100% (24/24), done.
Compressing objects: 100% (20/20), done.
Writing objects: 100% (23/23), 6.67 KiB | 525.00 KiB/s, done.
Total 23 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
remote:
remote: Create a pull request for 'master-my_first_branch-xyz' on GitHub by visiting:
remote: https://github.com/xyz-odoo/technical-training-sandbox/pull/new/master-my_first_branch-xyz
remote:
To github.com:xyz-odoo/technical-training-sandbox.git
* [new branch] master-my_first_branch-xyz -> master-my_first_branch-xyz
Branch 'master-my_first_branch-xyz' set up to track remote branch 'master-my_first_branch-xyz' from 'training-dev'.
There are two ways to create the PR:
- Click on the link displayed in the output of the push command.
- Open a browser in your development repository `https://github.com/xyz-odoo/technical-training-sandbox`.
There will be a button to create a new pull request.
.. image:: 16_guidelines_pr/media/pr_from_branch.png
You will notice that your commit message is used as the pull request message. This occurs if you have only 1 commit.
If you have multiple commits, you can either make a summary of the commits as the PR message or if there aren't that many
commits you can just copy/paste your commit messages as the PR message.
.. image:: 16_guidelines_pr/media/pr_message.png
Additional Info for Odoo Staff
==============================
Now that you know the basics, here are some more useful pages if you haven't seen them already:
- Our `welcome <https://github.com/odoo/enterprise/wiki/Welcome>`__ page has extra info about
working at Odoo as well as additional
`tutorials <https://github.com/odoo/enterprise/wiki/Welcome#3-technical-training>`__ that are
both language and Odoo specific.
- Once you are ready to start doing tasks, it is important to note that Odoo follows a
`rebasing workflow instead of merging <https://www.atlassian.com/git/tutorials/merging-vs-rebasing>`__
for your PRs.
`This cheatsheet <https://github.com/odoo/enterprise/wiki/GIT-Cheatsheet#pull-request-flow>`__ describes
how to do this flow. If you want to become more of a rebasing master, then
`this tutorial <https://www.atlassian.com/git/tutorials/rewriting-history>`__ is extra useful.
Test on the runbot
==================
Odoo has its own :abbr:`CI (Continuous integration)` server named `runbot <https://runbot.odoo.com/>`__. All
commits, branches and PR will be tested to avoid regressions or breaking of the stable versions.
All the runs that pass the tests are deployed on their own server with demo data.
.. exercise:: Play with the runbot.
Feel free to go to the runbot website and open the last stable version of Odoo to check out all the available
applications and functionalities.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/A_i18n:
================================
Advanced A: Internationalization
================================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,528 @@
.. _howto/rdtraining/B_acl_irrules:
================================
Advanced B: ACL and Record Rules
================================
.. warning::
This tutorial assumes you have completed the :ref:`Core Training
<howto/rdtraining>`.
To follow the exercise, it is recommended that you fetch the branch
14.0-core from the repository XXX, 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 <howto/rdtraining/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 exclusivities.
* 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 limiting 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>`.
:ref:`The guidelines <reference/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
<howto/rdtraining/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>`_.
.. admonition:: what is the *category_id*?
``category_id`` a *module category*, it is automatically generated from the
:ref:`category defined in module manifest files <howto/rdtraining/03_newapp>`.
.. todo:: the demo module needs to have the category set, to
*Real Estate/Brokerage*, ideally the newapp part would already do
that
.. exercise::
Create the ``security.xml`` file in the appropriate folder and add it to the
``__manifest__.py`` file.
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``.
.. 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:`howto/rdtraining/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 or update, but not search or read.
.. note::
* The group of an access right can be ommitted, 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 acces 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").
Access 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.
Access *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="[(4, 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 recorset in "sudo mode", this ignores
all access rules and access rights (although hard-coded group and user checks
may still apply).
* Performing raw SQL queries will bypass access rules and access rights 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 access 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 access 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, '=')
- Execute ``bypass.py`` in ``estate_account``, giving it the name of your
database, and the name of your version of ``action_sold`` (unless you named it
``action_sold`` then it's fine)
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 recorset, this
verifies whether the current user has access to the model itself.
* Calling ``check_access_rule(operations)`` on a non-empty recorset, this
verifies that the current user is allowed to perform the operation on *every*
record of the set.
.. warning:: Checking access rights and checking access rules are separate
operations, if you're checking access 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, and this specific property in particular.
Re-run the bypass script, check that the error occurs before the print.
.. _howto/rdtraining/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>`
this 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 an other we might need to manage our real-estate business
as multiple companies e.g. we might have largely autonomous agencies, or 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 like company-dependent fields and *multi-company rules*,
which is what we're going to concern outselves 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 access 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 muti-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 rea-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 an other part of the interface or by :doc:`perform
operations remotely using RPC <../../webservices/odoo>`, 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, and can see their
options from the Property form view when creating it.
The Settings menu just adds noise to their interface, it should only be
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -0,0 +1,425 @@
.. _howto/rdtraining/C_data:
================================
Advanced C: Master and Demo Data
================================
.. tip:: This tutorial assumes you followed the Core Training.
To do the exercise, fetch the branch 14.0-core from the repository XXX.
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<howto/rdtraining/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: countries, currencies, units of measure but
also complete country localization (legal reports, tax definitions, chart of account), and much
more...
Demo Data
---------
Next to master data which are requirements for the modules to work properly, we also like having
data for demonstration purpose setups:
* Help the sales representatives to make their demos quickly.
* Have a set of working data for developers to test the new features and see what it looks like
with something they might not have created without it.
* Test that the data is loaded correctly, without raising an error.
* Be ready to use most of the features quickly when creating a new database.
Demo data is automatically loaded when you start the server if you didn't say explicitly 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>`.
The data is declared either in CSV either 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 path to the files
declaring the data.
Usually, the demo data is set in a ``demo`` folder, the views and actions are put in a ``views``
folder, the security related data is put in a ``security`` folder, and the other data is set 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 in the other cases.
.. 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 a bit more complex it can be useful, or even needed, 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:`howto/rdtraining/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:: Both properties should be Residential properties.
``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 <howto/rdtraining/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 actually depends on the localization
installed.
``function``
~~~~~~~~~~~~
You might also need to execute python code when loading the 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.
.. _howto/rdtraining/C_data/x2m:
Add X2many fields
-----------------
**Reference**: the documentation related to this topic can be found in
:ref:`Common ORM methods<reference/orm/models/crud>`.
If you need to add related data in a One2many or a Many2many field, you can do so by using the
common ORM methods.
.. code-block:: xml
<odoo>
<record id="id1" model="tutorial.example">
<field name="related_ids" eval="[
(0, 0, {
'name': 'My name',
}),
(0, 0, {
'name': 'Your name',
}),
(4, ref('model.xml_id')),
]"/>
</record>
</odoo>
.. code-block:: text
id,parent_id:id,name
"child1","module.parent","Name1"
"child2","module.parent","Name2"
"child3","module.parent","Name3"
.. exercise:: Create one new Property, but this time with some offers created directly inside the
One2many field linking 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<howto/rdtraining/C_data/xml_id>`.
.. warning:: Data can always be deleted by the user. Always code defensively, taking this into
account.
Advanced
========
.. _howto/rdtraining/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>`__.

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/D_mixins:
==================
Advanced D: Mixins
==================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,293 @@
.. _howto/rdtraining/E_unittest:
=============================
Advanced E: Python Unit Tests
=============================
.. tip:: This tutorial assumes you followed the Core Training.
To do the exercise, fetch the branch 14.0-core from the repository XXX.
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 it 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 you develop 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 spec 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. By default tag value is 'standard' when not
given on include mode. '*' will match all tags. Tag
will also match module name (deprecated, use /module)
The module, class, and method will respectively match
the module name, test class name and test method name.
examples: :TestClass.test_func,/test_module,external
--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 if it is not the case.
When a test is written, it is important to make sure it always passes when modifications are
applied to the source code. To automatize 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 contains the
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 whatever the modifications done. Correcting a typo in an error message or
refactoring a whole module triggers the same tests. It will install all the modules. This means
something might not work if Runbot green but your changes depend on something you don'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 be excluded also.
* There is a nightly build running additional tests, like 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 on 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 on the main repositories.
When the last batch is green, the reviewer can ask robodoo to merge your PR (actually 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 modular also. This means the modules are defined in
the module that adds the functionality you add; and that 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 SavepointCase
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(SavepointCase):
def test_01(self):
...
@tagged('at_install') # this is the default
class AtInstallTestCase(SavepointCase):
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 from 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. This is why 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 located in a ``tests`` folder in 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
.. note:: Some older tests are extending ``odoo.tests.common.TransactionCase``, but they are less
scalable. The difference is that the setup is done per test method and not per test class.
The data changed are rollbacked between each test in `SavepointCase` to have the same behavior as
in `TransactionCase`.
All the tests should extend ``odoo.tests.common.SavepointCase``. You usually define a
``setUpClass``, and the tests. After doing the `setUpClass`, you have an `env` available on 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 SavepointCase
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(SavepointCase):
@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 more 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; that common
class can define the whole set up for the module. For instance in
`account <https://github.com/odoo/odoo/blob/14.0/addons/account/tests/common.py>`__.
.. exercise:: Ensure no one can create an offer for a sold Property, and create a test for 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.

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/F_jstour:
====================
Advanced F: JS Tours
====================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/G_website:
=================================
Advanced G: Controllers & Website
=================================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/H_adv_views:
==========================
Advanced H: Advanced Views
==========================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/I_jswidget:
============================
Advanced I: Custom JS Widget
============================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,313 @@
.. _howto/rdtraining/J_reports:
=======================
Advanced J: PDF Reports
=======================
.. warning::
This tutorial assumes you have completed the :ref:`Core Training <howto/rdtraining>`
and have installed :ref:`wkhtmltopdf <howto/rdtraining/02_setup/install-wkhtmltopdf>`.
To follow the exercise, it is recommended that you fetch the branch
14.0-core from the repository XXX, 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 <howto/rdtraining/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 in the top level of your module's
directory. If a module has many or multiple long report templates, then they are often organized in
a logical manner across different files named after the report(s) they contain. All actions
for the reports are usually stored in the same file ending with ``_report_views.xml`` regardless of the
number of reports it contains.
Therefore it is expected that your work tree will look something like:
.. code-block:: bash
estate
├── models
│ ├── *.py
│ └── __init__.py
├── report
│ ├── estate_report_templates.xml
│ └── estate_report_views.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 <howto/rdtraining/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 located in 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:`howto/rdtraining/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/14.0-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/14.0-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 FontAwesome (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 of 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 in and Odoo
will auto-magically add it for you. Another common use case of the report action is to link it to
a button like we learned in :ref:`howto/rdtraining/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 of 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 of 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 about 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,354 @@
.. _howto/rdtraining/K_dashboard:
======================
Advanced K: Dashboards
======================
.. warning::
This tutorial assumes you have completed the :ref:`Core Training <howto/rdtraining>` and have
access to Odoo Enterprise features.
To follow the exercise, it is recommended that you fetch the branch
14.0-core from the repository XXX, 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:`howto/rdtraining/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 <howto/rdtraining/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:`howto/rdtraining/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/14.0-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/14.0-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:`howto/rdtraining/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:`howto/rdtraining/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:`howto/rdtraining/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,
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:`howto/rdtraining/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:`howto/rdtraining/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/14.0-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 sort of useful helper methods you will probably use in
the future). Note that it is standard to include ``report`` in the model name.
Remember your new model will need to be added to your security file like you learned in
:ref:`howto/rdtraining/05_securityintro`!
Then we define the fields we need for our dashboard the same way as any other model (like you
learned in :ref:`howto/rdtraining/04_basicmodel`), except that every field is
``readonly=True``. After all, our model is for readonly 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 of 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 to how its made
except that its file is located 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:`howto/rdtraining/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 to not take into account 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.
In order 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/L_cron:
======================================
Advanced L: Scheduled & Server Actions
======================================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/M_migration:
======================
Advanced M: Migrations
======================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/N_security:
====================
Advanced N: Security
====================
Hopefully, this topic will be written soon :-)

View File

@ -0,0 +1,9 @@
:orphan:
.. _howto/rdtraining/O_perf:
========================
Advanced O: Performances
========================
Hopefully, this topic will be written soon :-)

View File

@ -260,6 +260,8 @@ section of the view, and allowing a few *optional* attributes:
.. deprecated act_window & report
.. ignored url, act_window and ir_set
.. _reference/data/csvdatafiles:
CSV data files
==============

View File

@ -276,6 +276,8 @@ to create. The value to set can be provided in two ways:
.. note:: using the result of this operation is a significant use-case for
the ``raw`` directive.
.. _reference/qweb/sub-templates:
calling sub-templates
=====================

View File

@ -12,63 +12,158 @@ Both mechanisms are linked to specific users through *groups*: a user belongs
to any number of groups, and security mechanisms are associated to groups,
thus applying security mechamisms to users.
.. class:: res.groups
.. attribute:: name
serves as user-readable identification for the group (spells out the
role / purpose of the group)
.. attribute:: category_id
The *module category*, serves to associate groups with an Odoo App
(~a set of related business models) and convert them into an exclusive
selection in the user form.
.. todo:: clarify & document special cases & relationship between
groups & categories better
.. attribute:: implied_ids
Other groups to set on the user alongside this one. This is a
convenience pseudo-inheritance relationship: it's possible to
explicitely remove implied groups from a user without removing the
implier.
.. attribute:: comment
Additional notes on the group e.g.
.. _reference/security/acl:
Access Control
==============
Access Rights
=============
Managed by the ``ir.model.access`` records, defines access to a whole model.
*Grants* access to an entire model for a given set of operations. If no access
rights matches an operation on a model for a user (through their group), the
user doesn't have access.
Each access control has a model to which it grants permissions, the
permissions it grants and optionally a group.
Access rights are additive, a user's accesses are the union of the accesses
they get through all their groups e.g. given a user who is part of group A
granting read and create access and a group B granting update access, the user
will have all three of create, read, and update.
Access controls are additive, for a given model a user has access all
permissions granted to any of its groups: if the user belongs to one group
which allows writing and another which allows deleting, they can both write
and delete.
.. class:: ir.model.access
If no group is specified, the access control applies to all users, otherwise
it only applies to the members of the given group.
.. attribute:: name
Available permissions are creation (``perm_create``), searching and reading
(``perm_read``), updating existing records (``perm_write``) and deleting
existing records (``perm_unlink``)
The purpose or role of the group.
.. attribute:: model_id
The model whose access the ACL controls.
.. attribute:: group_id
The :class:`res.groups` to which the accesses are granted, an empty
:attr:`group_id` means the ACL is granted to *every user*
(non-employees e.g. portal or public users).
The :samp:`perm_{method}` attributes grant the corresponding CRUD access
when set, they are all unset by default.
.. attribute:: perm_create
.. attribute:: perm_read
.. attribute:: perm_write
.. attribute:: perm_unlink
.. _reference/security/rules:
Record Rules
Access Rules
============
Record rules are conditions that records must satisfy for an operation
(create, read, update or delete) to be allowed. It is applied record-by-record
after access control has been applied.
Record rules are *conditions* which must be satisfied in order for an operation
to be allowed. Record rules are evaluated record-by-record, following access
rights.
A record rule has:
Access rules are default-allow: if access rights grant access and no rule
applies to the operation and model for the user, the access is granted.
* a model on which it applies
* a set of permissions to which it applies (e.g. if ``perm_read`` is set, the
rule will only be checked when reading a record)
* a set of user groups to which the rule applies, if no group is specified
the rule is *global*
* a :ref:`domain <reference/orm/domains>` used to check whether a given record
matches the rule (and is accessible) or does not (and is not accessible).
The domain is evaluated with two variables in context: ``user`` is the
current user's record and ``time`` is the `time module`_
.. class:: ir.rule
Global rules and group rules (rules restricted to specific groups versus
groups applying to all users) are used quite differently:
.. attribute:: name
* Global rules are subtractive, they *must all* be matched for a record to be
accessible
* Group rules are additive, if *any* of them matches (and all global rules
match) then the record is accessible
The description of the rule.
This means the first *group rule* restricts access, but any further
*group rule* expands it, while *global rules* can only ever restrict access
(or have no effect).
.. attribute:: model_id
.. warning:: record rules do not apply to the Superuser account
:class: aphorism
The model to which the rule applies.
.. attribute:: groups
The :class:`res.groups` to which access is granted (or not). Multiple
groups can be specified. If no group is specified, the rule is *global*
which is treated differently than "group" rules (see below).
.. attribute:: global
Computed on the basis of :attr:`groups`, provides easy access to the
global status (or not) of the rule.
.. attribute:: domain_force
A predicate specified as a :ref:`domain <reference/orm/domains>`, the
rule allows the selected operations if the domain matches the record,
and forbids it otherwise.
The domain is a *python expression* which can use the following
variables:
``time``
Python's :mod:`python:time` module.
``user``
The current user, as a singleton recordset.
``company_id``
The current user's currently selected company as a single company id
(not a recordset).
``company_ids``
All the companies to which the current user has access as a list of
company ids (not a recordset), see
:ref:`howto/company/security` for more details.
The :samp:`perm_{method}` have completely different semantics than for
:class:`ir.model.access`: for rules, they specify which operation the rules
applies *for*. If an operation is not selected, then the rule is not checked
for it, as if the rule did not exist.
All operations are selected by default.
.. attribute:: perm_create
.. attribute:: perm_read
.. attribute:: perm_write
.. attribute:: perm_unlink
.. _reference/security/rules/global:
Global rules versus group rules
-------------------------------
There is a large difference between global and group rules in how they compose
and combine:
* Global rules *intersect*, if two global rules apply then *both* must be
satisfied for the access to be granted, this means adding global rules always
restricts access further.
* Group rules *unify*, if two group rules apply then *either* can be
satisfied for the access to be granted. This means adding group rules can
expand access, but not beyond the bounds defined by global rules.
* The global and group rulesets *intersect*, which means the first group rule
being added to a given global ruleset will restrict access.
.. danger::
Creating multiple global rules is risky as it's possible to create
non-overlapping rulesets, which will remove all access.
.. _reference/security/fields:

View File

@ -194,6 +194,8 @@ they're not going to get run:
class SmallTest(unittest.TestCase):
...
.. _reference/testing/tags:
Special tags
^^^^^^^^^^^^