[IMP] developer: improve navigation in top-level pages
Prior to this commit, users had to either know in advance or guess the
location of the content they were looking for. Top-level pages of the
"Developer" section of the documentation, in particular the "Developer"
page itself, were listing their sub-pages without directions for users.
This commit brings the following changes to improve the navigation:
- add directions for users on the "Developer" page and list the three
main categories of developer documentation ("Tutorials", "How-to
guides", and "Reference") with explanations of their content and
target audience;
- add categories for content cards on the "Tutorials" and "How-to
guides" pages, and fine-tune the toctree of the "Reference" page to
more easily locate specific topics;
- clarify what are the "Python framework" and the "JavaScript framework"
by relabelling them to "Server framework" and "Web framework" on
top-level pages, as some users were confused to find that the JS
framework was not responsible for the server, and others that the
documentation for QWeb template is located in the Python documentation;
- extract the "Setup guide" from the "Getting started" tutorial and
rename the latter to "Server framework 101" to allow reusing the setup
guide in other tutorials and make clear that the "Server framework 101"
tutorial is not about the Web framework.
task-3802536
closes odoo/documentation#8597
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Valeriya (vchu) <vchu@odoo.com>
@@ -0,0 +1,133 @@
|
||||
.. _tutorials/server_framework_101/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/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`_.
|
||||
|
||||
.. note::
|
||||
Since version 15.0, Odoo is actively transitioning to using its own in-house developed `OWL
|
||||
framework <https://odoo.github.io/owl/>`_ as part of its presentation tier. The legacy JavaScript
|
||||
framework is still supported but will be deprecated over time. This will be discussed further in
|
||||
advanced topics.
|
||||
|
||||
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.
|
||||
|
||||
:doc:`Object views <../../reference/user_interface/view_architectures>`
|
||||
Define UI display
|
||||
|
||||
:ref:`Data files <reference/data>`
|
||||
XML or CSV files declaring the model data:
|
||||
|
||||
* :doc:`views <../../reference/user_interface/view_architectures>` 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.
|
||||
|
||||
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? It is now time to :doc:`write your own application <02_newapp>`!
|
||||
|
||||
.. _multitier architecture:
|
||||
https://en.wikipedia.org/wiki/Multitier_architecture
|
||||
|
||||
.. _Python tutorial:
|
||||
https://docs.python.org/3.7/tutorial/
|
||||
|
||||
.. _two versions:
|
||||
https://www.odoo.com/page/editions
|
||||
@@ -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">>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);">>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 |
@@ -0,0 +1,99 @@
|
||||
============================
|
||||
Chapter 2: 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:: 02_newapp/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:: 02_newapp/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:: 02_newapp/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:: 02_newapp/app_in_list.png
|
||||
:align: center
|
||||
:alt: The new module appears in the list
|
||||
|
||||
The first step of module creation is to create its directory. In the `tutorials`
|
||||
directory, add a new directory :file:`estate`.
|
||||
|
||||
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/tutorials/estate/__init__.py``
|
||||
- ``/home/$USER/src/tutorials/estate/__manifest__.py``
|
||||
|
||||
The ``__manifest__.py`` file should only define the name and the dependencies of our modules.
|
||||
The only necessary framework module for now is ``base``.
|
||||
|
||||
|
||||
Restart the Odoo server and 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 ;-)
|
||||
|
||||
.. warning::
|
||||
Remember to enable the :ref:`developer mode <developer-mode>` as explained in the previous
|
||||
chapter. You won't see the :guilabel:`Update Apps List` button otherwise.
|
||||
|
||||
.. 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 :doc:`create our first model <03_basicmodel>`!
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,303 @@
|
||||
==================================
|
||||
Chapter 3: Models And Basic Fields
|
||||
==================================
|
||||
|
||||
At the end of the :doc:`previous chapter <02_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.
|
||||
|
||||
.. warning::
|
||||
|
||||
Do not use mutable global variables.
|
||||
|
||||
A single Odoo instance can run several databases in parallel within the same python process.
|
||||
Distinct modules might be installed on each of these databases, therefore we cannot rely on
|
||||
global variables that would be updated depending on installed modules.
|
||||
|
||||
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`. 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=addons,../enterprise/,../tutorials/ -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=addons,../enterprise/,../tutorials/ -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 = fields.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
|
||||
:doc:`add some security <04_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
|
||||
@@ -0,0 +1,124 @@
|
||||
==========================================
|
||||
Chapter 4: Security - A Brief Introduction
|
||||
==========================================
|
||||
|
||||
In the :doc:`previous chapter <03_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 :doc:`../restrict_data_access`. 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
|
||||
<{GITHUB_PATH}/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_au_1,au,"Australian Capital Territory","ACT"
|
||||
state_au_2,au,"New South Wales","NSW"
|
||||
state_au_3,au,"Northern Territory","NT"
|
||||
state_au_4,au,"Queensland","QLD"
|
||||
...
|
||||
|
||||
- ``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 models ['estate.property'] have 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 models ['estate.property'] have no access rules in module estate, consider adding some, like:
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
||||
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.
|
||||
- ``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 :doc:`interact with the UI <05_firstui>`!
|
||||
|
||||
.. [#who] meaning which Odoo user (or group of users)
|
||||
|
||||
.. [#unlink] 'unlink' is the equivalent of 'delete'
|
||||
291
content/developer/tutorials/server_framework_101/05_firstui.rst
Normal file
@@ -0,0 +1,291 @@
|
||||
========================================
|
||||
Chapter 5: Finally, Some UI To Play With
|
||||
========================================
|
||||
|
||||
Now that we've created our new :doc:`model <03_basicmodel>` and its
|
||||
corresponding :doc:`access rights <04_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 :doc:`04_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
|
||||
:doc:`../../reference/backend/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/views/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
|
||||
:doc:`later chapter <09_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 :doc:`later <14_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:: 05_firstui/estate_menu_root.png
|
||||
:align: center
|
||||
:alt: Root menus
|
||||
|
||||
.. image:: 05_firstui/estate_menu_action.png
|
||||
:align: center
|
||||
:alt: First level and action menus
|
||||
|
||||
.. image:: 05_firstui/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:: 05_firstui/menu_01.png
|
||||
:align: center
|
||||
:alt: Root menus
|
||||
|
||||
.. image:: 05_firstui/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:: 05_firstui/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=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:: 05_firstui/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 :doc:`our own views <06_basicviews>`.
|
||||
|
||||
.. [#refresh] A refresh is needed since the web client keeps a cache of the various menus
|
||||
and views for performance reasons.
|
||||
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,240 @@
|
||||
======================
|
||||
Chapter 6: Basic Views
|
||||
======================
|
||||
|
||||
We have seen in the :doc:`previous chapter <05_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/view_architectures/list`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the list view should look like this:
|
||||
|
||||
.. image:: 06_basicviews/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/view_architectures/form`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the form view should look like this:
|
||||
|
||||
.. image:: 06_basicviews/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=addons,../enterprise/,../tutorials/ -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/view_architectures/search`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the search view should look like this:
|
||||
|
||||
.. image:: 06_basicviews/search_01.png
|
||||
:align: center
|
||||
:alt: Search fields
|
||||
|
||||
.. image:: 06_basicviews/search_02.png
|
||||
:align: center
|
||||
:alt: Filter
|
||||
|
||||
.. image:: 06_basicviews/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/view_architectures/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)]
|
||||
|
||||
.. note:: XML does not allow ``<`` and ``&`` to be used inside XML
|
||||
elements. To avoid parsing errors, entity references should be used:
|
||||
``<`` for ``<`` and ``&`` for ``&``. Other entity references
|
||||
(``>``, ``'`` & ``"``) are optional.
|
||||
|
||||
.. example::
|
||||
.. code-block:: xml
|
||||
|
||||
<filter name="negative" domain="[('test_val', '<', 0)]"/>
|
||||
|
||||
.. 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
|
||||
:doc:`link between models <07_relations>`.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
@@ -0,0 +1,264 @@
|
||||
===================================
|
||||
Chapter 7: Relations Between Models
|
||||
===================================
|
||||
|
||||
The :doc:`previous chapter <06_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 estate 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:: 07_relations/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:: 07_relations/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/12/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
|
||||
:doc:`model <03_basicmodel>`, set the
|
||||
:doc:`model <04_securityintro>`, add an
|
||||
:doc:`action and a menu <05_firstui>`, and
|
||||
:doc:`create a view <06_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:: 07_relations/property_tag.png
|
||||
:align: center
|
||||
:alt: Property tag
|
||||
|
||||
- tags should be added to the ``estate.property`` model:
|
||||
|
||||
.. image:: 07_relations/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 :doc:`a later chapter of the training <11_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:: 07_relations/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
|
||||
:doc:`next chapter <08_compute_onchange>` will be lighter, don't worry ;-)
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,303 @@
|
||||
========================================
|
||||
Chapter 8: Computed Fields And Onchanges
|
||||
========================================
|
||||
|
||||
The :doc:`relations between models <07_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:: 08_compute_onchange/compute.gif
|
||||
:align: center
|
||||
:alt: Compute fields
|
||||
|
||||
- In the property offer model, the validity date should be computed and can be updated:
|
||||
|
||||
.. image:: 08_compute_onchange/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:`tutorials/server_framework_101/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:: 08_compute_onchange/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 :doc:`next chapter <09_actions>`, we'll see how we can trigger some
|
||||
business logic when buttons are clicked.
|
||||
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 46 KiB |
137
content/developer/tutorials/server_framework_101/09_actions.rst
Normal file
@@ -0,0 +1,137 @@
|
||||
==================================
|
||||
Chapter 9: Ready For Some Action?
|
||||
==================================
|
||||
|
||||
So far we have mostly built our module by declaring fields and views. We just introduced business
|
||||
logic in the :doc:`previous chapter <08_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.
|
||||
|
||||
Object Type
|
||||
===========
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:doc:`../../reference/backend/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:: 09_actions/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:: 09_actions/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:: 09_actions/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!
|
||||
|
||||
Action Type
|
||||
===========
|
||||
|
||||
In :doc:`05_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 :doc:`next chapter <10_constraints>` we'll see how we can prevent
|
||||
encoding incorrect data in Odoo.
|
||||
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 97 KiB |
@@ -0,0 +1,128 @@
|
||||
=======================
|
||||
Chapter 10: Constraints
|
||||
=======================
|
||||
|
||||
The :doc:`previous chapter <09_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:: 10_constraints/sql_01.gif
|
||||
:align: center
|
||||
:alt: Constraints on amounts
|
||||
|
||||
- Property types and tags should have a unique name
|
||||
|
||||
.. image:: 10_constraints/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 be possible to accept an offer
|
||||
lower than 90% of the expected price.
|
||||
|
||||
.. image:: 10_constraints/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/274dd3bf503e1b612179db92e410b336bfaecfb4/addons/stock/models/stock_quant.py#L239-L244>`__.
|
||||
|
||||
.. 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 from `odoo.tools.float_utils` 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 :doc:`next chapter <11_sprinkles>`.
|
||||
|
||||
.. _PostgreSQL's documentation:
|
||||
.. _table_constraint:
|
||||
https://www.postgresql.org/docs/12/ddl-constraints.html
|
||||
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 128 KiB |
@@ -0,0 +1,521 @@
|
||||
=============================
|
||||
Chapter 11: Add The Sprinkles
|
||||
=============================
|
||||
|
||||
Our real estate module now makes sense from a business perspective. We created
|
||||
:doc:`specific views <06_basicviews>`, added several
|
||||
:doc:`action buttons <09_actions>` and
|
||||
:doc:`constraints <10_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
|
||||
:doc:`../../reference/user_interface/view_records` and
|
||||
:doc:`../../reference/user_interface/view_architectures`.
|
||||
|
||||
Inline Views
|
||||
============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, a specific list of properties should be added to the property
|
||||
type view:
|
||||
|
||||
.. image:: 11_sprinkles/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:: 11_sprinkles/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:: 11_sprinkles/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 :doc:`05_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 can be used in combination with an ``invisible`` attribute in the view to display
|
||||
buttons conditionally.
|
||||
|
||||
.. exercise:: Add conditional display of buttons.
|
||||
|
||||
Use the ``invisible`` 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 ``invisible=`` 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. Note that ``invisible`` can also be applied to other elements of
|
||||
the view such as ``button`` or ``group``.
|
||||
|
||||
`invisible`, `readonly` and `required` can have any Python expression as value. The expression
|
||||
gives the condition in which the property applies. For example:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<form>
|
||||
<field name="description" invisible="not is_partner"/>
|
||||
<field name="is_partner" invisible="True"/>
|
||||
</form>
|
||||
|
||||
This means that the ``description`` field is invisible when ``is_partner`` is ``False``. It is
|
||||
important to note that a field used in ``invisible`` **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 ``invisible``.
|
||||
|
||||
- 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`` attribute.
|
||||
|
||||
.. 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:: 11_sprinkles/decoration.png
|
||||
:align: center
|
||||
:alt: List view with decorations and optional field
|
||||
|
||||
.. image:: 11_sprinkles/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/view_architectures/search` and :ref:`reference/view_architectures/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:: 11_sprinkles/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)]"/>
|
||||
</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:: 11_sprinkles/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
|
||||
:doc:`09_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 :doc:`next chapter
|
||||
<12_inheritance>` doesn't require stat buttons ;-)
|
||||
|
||||
.. _order_by:
|
||||
https://www.postgresql.org/docs/12/queries-order.html
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,249 @@
|
||||
=======================
|
||||
Chapter 12: 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:: 12_inheritance/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:: 12_inheritance/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.
|
||||
|
||||
It is also important to note that even though we can directly override the
|
||||
:meth:`~odoo.models.Model.unlink` method, you will almost always want to write a new method with
|
||||
the decorator :func:`~odoo.api.ondelete` instead. Methods marked with this decorator will be
|
||||
called during :meth:`~odoo.models.Model.unlink` and avoids some issues that can occur during
|
||||
uninstalling the model's module when :meth:`~odoo.models.Model.unlink` is directly overridden.
|
||||
|
||||
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: create a new method with the :func:`~odoo.api.ondelete` decorator 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:: 12_inheritance/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 the field that references the salesperson in
|
||||
``estate.property``
|
||||
===================== ================================================================
|
||||
|
||||
- Add a domain to the field so it only lists the available properties.
|
||||
|
||||
In the next section 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/view_records/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:: 12_inheritance/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 :doc:`next chapter <13_other_module>`, we will learn how to
|
||||
interact with other modules.
|
||||
|
||||
.. _XPath: https://w3.org/TR/xpath
|
||||
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,170 @@
|
||||
=======================================
|
||||
Chapter 13: Interact With Other Modules
|
||||
=======================================
|
||||
|
||||
In the :doc:`previous chapter <12_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:: 13_other_module/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
|
||||
:doc:`beginning of the tutorial <02_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.
|
||||
|
||||
.. _tutorials/server_framework_101/13_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
|
||||
:doc:`previous chapter <12_inheritance>` since you might have missed
|
||||
something ;-)
|
||||
|
||||
As a first step, we need to extend the action called when pressing the
|
||||
:doc:`'Sold' button <09_actions>` on a property. To do so, we need to
|
||||
create a :doc:`model inheritance <12_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'
|
||||
|
||||
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' which have been
|
||||
made human readable with the :class:`~odoo.fields.Command` namespace. This namespace represents
|
||||
a triplet command to execute on a set of records. The triplet was originally the only option to
|
||||
do these commands, but it is now standard to use the namespace instead. The format is to place
|
||||
them in a list which is executed sequentially. Here is a simple example to include a One2many
|
||||
field ``line_ids`` at creation of a ``test_model``::
|
||||
|
||||
from odoo import Command
|
||||
|
||||
def inherited_action(self):
|
||||
self.env["test_model"].create(
|
||||
{
|
||||
"name": "Test",
|
||||
"line_ids": [
|
||||
Command.create({
|
||||
"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 :doc:`next chapter
|
||||
<14_qwebintro>`, we will introduce the templating mechanism used in Odoo.
|
||||
|
After Width: | Height: | Size: 513 KiB |
@@ -0,0 +1,129 @@
|
||||
===================================
|
||||
Chapter 14: 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/view_architectures/kanban`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section a Kanban view of the properties should be created:
|
||||
|
||||
.. image:: 14_qwebintro/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 t-if="record.state.raw_value == 'new'">
|
||||
This is new!
|
||||
</div>
|
||||
</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/view_architectures/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!
|
||||
|
||||
.. _templating:
|
||||
https://en.wikipedia.org/wiki/Template_processor
|
||||
.. _kanban board:
|
||||
https://en.wikipedia.org/wiki/Kanban_board
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,30 @@
|
||||
==========================
|
||||
Chapter 15: The final word
|
||||
==========================
|
||||
|
||||
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
|
||||
:doc:`/contributing/development/coding_guidelines`.
|
||||
|
||||
.. exercise:: Polish your code.
|
||||
|
||||
Refactor your code to respect the coding guidelines. Don't forget to run your linter and
|
||||
respect the module structure, the variable names, the method name convention, the model
|
||||
attribute order and the xml ids.
|
||||
|
||||
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.
|
||||