[MERGE] Forward-port of branch 14.0 to master
172
.tx/config
@ -2,59 +2,14 @@
|
||||
host = https://www.transifex.com
|
||||
type = PO
|
||||
|
||||
[odoo-14-doc.accounting]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/accounting.po
|
||||
source_file = locale/sources/accounting.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.applications]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/applications.po
|
||||
source_file = locale/sources/applications.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.contributing]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/contributing.po
|
||||
source_file = locale/sources/contributing.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.crm]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/crm.po
|
||||
source_file = locale/sources/crm.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.db_management]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/db_management.po
|
||||
source_file = locale/sources/db_management.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.discuss]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/discuss.po
|
||||
source_file = locale/sources/discuss.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.ecommerce]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/ecommerce.po
|
||||
source_file = locale/sources/ecommerce.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.email_marketing]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/email_marketing.po
|
||||
source_file = locale/sources/email_marketing.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.expense]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/expense.po
|
||||
source_file = locale/sources/expense.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.events]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/events.po
|
||||
source_file = locale/sources/events.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.fsm]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/fsm.po
|
||||
source_file = locale/sources/fsm.pot
|
||||
[odoo-14-doc.finance]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/finance.po
|
||||
source_file = locale/sources/finance.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.general]
|
||||
@ -62,84 +17,24 @@ file_filter = locale/<lang>/LC_MESSAGES/general.po
|
||||
source_file = locale/sources/general.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.getting_started]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/getting_started.po
|
||||
source_file = locale/sources/getting_started.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.helpdesk]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/helpdesk.po
|
||||
source_file = locale/sources/helpdesk.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.index]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/index.po
|
||||
source_file = locale/sources/index.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.inventory]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/inventory.po
|
||||
source_file = locale/sources/inventory.pot
|
||||
[odoo-14-doc.inventory_and_mrp]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/inventory_and_mrp.po
|
||||
source_file = locale/sources/inventory_and_mrp.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.iot]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/iot.po
|
||||
source_file = locale/sources/iot.pot
|
||||
[odoo-14-doc.marketing]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/marketing.po
|
||||
source_file = locale/sources/marketing.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.livechat]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/livechat.po
|
||||
source_file = locale/sources/livechat.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.mobile]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/mobile.po
|
||||
source_file = locale/sources/mobile.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.marketing_automation]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/marketing_automation.po
|
||||
source_file = locale/sources/marketing_automation.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.manufacturing]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/manufacturing.po
|
||||
source_file = locale/sources/manufacturing.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.planning]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/planning.po
|
||||
source_file = locale/sources/planning.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.point_of_sale]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/point_of_sale.po
|
||||
source_file = locale/sources/point_of_sale.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.portal]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/portal.po
|
||||
source_file = locale/sources/portal.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.practical]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/practical.po
|
||||
source_file = locale/sources/practical.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.project]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/project.po
|
||||
source_file = locale/sources/project.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.purchase]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/purchase.po
|
||||
source_file = locale/sources/purchase.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.quality]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/quality.po
|
||||
source_file = locale/sources/quality.pot
|
||||
[odoo-14-doc.productivity]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/productivity.po
|
||||
source_file = locale/sources/productivity.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.sales]
|
||||
@ -147,42 +42,17 @@ file_filter = locale/<lang>/LC_MESSAGES/sales.po
|
||||
source_file = locale/sources/sales.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.sign]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/sign.po
|
||||
source_file = locale/sources/sign.pot
|
||||
[odoo-14-doc.services]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/services.po
|
||||
source_file = locale/sources/services.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.sms_marketing]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/sms_marketing.po
|
||||
source_file = locale/sources/sms_marketing.pot
|
||||
[odoo-14-doc.user_settings]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/settings.po
|
||||
source_file = locale/sources/settings.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.social_marketing]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/social_marketing.po
|
||||
source_file = locale/sources/social_marketing.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.studio]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/studio.po
|
||||
source_file = locale/sources/studio.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.support]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/support.po
|
||||
source_file = locale/sources/support.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.survey]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/survey.po
|
||||
source_file = locale/sources/survey.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.timesheets]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/timesheets.po
|
||||
source_file = locale/sources/timesheets.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-14-doc.website]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/website.po
|
||||
source_file = locale/sources/website.pot
|
||||
[odoo-14-doc.websites]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/websites.po
|
||||
source_file = locale/sources/websites.pot
|
||||
source_lang = en
|
||||
|
@ -5,6 +5,7 @@ Tutorials
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
||||
howtos/rdtraining
|
||||
howtos/themes
|
||||
howtos/website
|
||||
howtos/backend
|
||||
|
74
content/developer/howtos/rdtraining.rst
Normal file
@ -0,0 +1,74 @@
|
||||
:show-content:
|
||||
|
||||
.. _howto/rdtraining:
|
||||
|
||||
===============
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Welcome to the Getting Started Odoo tutorial! If you reached this page that means you are
|
||||
interested in the development of your own Odoo module. It might also mean that you recently
|
||||
joined the Odoo company for a rather technical position. In any case, your journey to the
|
||||
technical side of Odoo starts here.
|
||||
|
||||
This training is split in two parts. The first part is the 'Core Training': its objective is to
|
||||
give you an insight of the most important parts of the Odoo development framework.
|
||||
The chapters should be followed in their given order since they cover the development of a new
|
||||
Odoo application from scratch in an incremental way. In other words, each chapter depends on the
|
||||
previous one.
|
||||
|
||||
The second part covers a set of 'Advanced' topics. Each topic can be followed independently, but
|
||||
requires the 'Core Training'. Note that some advanced topics cover basic features
|
||||
of an Odoo module, so it's a good idea to give them a quick look.
|
||||
|
||||
All topics are built around a business case we will enhance along the way. The reader is expected
|
||||
to actively take part in the training by writing the solution for each exercise.
|
||||
|
||||
Ready? Let's get started!
|
||||
|
||||
Core Training
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:caption: Advanced Topics
|
||||
:titlesonly:
|
||||
:glob:
|
||||
|
||||
rdtraining/0*
|
||||
rdtraining/1*
|
||||
|
||||
* :doc:`rdtraining/01_architecture`
|
||||
* :doc:`rdtraining/02_setup`
|
||||
* :doc:`rdtraining/03_newapp`
|
||||
* :doc:`rdtraining/04_basicmodel`
|
||||
* :doc:`rdtraining/05_securityintro`
|
||||
* :doc:`rdtraining/06_firstui`
|
||||
* :doc:`rdtraining/07_basicviews`
|
||||
* :doc:`rdtraining/08_relations`
|
||||
* :doc:`rdtraining/09_compute_onchange`
|
||||
* :doc:`rdtraining/10_actions`
|
||||
* :doc:`rdtraining/11_constraints`
|
||||
* :doc:`rdtraining/12_sprinkles`
|
||||
* :doc:`rdtraining/13_inheritance`
|
||||
* :doc:`rdtraining/14_other_module`
|
||||
* :doc:`rdtraining/15_qwebintro`
|
||||
* :doc:`rdtraining/16_guidelines_pr`
|
||||
|
||||
Advanced topics
|
||||
===============
|
||||
|
||||
.. toctree::
|
||||
:caption: Advanced Topics
|
||||
:titlesonly:
|
||||
|
||||
rdtraining/B_acl_irrules
|
||||
rdtraining/C_data
|
||||
rdtraining/E_unittest
|
||||
rdtraining/J_reports
|
||||
rdtraining/K_dashboard
|
||||
|
||||
* :doc:`rdtraining/B_acl_irrules`
|
||||
* :doc:`rdtraining/C_data`
|
||||
* :doc:`rdtraining/E_unittest`
|
||||
* :doc:`rdtraining/J_reports`
|
||||
* :doc:`rdtraining/K_dashboard`
|
131
content/developer/howtos/rdtraining/01_architecture.rst
Normal file
@ -0,0 +1,131 @@
|
||||
.. _howto/rdtraining/01_architecture:
|
||||
|
||||
================================
|
||||
Chapter 1: Architecture Overview
|
||||
================================
|
||||
|
||||
Multitier application
|
||||
=====================
|
||||
|
||||
Odoo follows a `multitier architecture`_, meaning that the presentation, the business
|
||||
logic and the data storage are separated. More specifically, it uses a three-tier architecture
|
||||
(image from Wikipedia):
|
||||
|
||||
.. image:: 01_architecture/media/three_tier.svg
|
||||
:align: center
|
||||
:alt: Three-tier architecture
|
||||
|
||||
The presentation tier is a combination of HTML5, JavaScript and CSS. The logic tier is exclusively
|
||||
written in Python, while the data tier only supports PostgreSQL as an RDBMS.
|
||||
|
||||
Depending on the scope of your module, Odoo development can be done in any of these tiers.
|
||||
Therefore, before going any further, it may be a good idea to refresh your memory if you don't have
|
||||
an intermediate level in these topics.
|
||||
|
||||
In order to go through this tutorial, you will need a very basic knowledge of HTML and an intermediate
|
||||
level of Python. Advanced topics will require more knowledge in the other subjects. There are
|
||||
plenty of tutorials freely accessible, so we cannot recommend one over another since it depends
|
||||
on your background.
|
||||
|
||||
For reference this is the official `Python tutorial`_.
|
||||
|
||||
Odoo modules
|
||||
============
|
||||
|
||||
Both server and client extensions are packaged as *modules* which are
|
||||
optionally loaded in a *database*. A module is a collection of functions and data that target a
|
||||
single purpose.
|
||||
|
||||
Odoo modules can either add brand new business logic to an Odoo system or
|
||||
alter and extend existing business logic. One module can be created to add your
|
||||
country's accounting rules to Odoo's generic accounting support, while
|
||||
a different module can add support for real-time visualisation of a bus fleet.
|
||||
|
||||
Everything in Odoo starts and ends with modules.
|
||||
|
||||
Terminology: developers group their business features in Odoo *modules*. The main user-facing
|
||||
modules are flagged and exposed as *Apps*, but a majority of the modules aren't Apps. *Modules*
|
||||
may also be referred to as *addons* and the directories where the Odoo server finds them
|
||||
form the ``addons_path``.
|
||||
|
||||
Composition of a module
|
||||
-----------------------
|
||||
|
||||
An Odoo module **can** contain a number of elements:
|
||||
|
||||
:ref:`Business objects <reference/orm>`
|
||||
A business object (e.g. an invoice) is declared as a Python class. The fields defined in
|
||||
these classes are automatically mapped to database columns thanks to the
|
||||
:abbr:`ORM (Object-Relational Mapping)` layer.
|
||||
|
||||
:ref:`Object views <reference/views>`
|
||||
Define UI display
|
||||
|
||||
:ref:`Data files <reference/data>`
|
||||
XML or CSV files declaring the model data:
|
||||
|
||||
* :ref:`views <reference/views>` or :ref:`reports <reference/reports>`,
|
||||
* configuration data (modules parametrization, :ref:`security rules <reference/security>`),
|
||||
* demonstration data
|
||||
* and more
|
||||
|
||||
:ref:`Web controllers <reference/controllers>`
|
||||
Handle requests from web browsers
|
||||
|
||||
Static web data
|
||||
Images, CSS or JavaScript files used by the web interface or website
|
||||
|
||||
None of these elements are mandatory. Some modules may only add data files (e.g. country-specific
|
||||
accounting configuration), while others may only add business objects. During this training, we will
|
||||
create business objects, object views and data files.
|
||||
:ref:`Web controllers <howto/rdtraining/G_website>` and
|
||||
:ref:`static web data <howto/rdtraining/I_jswidget>` are advanced topics.
|
||||
|
||||
Module structure
|
||||
----------------
|
||||
|
||||
Each module is a directory within a *module directory*. Module directories
|
||||
are specified by using the :option:`--addons-path <odoo-bin --addons-path>`
|
||||
option.
|
||||
|
||||
An Odoo module is declared by its :ref:`manifest <reference/module/manifest>`.
|
||||
|
||||
When an Odoo module includes business objects (i.e. Python files), they are organized as a
|
||||
`Python package <https://docs.python.org/3/tutorial/modules.html#packages>`_
|
||||
with a ``__init__.py`` file. This file contains import instructions for various Python
|
||||
files in the module.
|
||||
|
||||
Here is a simplified module directory:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
module
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── data
|
||||
│ └── *.xml
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
Odoo Editions
|
||||
=============
|
||||
|
||||
Odoo is available in `two versions`_: Odoo Enterprise (licensed & shared sources) and Odoo Community
|
||||
(open-source). In addition to services such as support or upgrades, the Enterprise version provides extra
|
||||
functionalities to Odoo. From a technical point-of-view, these functionalities are simply
|
||||
new modules installed on top of the modules provided by the Community version.
|
||||
|
||||
Ready to start? Before writing actual code, let's go to the
|
||||
:ref:`next chapter <howto/rdtraining/02_setup>` to review the Odoo installation process. Even if
|
||||
Odoo is already running on your system, we strongly suggest you go through this chapter
|
||||
to make sure we start on the same page during the development of our new application.
|
||||
|
||||
.. _multitier architecture:
|
||||
https://en.wikipedia.org/wiki/Multitier_architecture
|
||||
|
||||
.. _Python tutorial:
|
||||
https://docs.python.org/3.6/tutorial/
|
||||
|
||||
.. _two versions:
|
||||
https://www.odoo.com/page/editions
|
@ -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 |
452
content/developer/howtos/rdtraining/02_setup.rst
Normal file
@ -0,0 +1,452 @@
|
||||
.. _howto/rdtraining/02_setup:
|
||||
|
||||
=========================================
|
||||
Chapter 2: Development Environment Set-up
|
||||
=========================================
|
||||
|
||||
There are multiple ways to install Odoo depending on the intended use case.
|
||||
|
||||
This document attempts to describe the installation options for an internal Odoo R&D developer. We
|
||||
assume that you are installing your development environment on a standard Odoo laptop with Linux
|
||||
Mint installed and up-to-date. At the time of writing, we are using a vanilla Linux Mint 20
|
||||
(Ubuntu 20.04) as a starting point.
|
||||
|
||||
|
||||
If you are using another environment, you can refer to :ref:`setup/install/source`.
|
||||
|
||||
|
||||
Fetch the sources & configure git
|
||||
=================================
|
||||
|
||||
Install and configure git
|
||||
-------------------------
|
||||
|
||||
The very first step of the installation process is to install the `git version control system <https://git-scm.com/>`__
|
||||
because the Odoo source code is managed on GitHub. Once installed, you can set your name and email:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo apt install git
|
||||
$ git config --global user.name "Your full name"
|
||||
$ git config --global user.email "xyz@odoo.com"
|
||||
|
||||
Configure GitHub
|
||||
----------------
|
||||
|
||||
To fetch the sources and contribute to Odoo's development you will need a GitHub account. We
|
||||
recommend using your trigram (xyz) followed by '-odoo' as your username: 'xyz-odoo'. If you prefer,
|
||||
you can also use your personal GitHub account.
|
||||
|
||||
|
||||
The easiest way to authenticate with GitHub is to use an SSH connection. Using the SSH
|
||||
authentication will allow you to connect to GitHub without supplying your username and
|
||||
password every time.
|
||||
|
||||
|
||||
The following instructions are based on the official `GitHub documentation <https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh>`__.
|
||||
|
||||
|
||||
Here is a step-by-step procedure:
|
||||
|
||||
|
||||
- Generate a new SSH key, add it to the ssh-agent and copy the SSH key to your clipboard.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ssh-keygen -t ed25519 -C "xyz@odoo.com"
|
||||
$ ssh-add ~/.ssh/id_ed25519
|
||||
$ sudo apt-get install xclip
|
||||
$ xclip -sel clip < ~/.ssh/id_ed25519.pub
|
||||
|
||||
|
||||
In Github:
|
||||
|
||||
|
||||
- In the upper-right corner of any page, click your profile photo, then click Settings
|
||||
|
||||
.. image:: 02_setup/media/userbar-account-settings.png
|
||||
|
||||
- In the user settings sidebar, click SSH and GPG keys.
|
||||
|
||||
.. image:: 02_setup/media/settings-sidebar-ssh-keys.png
|
||||
|
||||
- Click New SSH key or Add SSH key.
|
||||
|
||||
.. image:: 02_setup/media/ssh-add-ssh-key.png
|
||||
|
||||
- In the "Title" field, add a descriptive label for the new key.
|
||||
- Paste your key into the "Key" field.
|
||||
|
||||
.. image:: 02_setup/media/ssh-key-paste.png
|
||||
|
||||
- Click Add SSH key.
|
||||
|
||||
|
||||
Fetch the sources
|
||||
-----------------
|
||||
|
||||
All the Odoo sources will be located in `$HOME/src/`
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ mkdir -p $HOME/src
|
||||
$ cd $HOME/src
|
||||
$ git clone git@github.com:odoo/odoo.git
|
||||
$ git clone git@github.com:odoo/enterprise.git
|
||||
|
||||
.. tip:: Cloning the repositories will take a while, enjoy a cup of coffee while you wait.
|
||||
|
||||
.. tip:: You may need to ask your manager for read rights to fetch the enterprise repository.
|
||||
|
||||
.. _howto/rdtraining/02_setup/development_repository:
|
||||
|
||||
Configure development repository
|
||||
--------------------------------
|
||||
|
||||
To contribute to Odoo's development you will need to
|
||||
`fork the repository <https://guides.github.com/activities/forking/>`__, create a branch containing
|
||||
your code in the fork and submit a
|
||||
`Pull Request <https://docs.github.com/en/github/getting-started-with-github/github-glossary#pull-request>`__
|
||||
to the Odoo repository.
|
||||
|
||||
If you are lucky enough to work at Odoo, the forks already exist. They are called
|
||||
`odoo-dev/odoo` and `odoo-dev/enterprise`.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd $HOME/src/odoo
|
||||
$ git remote add odoo-dev git@github.com:odoo-dev/odoo.git #add odoo-dev as a new remote
|
||||
$ git remote rename origin odoo #change the name of origin (the odoo repository) to odoo
|
||||
$ git remote set-url --push odoo no_push #remove the possibility to push to odoo (you can only push to odoo-dev)
|
||||
|
||||
$ cd $HOME/src/enterprise
|
||||
$ git remote add enterprise-dev git@github.com:odoo-dev/enterprise.git
|
||||
$ git remote rename origin enterprise
|
||||
$ git remote set-url --push enterprise no_push
|
||||
|
||||
|
||||
Useful git commands
|
||||
-------------------
|
||||
|
||||
Here are some useful git commands for your day-to-day work.
|
||||
|
||||
* Change branch:
|
||||
When you change branches, both repositories (odoo and enterprise) must be synchronized, i.e. both
|
||||
need to be in the same branch.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd $HOME/src/odoo
|
||||
$ git checkout 14.0
|
||||
|
||||
$ cd $HOME/src/enterprise
|
||||
$ git checkout 14.0
|
||||
|
||||
* Fetch and rebase:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd $HOME/src/odoo
|
||||
$ git fetch --all --prune
|
||||
$ git rebase --autostash odoo/14.0
|
||||
|
||||
$ cd $HOME/src/enterprise
|
||||
$ git fetch --all --prune
|
||||
$ git rebase --autostash enterprise/14.0
|
||||
|
||||
|
||||
Install the dependencies
|
||||
========================
|
||||
|
||||
Python
|
||||
------
|
||||
|
||||
Odoo requires Python 3.6 or later, if your computer is up-to-date you should already be at this
|
||||
version or higher.
|
||||
|
||||
You can check your Python version with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python3 --version
|
||||
|
||||
Install pip3 and libraries
|
||||
--------------------------
|
||||
|
||||
For libraries using native code, installation of development tools and native dependencies is
|
||||
required before installing the Python dependencies of Odoo.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo apt install python3-pip python3-dev libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev libssl-dev libpq-dev libjpeg-dev
|
||||
|
||||
|
||||
Install odoo requirements
|
||||
-------------------------
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd $HOME/src/odoo
|
||||
$ pip3 install -r requirements.txt
|
||||
|
||||
.. _howto/rdtraining/02_setup/install-wkhtmltopdf:
|
||||
|
||||
Install wkhtmltopdf
|
||||
-------------------
|
||||
|
||||
wkhtmltopdf is a library to render HTML into PDF. Odoo uses it to create PDF reports. wkhtmltopdf
|
||||
is not installed through pip and must be installed manually in version 0.12.5 to support
|
||||
headers and footers.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd /tmp/
|
||||
$ sudo wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.focal_amd64.deb
|
||||
$ sudo gdebi --n wkhtmltox_0.12.5-1.focal_amd64.deb
|
||||
$ sudo ln -s /usr/local/bin/wkhtmltopdf /usr/bin
|
||||
$ sudo ln -s /usr/local/bin/wkhtmltoimage /usr/bin
|
||||
|
||||
Right-to-left interface support
|
||||
-------------------------------
|
||||
|
||||
In order to support right-to-left (RTL) languages, we need `rtlcss` to convert the CSS files:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo apt-get install nodejs npm
|
||||
$ sudo npm install -g rtlcss
|
||||
|
||||
Install PostgreSQL
|
||||
------------------
|
||||
|
||||
As seen in :ref:`howto/rdtraining/01_architecture`, Odoo uses PostgreSQL as a RDBMS. In the context of a
|
||||
development machine, the easiest approach is to install it locally. Then we can create a PostgreSQL user
|
||||
corresponding to our current user:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo apt install postgresql postgresql-client
|
||||
$ sudo -u postgres createuser -s $USER
|
||||
|
||||
|
||||
Some useful SQL commands:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ createdb $DB_NAME #Create a database
|
||||
$ dropdb $DB_NAME #Drop a database
|
||||
|
||||
$ psql $DB_NAME #Connect to a database
|
||||
\l #List all the available databases
|
||||
\dt #List all the tables of the $DB_NAME database
|
||||
\d $TABLE_NAME #Show the structure of the table $TABLE_NAME
|
||||
\q #Quit the psql environment (ctrl + d)
|
||||
|
||||
Run the server
|
||||
==============
|
||||
|
||||
Running odoo-bin
|
||||
----------------
|
||||
|
||||
Once all dependencies are set up, Odoo can be launched by running odoo-bin, the command-line interface of the server.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd $HOME/src/odoo/
|
||||
$ ./odoo-bin --addons-path="addons/,../enterprise/" -d rd-demo
|
||||
|
||||
There are multiple :ref:`command-line arguments <reference/cmdline/server>` that you can use to
|
||||
configure the server. In this training you will only need some of them.
|
||||
|
||||
.. option:: -d <database>
|
||||
|
||||
The database that is going to be used.
|
||||
|
||||
.. option:: --addons-path <directories>
|
||||
|
||||
A comma-separated list of directories in which modules are stored. These directories are scanned
|
||||
for modules.
|
||||
|
||||
.. option:: --limit-time-cpu <limit>
|
||||
|
||||
Prevents the worker from using more than <limit> CPU seconds for each request.
|
||||
|
||||
.. option:: --limit-time-real <limit>
|
||||
|
||||
Prevents the worker from taking longer than <limit> seconds to process a request.
|
||||
|
||||
The last two can be used to prevent the worker from being killed when debugging the source code.
|
||||
|
||||
.. tip:: You may face an error similar to `AttributeError: module '$MODULE_NAME' has no attribute '$ATTRIBUTE'`
|
||||
|
||||
In this case you may need to re-install the module with `$ pip install --upgrade --force-reinstall $MODULE_NAME`
|
||||
|
||||
If this error occurs with more than one module then you may need to re-install all the
|
||||
requirements with `$ pip3 install --upgrade --force-reinstall -r requirements.txt`
|
||||
|
||||
You can also clear the python cache to solve the issue
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd $HOME/.local/lib/python3.8/site-packages/
|
||||
$ find -name '*.pyc' -type f -delete
|
||||
|
||||
|
||||
Log in to Odoo
|
||||
--------------
|
||||
|
||||
Open `http://localhost:8069/` on your browser. We recommend you use:
|
||||
`Firefox <https://www.mozilla.org/fr/firefox/new/>`__,
|
||||
`Chrome <https://www.google.com/intl/fr/chrome/>`__
|
||||
(`Chromium <https://www.chromium.org/Home>`__ the open source equivalent) or any other browser with
|
||||
development tools.
|
||||
|
||||
To log in as the administrator user, you can use the following credentials:
|
||||
|
||||
* email = `admin`
|
||||
* password = `admin`
|
||||
|
||||
The developer mode
|
||||
==================
|
||||
|
||||
The Developer or Debug Mode gives you access to additional (advanced) tools.
|
||||
|
||||
This is useful for training and we assume that the user is in developer mode for the rest of the tutorials.
|
||||
|
||||
To activate the developer or debug mode you can follow the steps `here <https://www.odoo.com/documentation/user/general/developer_mode/activate.html>`__.
|
||||
|
||||
Extra tools
|
||||
===========
|
||||
|
||||
Code Editor
|
||||
-----------
|
||||
|
||||
If you are working at Odoo, many of your colleagues are using `VSCode`_ (`VSCodium`_ the open source
|
||||
equivalent), `Sublime Text`_, `Atom`_ or `PyCharm`_. However you are free to
|
||||
choose your preferred editor.
|
||||
|
||||
Don't forget to configure your linters correctly. Using a linter can help you by showing syntax and semantic
|
||||
warnings or errors. Odoo source code tries to respect Python and JavaScript standards, but some of
|
||||
them can be ignored.
|
||||
|
||||
For Python, we use PEP8 with these options ignored:
|
||||
|
||||
- E501: line too long
|
||||
- E301: expected 1 blank line, found 0
|
||||
- E302: expected 2 blank lines, found 1
|
||||
|
||||
For JavaScript, we use ESLint and you can find a `configuration file example here`_.
|
||||
|
||||
.. _configuration file example here: https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines#use-a-linter
|
||||
.. _VSCode: https://code.visualstudio.com/
|
||||
.. _VSCodium: https://vscodium.com/
|
||||
.. _Sublime Text: https://www.sublimetext.com/
|
||||
.. _PyCharm: https://www.jetbrains.com/pycharm/download/#section=linux
|
||||
.. _Atom: https://atom.io/
|
||||
|
||||
Administrator tools for PostgreSQL
|
||||
-----------------------------------
|
||||
|
||||
You can manage your PostgreSQL databases using the command line as demonstrated earlier or using
|
||||
a GUI application such as `pgAdmin <https://www.pgadmin.org/download/pgadmin-4-apt/>`__ or `DBeaver <https://dbeaver.io/>`__.
|
||||
|
||||
To connect the GUI application to your database we recommend you connect using the Unix socket.
|
||||
|
||||
* Host name/address = /var/run/postgresql
|
||||
* Port = 5432
|
||||
* Username = $USER
|
||||
|
||||
|
||||
Python Debugging
|
||||
----------------
|
||||
|
||||
When facing a bug or trying to understand how the code works, simply printing things out can
|
||||
go a long way, but a proper debugger can save a lot of time.
|
||||
|
||||
You can use a classic Python library debugger (`pdb <https://docs.python.org/3/library/pdb.html>`__,
|
||||
`pudb <https://pypi.org/project/pudb/>`__ or `ipdb <https://pypi.org/project/ipdb/>`__) or you can
|
||||
use your editor's debugger. To avoid difficult configurations in the beginning, it is
|
||||
easier if you use a library debugger.
|
||||
|
||||
In the following example we use ipdb, but the process is similar with other libraries.
|
||||
|
||||
- Install the library:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip3 install ipdb
|
||||
|
||||
- Trigger (breakpoint):
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
import ipdb; ipdb.set_trace()
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 2
|
||||
|
||||
def copy(self, default=None):
|
||||
import ipdb; ipdb.set_trace()
|
||||
self.ensure_one()
|
||||
chosen_name = default.get('name') if default else ''
|
||||
new_name = chosen_name or _('%s (copy)') % self.name
|
||||
default = dict(default or {}, name=new_name)
|
||||
return super(Partner, self).copy(default)
|
||||
|
||||
Here is a list of commands:
|
||||
|
||||
.. option:: h(elp) [command]
|
||||
|
||||
Without an argument, print the list of available commands. With a command as an argument, print help
|
||||
about that command.
|
||||
|
||||
.. option:: pp expression
|
||||
|
||||
The value of the ``expression`` is pretty-printed using the ``pprint`` module.
|
||||
|
||||
.. option:: w(here)
|
||||
|
||||
Print a stack trace, with the most recent frame at the bottom.
|
||||
|
||||
.. option:: d(own)
|
||||
|
||||
Move the current frame one level down in the stack trace (to a newer frame).
|
||||
|
||||
.. option:: u(p)
|
||||
|
||||
Move the current frame one level up in the stack trace (to an older frame).
|
||||
|
||||
.. option:: n(ext)
|
||||
|
||||
Continue the execution until the next line in the current function is reached or it returns.
|
||||
|
||||
.. option:: c(ontinue)
|
||||
|
||||
Continue the execution and only stop when a breakpoint is encountered.
|
||||
|
||||
.. option:: s(tep)
|
||||
|
||||
Execute the current line, stop at the first possible occasion (either in a function that is
|
||||
called or on the next line in the current function).
|
||||
|
||||
.. option:: q(uit)
|
||||
|
||||
Quit the debugger. The program being executed is aborted.
|
||||
|
||||
.. tip::
|
||||
|
||||
To avoid killing the worker when debugging, you can add these arguments when launching the
|
||||
server: `--limit-time-cpu=9999 --limit-time-real=9999`
|
||||
Another solution is to add them directly in the `~/.odoorc` file:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cat ~/.odoorc
|
||||
[options]
|
||||
limit_time_cpu = 9999
|
||||
limit_time_real = 9999
|
||||
|
||||
Now that your server is running, it's time to start
|
||||
:ref:`writing your own application <howto/rdtraining/03_newapp>`!
|
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 20 KiB |
104
content/developer/howtos/rdtraining/03_newapp.rst
Normal file
@ -0,0 +1,104 @@
|
||||
.. _howto/rdtraining/03_newapp:
|
||||
|
||||
============================
|
||||
Chapter 3: A New Application
|
||||
============================
|
||||
|
||||
The purpose of this chapter is to lay the foundation for the creation of a completely new Odoo module.
|
||||
We will start from scratch with the minimum needed to have our module recognized by Odoo.
|
||||
In the upcoming chapters, we will progressively add features to build a realistic business case.
|
||||
|
||||
The Real Estate Advertisement module
|
||||
====================================
|
||||
|
||||
Our new module will cover a business area which is very specific and therefore not included in the
|
||||
standard set of modules: real estate. It is worth noting that before
|
||||
developing a new module, it is good practice to verify that Odoo doesn't already provide a way
|
||||
to answer the specific business case.
|
||||
|
||||
Here is an overview of the main list view containing some advertisements:
|
||||
|
||||
.. image:: 03_newapp/media/overview_list_view_01.png
|
||||
:align: center
|
||||
:alt: List view 01
|
||||
|
||||
The top area of the form view summarizes important information for the property, such as the name,
|
||||
the property type, the postcode and so on. The first tab contains information describing the
|
||||
property: bedrooms, living area, garage, garden...
|
||||
|
||||
.. image:: 03_newapp/media/overview_form_view_01.png
|
||||
:align: center
|
||||
:alt: Form view 01
|
||||
|
||||
The second tab lists the offers for the property. We can see here that potential buyers can make
|
||||
offers above or below the expected selling price. It is up to the seller to accept an offer.
|
||||
|
||||
.. image:: 03_newapp/media/overview_form_view_02.png
|
||||
:align: center
|
||||
:alt: Form view 02
|
||||
|
||||
Here is a quick video showing the workflow of the module.
|
||||
|
||||
Hopefully, this video will be recorded soon :-)
|
||||
|
||||
Prepare the addon directory
|
||||
===========================
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`manifest <reference/module/manifest>`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: the goal of this section is to have Odoo recognize our new module, which will
|
||||
be an empty shell for now. It will be listed in the Apps:
|
||||
|
||||
.. image:: 03_newapp/media/app_in_list.png
|
||||
:align: center
|
||||
:alt: The new module appears in the list
|
||||
|
||||
The first step of module creation is to create a new directory. To ease the development, we
|
||||
suggest you first create the directory ``/home/$USER/src/custom``. In this directory we add
|
||||
another directory ``estate``, which is our module.
|
||||
|
||||
A module must contain at least 2 files: the ``__manifest__.py`` file and a ``__init__.py`` file.
|
||||
The ``__init__.py`` file can remain empty for now and we'll come back to it in the next chapter.
|
||||
On the other hand, the ``__manifest__.py`` file must describe our module and cannot remain empty.
|
||||
Its only required field is the ``name``, but it usually contains much more information.
|
||||
|
||||
Take a look at the
|
||||
`CRM file <https://github.com/odoo/odoo/blob/fc92728fb2aa306bf0e01a7f9ae1cfa3c1df0e10/addons/crm/__manifest__.py#L1-L67>`__
|
||||
as an example. In addition to providing the description of the module (``name``, ``category``,
|
||||
``summary``, ``website``...), it lists its dependencies (``depends``). A dependency means that the
|
||||
Odoo framework will ensure that these modules are installed before our module is installed. Moreover, if
|
||||
one of these dependencies is uninstalled, then our module and **any other that depends on it will also
|
||||
be uninstalled**. Think about your favorite Linux distribution package manager
|
||||
(``apt``, ``dnf``, ``pacman``...): Odoo works in the same way.
|
||||
|
||||
.. exercise:: Create the required addon files.
|
||||
|
||||
Create the following folders and files:
|
||||
|
||||
- ``/home/$USER/src/custom/estate/__init__.py``
|
||||
- ``/home/$USER/src/custom/estate/__manifest__.py``
|
||||
|
||||
The ``__manifest__.py`` file should only define the name and the dependencies of our modules.
|
||||
Two framework modules are necessary: ``base`` and ``web``.
|
||||
|
||||
|
||||
Restart the Odoo server and add the ``custom`` folder to the ``addons-path``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./odoo-bin --addons-path=../custom,../enterprise/,addons
|
||||
|
||||
Go to Apps, click on Update Apps List, search for ``estate`` and... tadaaa, your module appears!
|
||||
Did it not appear? Maybe try removing the default 'Apps' filter ;-)
|
||||
|
||||
.. exercise:: Make your module an 'App'.
|
||||
|
||||
Add the appropriate key to your ``__manifest__.py`` so that the module appears when the 'Apps'
|
||||
filter is on.
|
||||
|
||||
You can even install the module! But obviously it's an empty shell, so no menu will appear.
|
||||
|
||||
All good? If yes, then let's :ref:`create our first model <howto/rdtraining/04_basicmodel>`!
|
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 27 KiB |
298
content/developer/howtos/rdtraining/04_basicmodel.rst
Normal file
@ -0,0 +1,298 @@
|
||||
.. _howto/rdtraining/04_basicmodel:
|
||||
|
||||
==================================
|
||||
Chapter 4: Models And Basic Fields
|
||||
==================================
|
||||
|
||||
At the end of the :ref:`previous chapter <howto/rdtraining/03_newapp>`, we were able to create an Odoo
|
||||
module. However, at this point it is still an empty shell which doesn't allow us to store any data.
|
||||
In our real estate module, we want to store the information related to the properties
|
||||
(name, description, price, living area...) in a database. The Odoo framework provides tools to
|
||||
facilitate database interactions.
|
||||
|
||||
Before moving forward in the exercise, make sure the ``estate`` module is installed, i.e. it
|
||||
must appear as 'Installed' in the Apps list.
|
||||
|
||||
Object-Relational Mapping
|
||||
=========================
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in the
|
||||
:ref:`reference/orm/model` API.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the table ``estate_property`` should be created:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ psql -d rd-demo
|
||||
rd-demo=# SELECT COUNT(*) FROM estate_property;
|
||||
count
|
||||
-------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
A key component of Odoo is the `ORM`_ layer.
|
||||
This layer avoids having to manually write most `SQL`_
|
||||
and provides extensibility and security services\ [#rawsql]_.
|
||||
|
||||
Business objects are declared as Python classes extending
|
||||
:class:`~odoo.models.Model`, which integrates them into the automated
|
||||
persistence system.
|
||||
|
||||
Models can be configured by setting attributes in their
|
||||
definition. The most important attribute is
|
||||
:attr:`~odoo.models.Model._name`, which is required and defines the name for
|
||||
the model in the Odoo system. Here is a minimum definition of a
|
||||
model::
|
||||
|
||||
from odoo import models
|
||||
|
||||
class TestModel(models.Model):
|
||||
_name = "test.model"
|
||||
|
||||
This definition is enough for the ORM to generate a database table named ``test_model``. The
|
||||
``.`` in the model ``_name`` is automatically converted into a ``_`` by the ORM. By convention all
|
||||
models are located in a ``models`` directory and each model is defined in its own Python
|
||||
file.
|
||||
|
||||
Take a look at how the ``crm_recurring_plan`` table is defined and how the corresponding Python
|
||||
file is imported:
|
||||
|
||||
1. The model is defined in the file ``crm/models/crm_recurring_plan.py``
|
||||
(see `here <https://github.com/odoo/odoo/blob/e80911aaead031e7523173789e946ac1fd27c7dc/addons/crm/models/crm_recurring_plan.py#L1-L9>`__)
|
||||
2. The file ``crm_recurring_plan.py`` is imported in ``crm/models/__init__.py``
|
||||
(see `here <https://github.com/odoo/odoo/blob/e80911aaead031e7523173789e946ac1fd27c7dc/addons/crm/models/__init__.py#L15>`__)
|
||||
3. The folder ``models`` is imported in ``crm/__init__.py``
|
||||
(see `here <https://github.com/odoo/odoo/blob/e80911aaead031e7523173789e946ac1fd27c7dc/addons/crm/__init__.py#L5>`__)
|
||||
|
||||
.. exercise:: Define the real estate properties model.
|
||||
|
||||
Based on example given in the CRM module, create the appropriate files and folder for the
|
||||
``estate_property`` table.
|
||||
|
||||
When the files are created, add a minimum definition for the
|
||||
``estate.property`` model.
|
||||
|
||||
Any modification of the Python files requires a restart of the Odoo server. When we restart
|
||||
the server, we will add the parameters ``-d`` and ``-u``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate
|
||||
|
||||
``-u estate`` means we want to upgrade the ``estate`` module, i.e. the ORM will
|
||||
apply database schema changes. In this case it creates a new table. ``-d rd-demo`` means
|
||||
that the upgrade should be performed on the ``rd-demo`` database. ``-u`` should always be used in
|
||||
combination with ``-d``.
|
||||
|
||||
During the startup you should see the following warnings:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
...
|
||||
WARNING rd-demo odoo.models: The model estate.property has no _description
|
||||
...
|
||||
WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules, consider adding one...
|
||||
...
|
||||
|
||||
If this is the case, then you should be good! To be sure, double check with ``psql`` as demonstrated in
|
||||
the **Goal**.
|
||||
|
||||
.. exercise:: Add a description.
|
||||
|
||||
Add a ``_description`` to your model to get rid of one of the warnings.
|
||||
|
||||
Model fields
|
||||
============
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in the
|
||||
:ref:`reference/orm/fields` API.
|
||||
|
||||
Fields are used to define what the model can store and where they are stored. Fields are
|
||||
defined as attributes in the model class::
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class TestModel(models.Model):
|
||||
_name = "test.model"
|
||||
_description = "Test Model"
|
||||
|
||||
name = fields.Char()
|
||||
|
||||
The ``name`` field is a :class:`~odoo.fields.Char` which will be represented as a Python
|
||||
unicode ``str`` and a SQL ``VARCHAR``.
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, several basic fields should have been added to the table
|
||||
``estate_property``:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ psql -d rd-demo
|
||||
|
||||
rd-demo=# \d estate_property;
|
||||
Table "public.estate_property"
|
||||
Column | Type | Collation | Nullable | Default
|
||||
--------------------+-----------------------------+-----------+----------+---------------------------------------------
|
||||
id | integer | | not null | nextval('estate_property_id_seq'::regclass)
|
||||
create_uid | integer | | |
|
||||
create_date | timestamp without time zone | | |
|
||||
write_uid | integer | | |
|
||||
write_date | timestamp without time zone | | |
|
||||
name | character varying | | |
|
||||
description | text | | |
|
||||
postcode | character varying | | |
|
||||
date_availability | date | | |
|
||||
expected_price | double precision | | |
|
||||
selling_price | double precision | | |
|
||||
bedrooms | integer | | |
|
||||
living_area | integer | | |
|
||||
facades | integer | | |
|
||||
garage | boolean | | |
|
||||
garden | boolean | | |
|
||||
garden_area | integer | | |
|
||||
garden_orientation | character varying | | |
|
||||
Indexes:
|
||||
"estate_property_pkey" PRIMARY KEY, btree (id)
|
||||
Foreign-key constraints:
|
||||
"estate_property_create_uid_fkey" FOREIGN KEY (create_uid) REFERENCES res_users(id) ON DELETE SET NULL
|
||||
"estate_property_write_uid_fkey" FOREIGN KEY (write_uid) REFERENCES res_users(id) ON DELETE SET NULL
|
||||
|
||||
|
||||
There are two broad categories of fields: 'simple' fields, which are atomic
|
||||
values stored directly in the model's table, and 'relational' fields, which link
|
||||
records (of the same or different models).
|
||||
|
||||
Simple field examples are :class:`~odoo.fields.Boolean`, :class:`~odoo.fields.Float`,
|
||||
:class:`~odoo.fields.Char`, :class:`~odoo.fields.Text`, :class:`~odoo.fields.Date`
|
||||
and :class:`~odoo.fields.Selection`.
|
||||
|
||||
.. exercise:: Add basic fields to the Real Estate Property table.
|
||||
|
||||
Add the following basic fields to the table:
|
||||
|
||||
========================= =========================
|
||||
Field Type
|
||||
========================= =========================
|
||||
name Char
|
||||
description Text
|
||||
postcode Char
|
||||
date_availability Date
|
||||
expected_price Float
|
||||
selling_price Float
|
||||
bedrooms Integer
|
||||
living_area Integer
|
||||
facades Integer
|
||||
garage Boolean
|
||||
garden Boolean
|
||||
garden_area Integer
|
||||
garden_orientation Selection
|
||||
========================= =========================
|
||||
|
||||
The ``garden_orientation`` field must have 4 possible values: 'North', 'South', 'East'
|
||||
and 'West'. The selection list is defined as a list of tuples, see
|
||||
`here <https://github.com/odoo/odoo/blob/b0e0035b585f976e912e97e7f95f66b525bc8e43/addons/crm/report/crm_activity_report.py#L31-L34>`__
|
||||
for an example.
|
||||
|
||||
When the fields are added to the model, restart the server with ``-u estate``
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate
|
||||
|
||||
Connect to ``psql`` and check the structure of the table ``estate_property``. You'll notice that
|
||||
a couple of extra fields were also added to the table. We will revisit them later.
|
||||
|
||||
Common Attributes
|
||||
-----------------
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the columns ``name`` and ``expected_price`` should be
|
||||
not nullable in the table ``estate_property``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
rd-demo=# \d estate_property;
|
||||
Table "public.estate_property"
|
||||
Column | Type | Collation | Nullable | Default
|
||||
--------------------+-----------------------------+-----------+----------+---------------------------------------------
|
||||
...
|
||||
name | character varying | | not null |
|
||||
...
|
||||
expected_price | double precision | | not null |
|
||||
...
|
||||
|
||||
Much like the model itself, fields can be configured by passing
|
||||
configuration attributes as parameters::
|
||||
|
||||
name = field.Char(required=True)
|
||||
|
||||
Some attributes are available on all fields, here are the most common ones:
|
||||
|
||||
:attr:`~odoo.fields.Field.string` (``str``, default: field's name)
|
||||
The label of the field in UI (visible by users).
|
||||
:attr:`~odoo.fields.Field.required` (``bool``, default: ``False``)
|
||||
If ``True``, the field can not be empty. It must either have a default
|
||||
value or always be given a value when creating a record.
|
||||
:attr:`~odoo.fields.Field.help` (``str``, default: ``''``)
|
||||
Provides long-form help tooltip for users in the UI.
|
||||
:attr:`~odoo.fields.Field.index` (``bool``, default: ``False``)
|
||||
Requests that Odoo create a `database index`_ on the column.
|
||||
|
||||
.. exercise:: Set attributes for existing fields.
|
||||
|
||||
Add the following attributes:
|
||||
|
||||
========================= =========================
|
||||
Field Attribute
|
||||
========================= =========================
|
||||
name required
|
||||
expected_price required
|
||||
========================= =========================
|
||||
|
||||
After restarting the server, both fields should be not nullable.
|
||||
|
||||
Automatic Fields
|
||||
----------------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/fields/automatic`.
|
||||
|
||||
You may have noticed your model has a few fields you never defined.
|
||||
Odoo creates a few fields in all models\ [#autofields]_. These fields are
|
||||
managed by the system and can't be written to, but they can be read if
|
||||
useful or necessary:
|
||||
|
||||
:attr:`~odoo.fields.Model.id` (:class:`~odoo.fields.Id`)
|
||||
The unique identifier for a record of the model.
|
||||
:attr:`~odoo.fields.Model.create_date` (:class:`~odoo.fields.Datetime`)
|
||||
Creation date of the record.
|
||||
:attr:`~odoo.fields.Model.create_uid` (:class:`~odoo.fields.Many2one`)
|
||||
User who created the record.
|
||||
:attr:`~odoo.fields.Model.write_date` (:class:`~odoo.fields.Datetime`)
|
||||
Last modification date of the record.
|
||||
:attr:`~odoo.fields.Model.write_uid` (:class:`~odoo.fields.Many2one`)
|
||||
User who last modified the record.
|
||||
|
||||
|
||||
Now that we have created our first model, let's
|
||||
:ref:`add some security <howto/rdtraining/05_securityintro>`!
|
||||
|
||||
|
||||
.. [#autofields] it is possible to :ref:`disable the automatic creation of some
|
||||
fields <reference/fields/automatic/log_access>`
|
||||
.. [#rawsql] writing raw SQL queries is possible, but requires caution as this
|
||||
bypasses all Odoo authentication and security mechanisms.
|
||||
|
||||
.. _database index:
|
||||
https://use-the-index-luke.com/sql/preface
|
||||
.. _ORM:
|
||||
https://en.wikipedia.org/wiki/Object-relational_mapping
|
||||
.. _SQL:
|
||||
https://en.wikipedia.org/wiki/SQL
|
126
content/developer/howtos/rdtraining/05_securityintro.rst
Normal file
@ -0,0 +1,126 @@
|
||||
.. _howto/rdtraining/05_securityintro:
|
||||
|
||||
==========================================
|
||||
Chapter 5: Security - A Brief Introduction
|
||||
==========================================
|
||||
|
||||
In the :ref:`previous chapter <howto/rdtraining/04_basicmodel>`, we created our first table intended
|
||||
to store business data. In a business application such as Odoo, one of the first questions to consider
|
||||
is who\ [#who]_ can access the data. Odoo provides a security mechanism to allow access
|
||||
to the data for specific groups of users.
|
||||
|
||||
The topic of security is covered in more detail in :ref:`howto/rdtraining/B_acl_irrules`. This chapter
|
||||
aims to cover the minimum required for our new module.
|
||||
|
||||
Data Files (CSV)
|
||||
================
|
||||
|
||||
Odoo is a highly data driven system. Although behavior is customized using Python code, part of a
|
||||
module's value is in the data it sets up when loaded. One way to load data is through a CSV
|
||||
file. One example is the
|
||||
`list of country states <https://github.com/odoo/odoo/blob/master/odoo/addons/base/data/res.country.state.csv>`__
|
||||
which is loaded at installation of the ``base`` module.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
"id","country_id:id","name","code"
|
||||
state_us_1,us,"Alabama","AL"
|
||||
state_us_2,us,"Alaska","AK"
|
||||
state_us_3,us,"Arizona","AZ"
|
||||
state_us_4,us,"Arkansas","AR"
|
||||
...
|
||||
|
||||
- ``id`` is an :term:`external identifier`. It can be used to refer to the record
|
||||
(without knowing its in-database identifier).
|
||||
- ``country_id:id`` refers to the country by using its :term:`external identifier`.
|
||||
- ``name`` is the name of the state.
|
||||
- ``code`` is the code of the state.
|
||||
|
||||
These three fields are
|
||||
`defined <https://github.com/odoo/odoo/blob/2ad2f3d6567b6266fc42c6d2999d11f3066b282c/odoo/addons/base/models/res_country.py#L108-L111>`__
|
||||
in the ``res.country.state`` model.
|
||||
|
||||
By convention, a file importing data is located in the ``data`` folder of a module. When the data
|
||||
is related to security, it is located in the ``security`` folder. When the data is related to
|
||||
views and actions (we will cover this later), it is located in the ``views`` folder.
|
||||
Additionally, all of these files must be declared in the ``data``
|
||||
list within the ``__manifest__.py`` file. Our example file is defined
|
||||
`in the manifest of the base module <https://github.com/odoo/odoo/blob/e8697f609372cd61b045c4ee2c7f0fcfb496f58a/odoo/addons/base/__manifest__.py#L29>`__.
|
||||
|
||||
Also note that the content of the data files is only loaded when a module is installed or
|
||||
updated.
|
||||
|
||||
.. warning::
|
||||
|
||||
The data files are sequentially loaded following their order in the ``__manifest__.py`` file.
|
||||
This means that if data ``A`` refers to data ``B``, you must make sure that ``B``
|
||||
is loaded before ``A``.
|
||||
|
||||
In the case of the country states, you will note that the
|
||||
`list of countries <https://github.com/odoo/odoo/blob/e8697f609372cd61b045c4ee2c7f0fcfb496f58a/odoo/addons/base/__manifest__.py#L22>`__
|
||||
is loaded **before** the
|
||||
`list of country states <https://github.com/odoo/odoo/blob/e8697f609372cd61b045c4ee2c7f0fcfb496f58a/odoo/addons/base/__manifest__.py#L29>`__.
|
||||
This is because the states refer to the countries.
|
||||
|
||||
Why is all this important for security? Because all the security configuration of a model is loaded through
|
||||
data files, as we'll see in the next section.
|
||||
|
||||
Access Rights
|
||||
=============
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/security/acl`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the following warning should not appear anymore:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules...
|
||||
|
||||
When no access rights are defined on a model, Odoo determines that no users can access the data.
|
||||
It is even notified in the log:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules, consider adding one. E.g. access_estate_property,access_estate_property,model_estate_property,base.group_user,1,0,0,0
|
||||
|
||||
Access rights are defined as records of the model ``ir.model.access``. Each
|
||||
access right is associated with a model, a group (or no group for global
|
||||
access) and a set of permissions: create, read, write and unlink\ [#unlink]_. Such access
|
||||
rights are usually defined in a CSV file named
|
||||
``ir.model.access.csv``.
|
||||
|
||||
Here is an example for our previous ``test.model``:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_test_model,access_test_model,model_test_model,base.group_user,1,0,0,0
|
||||
|
||||
- ``id`` is an :term:`external identifier`.
|
||||
- ``name`` is the name of the ``ir.model.access``.
|
||||
- ``model_id/id`` refers to the model which the access right applies to. The standard way to refer
|
||||
to the model is ``model_<model_name>``, where ``<model_name>`` is the ``_name`` of the model
|
||||
with the ``.`` replaced by ``_``. Seems cumbersome? Indeed it is...
|
||||
- ``group_id/id`` refers to the group which the access right applies to. We will cover the concept
|
||||
of groups in the :ref:`advanced topic <howto/rdtraining/N_security>` dedicated to the security.
|
||||
- ``perm_read,perm_write,perm_create,perm_unlink``: read, write, create and unlink permissions
|
||||
|
||||
.. exercise:: Add access rights.
|
||||
|
||||
Create the ``ir.model.access.csv`` file in the appropriate folder and define it in the
|
||||
``__manifest__.py`` file.
|
||||
|
||||
Give the read, write, create and unlink permissions to the group ``base.group_user``.
|
||||
|
||||
Tip: the warning message in the log gives you most of the solution ;-)
|
||||
|
||||
Restart the server and the warning message should have disappeared!
|
||||
|
||||
It's now time to finally :ref:`interact with the UI <howto/rdtraining/06_firstui>`!
|
||||
|
||||
.. [#who] meaning which Odoo user (or group of users)
|
||||
|
||||
.. [#unlink] 'unlink' is the equivalent of 'delete'
|
293
content/developer/howtos/rdtraining/06_firstui.rst
Normal file
@ -0,0 +1,293 @@
|
||||
.. _howto/rdtraining/06_firstui:
|
||||
|
||||
========================================
|
||||
Chapter 6: Finally, Some UI To Play With
|
||||
========================================
|
||||
|
||||
Now that we've created our new :ref:`model <howto/rdtraining/04_basicmodel>` and its corresponding
|
||||
:ref:`access rights <howto/rdtraining/05_securityintro>`, it is time to interact with
|
||||
the user interface.
|
||||
|
||||
At the end of this chapter, we will have created a couple of menus in order to access a default list
|
||||
and form view.
|
||||
|
||||
Data Files (XML)
|
||||
================
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/data`.
|
||||
|
||||
In :ref:`howto/rdtraining/05_securityintro`, we added data through a CSV file. The CSV
|
||||
format is convenient when the data to load has a simple format. When the format is more complex
|
||||
(e.g. load the structure of a view or an email template), we use the XML format. For example,
|
||||
this
|
||||
`help field <https://github.com/odoo/odoo/blob/09c59012bf80d2ccbafe21c39e604d6cfda72924/addons/crm/views/crm_lost_reason_views.xml#L61-L69>`__
|
||||
contains HTML tags. While it would be possible to load such data through a CSV file, it is more
|
||||
convenient to use an XML file.
|
||||
|
||||
The XML files must be added to the same folders as the CSV files and defined similarly in the
|
||||
``__manifest__.py``. The content of the data files is also sequentially loaded when a module is installed or
|
||||
updated, therefore all remarks made for CSV files hold true for XML files.
|
||||
When the data is linked to views, we add them to the ``views`` folder.
|
||||
|
||||
In this chapter we will load our first action and menus though an XML file. Actions and menus are
|
||||
standard records in the database.
|
||||
|
||||
.. note::
|
||||
|
||||
When performance is important, the CSV format is preferred over the XML format. This is the case in Odoo
|
||||
where loading a CSV file is faster than loading an XML file.
|
||||
|
||||
In Odoo, the user interface (actions, menus and views) is largely defined by creating
|
||||
and composing records defined in an XML file. A common pattern is Menu > Action > View.
|
||||
To access records the user navigates through several menu levels; the deepest level is an
|
||||
action which triggers the opening of a list of the records.
|
||||
|
||||
Actions
|
||||
=======
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/actions`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, an action should be loaded in the system. We won't see
|
||||
anything yet in the UI, but the file should be loaded in the log:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
INFO rd-demo odoo.modules.loading: loading estate/data/estate_property_views.xml
|
||||
|
||||
Actions can be triggered in three ways:
|
||||
|
||||
1. by clicking on menu items (linked to specific actions)
|
||||
2. by clicking on buttons in views (if these are connected to actions)
|
||||
3. as contextual actions on object
|
||||
|
||||
We will only cover the first case in this chapter. The second case will be covered in a
|
||||
:ref:`later chapter <howto/rdtraining/10_actions>` while the last is the focus of an advanced topic.
|
||||
In our Real Estate example, we would like to link a menu to the ``estate.property`` model, so we
|
||||
are able to create a new record. The action can be viewed as the link between the menu and
|
||||
the model.
|
||||
|
||||
A basic action for our ``test.model`` is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="test_model_action" model="ir.actions.act_window">
|
||||
<field name="name">Test action</field>
|
||||
<field name="res_model">test.model</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
- ``id`` is an :term:`external identifier`. It can be used to refer to the record
|
||||
(without knowing its in-database identifier).
|
||||
- ``model`` has a fixed value of ``ir.actions.act_window`` (:ref:`reference/actions/window`).
|
||||
- ``name`` is the name of the action.
|
||||
- ``res_model`` is the model which the action applies to.
|
||||
- ``view_mode`` are the views that will be available; in this case they are the list (tree) and form views.
|
||||
We'll see :ref:`later <howto/rdtraining/15_qwebintro>` that there can be other view modes.
|
||||
|
||||
Examples can be found everywhere in Odoo, but
|
||||
`this <https://github.com/odoo/odoo/blob/09c59012bf80d2ccbafe21c39e604d6cfda72924/addons/crm/views/crm_lost_reason_views.xml#L57-L70>`__
|
||||
is a good example of a simple action. Pay attention to the structure of the XML data file since you will
|
||||
need it in the following exercise.
|
||||
|
||||
.. exercise:: Add an action.
|
||||
|
||||
Create the ``estate_property_views.xml`` file in the appropriate folder and define it in the
|
||||
``__manifest__.py`` file.
|
||||
|
||||
Create an action for the model ``estate.property``.
|
||||
|
||||
Restart the server and you should see the file loaded in the log.
|
||||
|
||||
Menus
|
||||
=====
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/data/shortcuts`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, three menus should be created and the default view is
|
||||
displayed:
|
||||
|
||||
.. image:: 06_firstui/media/estate_menu_root.png
|
||||
:align: center
|
||||
:alt: Root menus
|
||||
|
||||
.. image:: 06_firstui/media/estate_menu_action.png
|
||||
:align: center
|
||||
:alt: First level and action menus
|
||||
|
||||
.. image:: 06_firstui/media/estate_form_default.png
|
||||
:align: center
|
||||
:alt: Default form view
|
||||
|
||||
To reduce the complexity in declaring a menu (``ir.ui.menu``) and connecting it to the corresponding action,
|
||||
we can use the ``<menuitem>`` shortcut .
|
||||
|
||||
A basic menu for our ``test_model_action`` is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<menuitem id="test_model_menu_action" action="test_model_action"/>
|
||||
|
||||
The menu ``test_model_menu_action`` is linked to the action ``test_model_action``, and the action
|
||||
is linked to the model ``test.model``. As previously mentioned, the action can be seen as the link
|
||||
between the menu and the model.
|
||||
|
||||
However, menus always follow an architecture, and in practice there are three levels of menus:
|
||||
|
||||
1. The root menu, which is displayed in the App switcher (the Odoo Community App switcher is a
|
||||
dropdown menu)
|
||||
2. The first level menu, displayed in the top bar
|
||||
3. The action menus
|
||||
|
||||
.. image:: 06_firstui/media/menu_01.png
|
||||
:align: center
|
||||
:alt: Root menus
|
||||
|
||||
.. image:: 06_firstui/media/menu_02.png
|
||||
:align: center
|
||||
:alt: First level and action menus
|
||||
|
||||
The easiest way to define the structure is to create it in the XML file. A basic
|
||||
structure for our ``test_model_action`` is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<menuitem id="test_menu_root" name="Test">
|
||||
<menuitem id="test_first_level_menu" name="First Level">
|
||||
<menuitem id="test_model_menu_action" action="test_model_action"/>
|
||||
</menuitem>
|
||||
</menuitem>
|
||||
|
||||
The name for the third menu is taken from the name of the ``action``.
|
||||
|
||||
.. exercise:: Add menus.
|
||||
|
||||
Create the ``estate_menus.xml`` file in the appropriate folder and define it in the
|
||||
``__manifest__.py`` file. Remember the sequential loading of the data files ;-)
|
||||
|
||||
Create the three levels of menus for the ``estate.property`` action created in the previous
|
||||
exercise. Refer to the **Goal** of this section for the expected result.
|
||||
|
||||
Restart the server and **refresh the browser**\ [#refresh]_. You should now see the menus,
|
||||
and you'll even be able to create your first real estate property advertisement!
|
||||
|
||||
Fields, Attributes And View
|
||||
===========================
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the selling price should be read-only and the number
|
||||
of bedrooms and the availability date should have default values. Additionally the selling price
|
||||
and availability date values won't be copied when the record is duplicated.
|
||||
|
||||
.. image:: 06_firstui/media/attribute_and_default.gif
|
||||
:align: center
|
||||
:alt: Interaction between model and view
|
||||
|
||||
The reserved fields ``active`` and ``state`` are added to the ``estate.property`` model.
|
||||
|
||||
So far we have only used the generic view for our real estate property advertisements, but
|
||||
in most cases we want to fine tune the view. There are many fine-tunings possible in Odoo, but
|
||||
usually the first step is to make sure that:
|
||||
|
||||
- some fields have a default value
|
||||
- some fields are read-only
|
||||
- some fields are not copied when duplicating the record
|
||||
|
||||
In our real estate business case, we would like the following:
|
||||
|
||||
- The selling price should be read-only (it will be automatically filled in later)
|
||||
- The availability date and the selling price should not be copied when duplicating a record
|
||||
- The default number of bedrooms should be 2
|
||||
- The default availability date should be in 3 months
|
||||
|
||||
Some New Attributes
|
||||
-------------------
|
||||
|
||||
Before moving further with the view design, let's step back to our model definition. We saw that some
|
||||
attributes, such as ``required=True``, impact the table schema in the database. Other attributes
|
||||
will impact the view or provide default values.
|
||||
|
||||
.. exercise:: Add new attributes to the fields.
|
||||
|
||||
Find the appropriate attributes (see :class:`~odoo.fields.Field`) to:
|
||||
|
||||
- set the selling price as read-only
|
||||
- prevent copying of the availability date and the selling price values
|
||||
|
||||
Restart the server and refresh the browser. You should not be able to set any selling prices. When
|
||||
duplicating a record, the availability date should be empty.
|
||||
|
||||
Default Values
|
||||
--------------
|
||||
|
||||
Any field can be given a default value. In the field definition, add the option
|
||||
``default=X`` where ``X`` is either a Python literal value (boolean, integer,
|
||||
float, string) or a function taking a model and returning a value::
|
||||
|
||||
name = fields.Char(default="Unknown")
|
||||
last_seen = fields.Datetime("Last Seen", default=lambda self: fields.Datetime.now())
|
||||
|
||||
The ``name`` field will have the value 'Unknown' by default while the ``last_seen`` field will be
|
||||
set as the current time.
|
||||
|
||||
.. exercise:: Set default values.
|
||||
|
||||
Add the appropriate default attributes so that:
|
||||
|
||||
- the default number of bedrooms is 2
|
||||
- the default availability date is in 3 months
|
||||
|
||||
Tip: this might help you: :meth:`~odoo.fields.Date.today`
|
||||
|
||||
Check that the default values are set as expected.
|
||||
|
||||
Reserved Fields
|
||||
---------------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/orm/fields/reserved`.
|
||||
|
||||
A few field names are reserved for pre-defined behaviors. They should be defined on a
|
||||
model when the related behavior is desired.
|
||||
|
||||
.. exercise:: Add active field.
|
||||
|
||||
Add the ``active`` field to the ``estate.property`` model.
|
||||
|
||||
Restart the server, create a new property, then come back to the list view... The property will
|
||||
not be listed! ``active`` is an example of a reserved field with a specific behavior: when
|
||||
a record has ``active=False``, it is automatically removed from any search. To display the
|
||||
created property, you will need to specifically search for inactive records.
|
||||
|
||||
.. image:: 06_firstui/media/inactive.gif
|
||||
:align: center
|
||||
:alt: Inactive records
|
||||
|
||||
.. exercise:: Set a default value for active field.
|
||||
|
||||
Set the appropriate default value for the ``active`` field so it doesn't disappear anymore.
|
||||
|
||||
Note that the default ``active=False`` value was assigned to all existing records.
|
||||
|
||||
.. exercise:: Add state field.
|
||||
|
||||
Add a ``state`` field to the ``estate.property`` model. Five values are possible: New,
|
||||
Offer Received, Offer Accepted, Sold and Canceled. It must be required, should not be copied
|
||||
and should have its default value set to 'New'.
|
||||
|
||||
Make sure to use the correct type!
|
||||
|
||||
The ``state`` will be used later on for several UI enhancements.
|
||||
|
||||
Now that we are able to interact with the UI thanks to the default views, the next step is
|
||||
obvious: we want to define :ref:`our own views <howto/rdtraining/07_basicviews>`.
|
||||
|
||||
.. [#refresh] A refresh is needed since the web client keeps a cache of the various menus
|
||||
and views for performance reasons.
|
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 |
BIN
content/developer/howtos/rdtraining/06_firstui/media/menu_01.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
content/developer/howtos/rdtraining/06_firstui/media/menu_02.png
Normal file
After Width: | Height: | Size: 29 KiB |
231
content/developer/howtos/rdtraining/07_basicviews.rst
Normal file
@ -0,0 +1,231 @@
|
||||
.. _howto/rdtraining/07_basicviews:
|
||||
|
||||
======================
|
||||
Chapter 7: Basic Views
|
||||
======================
|
||||
|
||||
We have seen in the :ref:`previous chapter <howto/rdtraining/06_firstui>` that Odoo is able to
|
||||
generate default views for a given model. In practice, the default view is **never** acceptable
|
||||
for a business application. Instead, we should at least organize the various fields in a logical
|
||||
manner.
|
||||
|
||||
Views are defined in XML files with actions and menus. They are instances of the
|
||||
``ir.ui.view`` model.
|
||||
|
||||
In our real estate module, we need to organize the fields in a logical way:
|
||||
|
||||
- in the list (tree) view, we want to display more than just the name.
|
||||
- in the form view, the fields should be grouped.
|
||||
- in the search view, we must be able to search on more than just the name. Specifically, we want a
|
||||
filter for the 'Available' properties and a shortcut to group by postcode.
|
||||
|
||||
List
|
||||
====
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/views/list`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the list view should look like this:
|
||||
|
||||
.. image:: 07_basicviews/media/list.png
|
||||
:align: center
|
||||
:alt: List view
|
||||
|
||||
List views, also called tree views, display records in a tabular form.
|
||||
|
||||
Their root element is ``<tree>``. The most basic version of this view simply
|
||||
lists all the fields to display in the table (where each field is a column):
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<tree string="Tests">
|
||||
<field name="name"/>
|
||||
<field name="last_seen"/>
|
||||
</tree>
|
||||
|
||||
A simple example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/6da14a3aadeb3efc40f145f6c11fc33314b2f15e/addons/crm/views/crm_lost_reason_views.xml#L46-L54>`__.
|
||||
|
||||
.. exercise:: Add a custom list view.
|
||||
|
||||
Define a list view for the ``estate.property`` model in the appropriate XML file. Check the
|
||||
**Goal** of this section for the fields to display.
|
||||
|
||||
Tips:
|
||||
|
||||
- do not add the ``editable="bottom"`` attribute that you can find in the example above. We'll
|
||||
come back to it later.
|
||||
- some field labels may need to be adapted to match the reference.
|
||||
|
||||
|
||||
As always, you need to restart the server (do not forget the ``-u`` option) and refresh the browser
|
||||
to see the result.
|
||||
|
||||
.. warning::
|
||||
|
||||
You will probably use some copy-paste in this chapter, therefore always make sure that the ``id``
|
||||
remains unique for each view!
|
||||
|
||||
Form
|
||||
====
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/views/form`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the form view should look like this:
|
||||
|
||||
.. image:: 07_basicviews/media/form.png
|
||||
:align: center
|
||||
:alt: Form view
|
||||
|
||||
Forms are used to create and edit single records.
|
||||
|
||||
Their root element is ``<form>``. They are composed of high-level structure
|
||||
elements (groups and notebooks) and interactive elements (buttons and fields):
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<form string="Test">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="last_seen"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Description">
|
||||
<field name="description"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
It is possible to use regular HTML tags such as ``div`` and ``h1`` as well as the the ``class`` attribute
|
||||
(Odoo provides some built-in classes) to fine-tune the look.
|
||||
|
||||
A simple example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/6da14a3aadeb3efc40f145f6c11fc33314b2f15e/addons/crm/views/crm_lost_reason_views.xml#L16-L44>`__.
|
||||
|
||||
.. exercise:: Add a custom form view.
|
||||
|
||||
Define a form view for the ``estate.property`` model in the appropriate XML file. Check the
|
||||
**Goal** of this section for the expected final design of the page.
|
||||
|
||||
This might require some trial and error before you get to the expected result ;-) It is advised
|
||||
that you add the fields and the tags one at a time to help understand how it works.
|
||||
|
||||
In order to avoid relaunching the server every time you do a modification to the view, it can
|
||||
be convenient to use the ``--dev xml`` parameter when launching the server:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate --dev xml
|
||||
|
||||
This parameter allows you to just refresh the page to view your view modifications.
|
||||
|
||||
Search
|
||||
======
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/views/search`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the search view should look like this:
|
||||
|
||||
.. image:: 07_basicviews/media/search_01.png
|
||||
:align: center
|
||||
:alt: Search fields
|
||||
|
||||
.. image:: 07_basicviews/media/search_02.png
|
||||
:align: center
|
||||
:alt: Filter
|
||||
|
||||
.. image:: 07_basicviews/media/search_03.png
|
||||
:align: center
|
||||
:alt: Group By
|
||||
|
||||
Search views are slightly different from the list and form views since they don't display
|
||||
*content*. Although they apply to a specific model, they are used to filter
|
||||
other views' content (generally aggregated views such as :ref:`reference/views/list`).
|
||||
Beyond the difference in use case, they are defined the same way.
|
||||
|
||||
Their root element is ``<search>``. The most basic version of this view simply
|
||||
lists all the fields for which a shortcut is desired:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<search string="Tests">
|
||||
<field name="name"/>
|
||||
<field name="last_seen"/>
|
||||
</search>
|
||||
|
||||
The default search view generated by Odoo provides a shortcut to filter by ``name``. It is very
|
||||
common to add the fields which the user is likely to filter on in a customized search view.
|
||||
|
||||
.. exercise:: Add a custom search view.
|
||||
|
||||
Define a search view for the ``estate.property`` model in the appropriate XML file. Check the
|
||||
first image of this section's **Goal** for the list of fields.
|
||||
|
||||
After restarting the server, it should be possible to filter on the given fields.
|
||||
|
||||
Search views can also contain ``<filter>`` elements, which act as toggles for
|
||||
predefined searches. Filters must have one of the following attributes:
|
||||
|
||||
- ``domain``: adds the given domain to the current search
|
||||
- ``context``: adds some context to the current search; uses the key ``group_by`` to group
|
||||
results on the given field name
|
||||
|
||||
A simple example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/715a24333bf000d5d98b9ede5155d3af32de067c/addons/delivery/views/delivery_view.xml#L30-L44>`__.
|
||||
|
||||
Before going further in the exercise, it is necessary to introduce the 'domain' concept.
|
||||
|
||||
Domains
|
||||
-------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/orm/domains`.
|
||||
|
||||
In Odoo, a domain encodes conditions on
|
||||
records: a domain is a list of criteria used to select a subset of a model's
|
||||
records. Each criterion is a triplet with a *field name*, an *operator* and a *value*.
|
||||
A record satisfies a criterion if the specified field meets the condition of the operator applied to the value.
|
||||
|
||||
For instance, when used on the *Product* model the following domain selects
|
||||
all *services* with a unit price greater than *1000*::
|
||||
|
||||
[('product_type', '=', 'service'), ('unit_price', '>', 1000)]
|
||||
|
||||
By default criteria are combined with an implicit AND, meaning *every* criterion
|
||||
needs to be satisfied for a record to match a domain. The logical operators
|
||||
``&`` (AND), ``|`` (OR) and ``!`` (NOT) can be used to explicitly combine
|
||||
criteria. They are used in prefix position (the operator is inserted before
|
||||
its arguments rather than between). For instance, to select products 'which are
|
||||
services *OR* have a unit price which is *NOT* between 1000 and 2000'::
|
||||
|
||||
['|',
|
||||
('product_type', '=', 'service'),
|
||||
'!', '&',
|
||||
('unit_price', '>=', 1000),
|
||||
('unit_price', '<', 2000)]
|
||||
|
||||
.. exercise:: Add filter and Group By.
|
||||
|
||||
The following should be added to the previously created search view:
|
||||
|
||||
- a filter which displays available properties, i.e. the state should be 'New' or
|
||||
'Offer Received'.
|
||||
- the ability to group results by postcode.
|
||||
|
||||
Looking good? At this point we are already able to create models and design a user interface which
|
||||
makes sense business-wise. However, a key component is still missing: the
|
||||
:ref:`link between models <howto/rdtraining/08_relations>`.
|
BIN
content/developer/howtos/rdtraining/07_basicviews/media/form.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
content/developer/howtos/rdtraining/07_basicviews/media/list.png
Normal file
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 |
266
content/developer/howtos/rdtraining/08_relations.rst
Normal file
@ -0,0 +1,266 @@
|
||||
.. _howto/rdtraining/08_relations:
|
||||
|
||||
===================================
|
||||
Chapter 8: Relations Between Models
|
||||
===================================
|
||||
|
||||
The :ref:`previous chapter <howto/rdtraining/07_basicviews>` covered the creation of custom views
|
||||
for a model containing basic fields. However, in any real business scenario we need more than
|
||||
one model. Moreover, links between models are necessary. One can easily imagine one model containing
|
||||
the customers and another one containing the list of users. You might need to refer to a customer
|
||||
or a user on any existing business model.
|
||||
|
||||
In our real estate module, we want the following information for a property:
|
||||
|
||||
- the customer who bought the property
|
||||
- the real restate agent who sold the property
|
||||
- the property type: house, apartment, penthouse, castle...
|
||||
- a list of tags characterizing the property: cozy, renovated...
|
||||
- a list of the offers received
|
||||
|
||||
Many2one
|
||||
========
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:class:`~odoo.fields.Many2one`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section:
|
||||
|
||||
- a new ``estate.property.type`` model should be created with the corresponding menu, action and views.
|
||||
|
||||
.. image:: 08_relations/media/property_type.png
|
||||
:align: center
|
||||
:alt: Property type
|
||||
|
||||
- three Many2one fields should be added to the ``estate.property`` model: property type, buyer and seller.
|
||||
|
||||
.. image:: 08_relations/media/property_many2one.png
|
||||
:align: center
|
||||
:alt: Property
|
||||
|
||||
In our real estate module, we want to define the concept of property type. A property type
|
||||
is, for example, a house or an apartment. It is a standard business need to categorize
|
||||
properties according to their type, especially to refine filtering.
|
||||
|
||||
A property can have **one** type, but the same type can be assigned to **many** properties.
|
||||
This is supported by the **many2one** concept.
|
||||
|
||||
A many2one is a simple link to another object. For example, in order to define a link to the
|
||||
``res.partner`` in our test model, we can write::
|
||||
|
||||
partner_id = fields.Many2one("res.partner", string="Partner")
|
||||
|
||||
By convention, many2one fields have the ``_id`` suffix. Accessing the data in the partner
|
||||
can then be easily done with::
|
||||
|
||||
print(my_test_object.partner_id.name)
|
||||
|
||||
.. seealso::
|
||||
|
||||
`foreign keys <https://www.postgresql.org/docs/current/tutorial-fk.html>`_
|
||||
|
||||
In practice a many2one can be seen as a dropdown list in a form view.
|
||||
|
||||
.. exercise:: Add the Real Estate Property Type table.
|
||||
|
||||
- Create the ``estate.property.type`` model and add the following field:
|
||||
|
||||
========================= ========================= =========================
|
||||
Field Type Attributes
|
||||
========================= ========================= =========================
|
||||
name Char required
|
||||
========================= ========================= =========================
|
||||
|
||||
- Add the menus as displayed in this section's **Goal**
|
||||
- Add the field ``property_type_id`` into your ``estate.property`` model and its form, tree
|
||||
and search views
|
||||
|
||||
This exercise is a good recap of the previous chapters: you need to create a
|
||||
:ref:`model <howto/rdtraining/04_basicmodel>`, set the
|
||||
:ref:`model <howto/rdtraining/05_securityintro>`, add an
|
||||
:ref:`action and a menu <howto/rdtraining/06_firstui>`, and
|
||||
:ref:`create a view <howto/rdtraining/07_basicviews>`.
|
||||
|
||||
Tip: do not forget to import any new Python files in ``__init__.py``, add new data files in
|
||||
``__manifest.py__`` or add the access rights ;-)
|
||||
|
||||
Once again, restart the server and refresh to see the results!
|
||||
|
||||
In the real estate module, there are still two missing pieces of information we want on a property:
|
||||
the buyer and the salesperson. The buyer can be any individual, but on the other hand the
|
||||
salesperson must be an employee of the real estate agency (i.e. an Odoo user).
|
||||
|
||||
In Odoo, there are two models which we commonly refer to:
|
||||
|
||||
- ``res.partner``: a partner is a physical or legal entity. It can be a company, an individual or
|
||||
even a contact address.
|
||||
- ``res.users``: the users of the system. Users can be 'internal', i.e. they have
|
||||
access to the Odoo backend. Or they can be 'portal', i.e. they cannot access the backend, only the
|
||||
frontend (e.g. to access their previous orders in eCommerce).
|
||||
|
||||
.. exercise:: Add the buyer and the salesperson.
|
||||
|
||||
Add a buyer and a salesperson to the ``estate.property`` model using the two common models
|
||||
mentioned above. They should be added in a new tab of the form view, as depicted in this section's **Goal**.
|
||||
|
||||
The default value for the salesperson must be the current user. The buyer should not be copied.
|
||||
|
||||
Tip: to get the default value, check the note below or look at an example
|
||||
`here <https://github.com/odoo/odoo/blob/5bb8b927524d062be32f92eb326ef64091301de1/addons/crm/models/crm_lead.py#L92>`__.
|
||||
|
||||
.. note::
|
||||
|
||||
The object ``self.env`` gives access to request parameters and other useful
|
||||
things:
|
||||
|
||||
- ``self.env.cr`` or ``self._cr`` is the database *cursor* object; it is
|
||||
used for querying the database
|
||||
- ``self.env.uid`` or ``self._uid`` is the current user's database id
|
||||
- ``self.env.user`` is the current user's record
|
||||
- ``self.env.context`` or ``self._context`` is the context dictionary
|
||||
- ``self.env.ref(xml_id)`` returns the record corresponding to an XML id
|
||||
- ``self.env[model_name]`` returns an instance of the given model
|
||||
|
||||
Now let's have a look at other types of links.
|
||||
|
||||
Many2many
|
||||
=========
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:class:`~odoo.fields.Many2many`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section:
|
||||
|
||||
- a new ``estate.property.tag`` model should be created with the corresponding menu and action.
|
||||
|
||||
.. image:: 08_relations/media/property_tag.png
|
||||
:align: center
|
||||
:alt: Property tag
|
||||
|
||||
- tags should be added to the ``estate.property`` model:
|
||||
|
||||
.. image:: 08_relations/media/property_many2many.png
|
||||
:align: center
|
||||
:alt: Property
|
||||
|
||||
In our real estate module, we want to define the concept of property tags. A property tag
|
||||
is, for example, a property which is 'cozy' or 'renovated'.
|
||||
|
||||
A property can have **many** tags and a tag can be assigned to **many** properties.
|
||||
This is supported by the **many2many** concept.
|
||||
|
||||
A many2many is a bidirectional multiple relationship: any record on one side can be related to any
|
||||
number of records on the other side. For example, in order to define a link to the
|
||||
``account.tax`` model on our test model, we can write::
|
||||
|
||||
tax_ids = fields.Many2many("account.tax", string="Taxes")
|
||||
|
||||
By convention, many2many fields have the ``_ids`` suffix. This means that several taxes can be
|
||||
added to our test model. It behaves as a list of records, meaning that accessing the data must be
|
||||
done in a loop::
|
||||
|
||||
for tax in my_test_object.tax_ids:
|
||||
print(tax.name)
|
||||
|
||||
A list of records is known as a *recordset*, i.e. an ordered collection of records. It supports
|
||||
standard Python operations on collections, such as ``len()`` and ``iter()``, plus extra set
|
||||
operations like ``recs1 | recs2``.
|
||||
|
||||
.. exercise:: Add the Real Estate Property Tag table.
|
||||
|
||||
- Create the ``estate.property.tag`` model and add the following field:
|
||||
|
||||
========================= ========================= =========================
|
||||
Field Type Attributes
|
||||
========================= ========================= =========================
|
||||
name Char required
|
||||
========================= ========================= =========================
|
||||
|
||||
- Add the menus as displayed in this section's **Goal**
|
||||
- Add the field ``tag_ids`` to your ``estate.property`` model and in its form and tree views
|
||||
|
||||
Tip: in the view, use the ``widget="many2many_tags"`` attribute as demonstrated
|
||||
`here <https://github.com/odoo/odoo/blob/5bb8b927524d062be32f92eb326ef64091301de1/addons/crm_iap_lead_website/views/crm_reveal_views.xml#L36>`__.
|
||||
The ``widget`` attribute will be explained in detail in :ref:`a later chapter of the training <howto/rdtraining/12_sprinkles>`.
|
||||
For now, you can try to adding and removing it and see the result ;-)
|
||||
|
||||
One2many
|
||||
========
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:class:`~odoo.fields.One2many`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section:
|
||||
|
||||
- a new ``estate.property.offer`` model should be created with the corresponding form and tree view.
|
||||
- offers should be added to the ``estate.property`` model:
|
||||
|
||||
.. image:: 08_relations/media/property_offer.png
|
||||
:align: center
|
||||
:alt: Property offers
|
||||
|
||||
In our real estate module, we want to define the concept of property offers. A property offer
|
||||
is an amount a potential buyer offers to the seller. The offer can be lower or higher than the
|
||||
expected price.
|
||||
|
||||
An offer applies to **one** property, but the same property can have **many** offers.
|
||||
The concept of **many2one** appears once again. However, in this case we want to display the list
|
||||
of offers for a given property so we will use the **one2many** concept.
|
||||
|
||||
A one2many is the inverse of a many2one. For example, we defined
|
||||
on our test model a link to the ``res.partner`` model thanks to the field ``partner_id``.
|
||||
We can define the inverse relation, i.e. the list of test models linked to our partner::
|
||||
|
||||
test_ids = fields.One2many("test.model", "partner_id", string="Tests")
|
||||
|
||||
The first parameter is called the ``comodel`` and the second parameter is the field we want to
|
||||
inverse.
|
||||
|
||||
By convention, one2many fields have the ``_ids`` suffix. They behave as a list of records, meaning
|
||||
that accessing the data must be done in a loop::
|
||||
|
||||
for test in partner.test_ids:
|
||||
print(test.name)
|
||||
|
||||
.. danger::
|
||||
|
||||
Because a :class:`~odoo.fields.One2many` is a virtual relationship,
|
||||
there *must* be a :class:`~odoo.fields.Many2one` field defined in the comodel.
|
||||
|
||||
.. exercise:: Add the Real Estate Property Offer table.
|
||||
|
||||
- Create the ``estate.property.offer`` model and add the following fields:
|
||||
|
||||
========================= ================================ ============= =================
|
||||
Field Type Attributes Values
|
||||
========================= ================================ ============= =================
|
||||
price Float
|
||||
status Selection no copy Accepted, Refused
|
||||
partner_id Many2one (``res.partner``) required
|
||||
property_id Many2one (``estate.property``) required
|
||||
========================= ================================ ============= =================
|
||||
|
||||
- Create a tree view and a form view with the ``price``, ``partner_id`` and ``status`` fields. No
|
||||
need to create an action or a menu.
|
||||
- Add the field ``offer_ids`` to your ``estate.property`` model and in its form view as
|
||||
depicted in this section's **Goal**.
|
||||
|
||||
There are several important things to notice here. First, we don't need an action or a menu for all
|
||||
models. Some models are intended to be accessed only through another model. This is the case in our
|
||||
exercise: an offer is always accessed through a property.
|
||||
|
||||
Second, despite the fact that the ``property_id`` field is required, we did not include it in the
|
||||
views. How does Odoo know which property our offer is linked to? Well that's part of the
|
||||
magic of using the Odoo framework: sometimes things are defined implicitly. When we create
|
||||
a record through a one2many field, the corresponding many2one is populated automatically
|
||||
for convenience.
|
||||
|
||||
Still alive? This chapter is definitely not the easiest one. It introduced a couple of new concepts
|
||||
while relying on everything that was introduced before. The
|
||||
:ref:`next chapter <howto/rdtraining/09_compute_onchange>` will be lighter, don't worry ;-)
|
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 |
305
content/developer/howtos/rdtraining/09_compute_onchange.rst
Normal file
@ -0,0 +1,305 @@
|
||||
.. _howto/rdtraining/09_compute_onchange:
|
||||
|
||||
========================================
|
||||
Chapter 9: Computed Fields And Onchanges
|
||||
========================================
|
||||
|
||||
The :ref:`relations between models <howto/rdtraining/08_relations>` are a key component of any Odoo
|
||||
module. They are necessary for the modelization of any business case.
|
||||
However, we may want links between the fields within a given model. Sometimes the
|
||||
value of one field is determined from the values of other fields and other times we want to help the
|
||||
user with data entry.
|
||||
|
||||
These cases are supported by the concepts of computed fields and onchanges. Although this chapter is
|
||||
not technically complex, the semantics of both concepts is very important.
|
||||
This is also the first time we will write Python logic. Until now we haven't written anything
|
||||
other than class definitions and field declarations.
|
||||
|
||||
Computed Fields
|
||||
===============
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/fields/compute`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section:
|
||||
|
||||
- In the property model, the total area and the best offer should be computed:
|
||||
|
||||
.. image:: 09_compute_onchange/media/compute.gif
|
||||
:align: center
|
||||
:alt: Compute fields
|
||||
|
||||
- In the property offer model, the validity date should be computed and can be updated:
|
||||
|
||||
.. image:: 09_compute_onchange/media/compute_inverse.gif
|
||||
:align: center
|
||||
:alt: Compute field with inverse
|
||||
|
||||
In our real estate module, we have defined the living area as well as the garden area. It is then
|
||||
natural to define the total area as the sum of both fields. We will use the concept of a computed
|
||||
field for this, i.e. the value of a given field will be computed from the value of other fields.
|
||||
|
||||
So far fields have been stored directly in and retrieved directly from the
|
||||
database. Fields can also be *computed*. In this case, the field's value is not
|
||||
retrieved from the database but computed on-the-fly by calling a method of the
|
||||
model.
|
||||
|
||||
To create a computed field, create a field and set its attribute
|
||||
:attr:`~odoo.fields.Field.compute` to the name of a method. The computation
|
||||
method should set the value of the computed field for every record in
|
||||
``self``.
|
||||
|
||||
By convention, :attr:`~odoo.fields.Field.compute` methods are private, meaning that they cannot
|
||||
be called from the presentation tier, only from the business tier (see
|
||||
:ref:`howto/rdtraining/01_architecture`). Private methods have a name starting with an underscore ``_``.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
The value of a computed field usually depends on the values of other fields in
|
||||
the computed record. The ORM expects the developer to specify those dependencies
|
||||
on the compute method with the decorator :func:`~odoo.api.depends`.
|
||||
The given dependencies are used by the ORM to trigger the recomputation of the
|
||||
field whenever some of its dependencies have been modified::
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class TestComputed(models.Model):
|
||||
_name = "test.computed"
|
||||
|
||||
total = fields.Float(compute="_compute_total")
|
||||
amount = fields.Float()
|
||||
|
||||
@api.depends("amount")
|
||||
def _compute_total(self):
|
||||
for record in self:
|
||||
record.total = 2.0 * record.amount
|
||||
|
||||
.. note:: ``self`` is a collection.
|
||||
:class: aphorism
|
||||
|
||||
The object ``self`` is a *recordset*, i.e. an ordered collection of
|
||||
records. It supports the standard Python operations on collections, e.g.
|
||||
``len(self)`` and ``iter(self)``, plus extra set operations such as ``recs1 |
|
||||
recs2``.
|
||||
|
||||
Iterating over ``self`` gives the records one by one, where each record is
|
||||
itself a collection of size 1. You can access/assign fields on single
|
||||
records by using the dot notation, e.g. ``record.name``.
|
||||
|
||||
Many examples of computed fields can be found in Odoo.
|
||||
`Here <https://github.com/odoo/odoo/blob/713dd3777ca0ce9d121d5162a3d63de3237509f4/addons/account/models/account_move.py#L3420-L3423>`__
|
||||
is a simple one.
|
||||
|
||||
.. exercise:: Compute the total area.
|
||||
|
||||
- Add the ``total_area`` field to ``estate.property``. It is defined as the sum of the
|
||||
``living_area`` and the ``garden_area``.
|
||||
|
||||
- Add the field in the form view as depicted on the first image of this section's **Goal**.
|
||||
|
||||
For relational fields it's possible to use paths through a field as a dependency::
|
||||
|
||||
description = fields.Char(compute="_compute_description")
|
||||
partner_id = fields.Many2one("res.partner")
|
||||
|
||||
@api.depends("partner_id.name")
|
||||
def _compute_description(self):
|
||||
for record in self:
|
||||
record.description = "Test for partner %s" % record.partner_id.name
|
||||
|
||||
The example is given with a :class:`~odoo.fields.Many2one`, but it is valid for
|
||||
:class:`~odoo.fields.Many2many` or a :class:`~odoo.fields.One2many`. An example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/713dd3777ca0ce9d121d5162a3d63de3237509f4/addons/account/models/account_reconcile_model.py#L248-L251>`__.
|
||||
|
||||
Let's try it in our module with the following exercise!
|
||||
|
||||
.. exercise:: Compute the best offer.
|
||||
|
||||
- Add the ``best_price`` field to ``estate.property``. It is defined as the highest (i.e. maximum) of the
|
||||
offers' ``price``.
|
||||
|
||||
- Add the field to the form view as depicted in the first image of this section's **Goal**.
|
||||
|
||||
Tip: you might want to try using the :meth:`~odoo.models.BaseModel.mapped` method. See
|
||||
`here <https://github.com/odoo/odoo/blob/f011c9aacf3a3010c436d4e4f408cd9ae265de1b/addons/account/models/account_payment.py#L686>`__
|
||||
for a simple example.
|
||||
|
||||
Inverse Function
|
||||
----------------
|
||||
|
||||
You might have noticed that computed fields are read-only by default. This is expected since the
|
||||
user is not supposed to set a value.
|
||||
|
||||
In some cases, it might be useful to still be able to set a value directly. In our real estate example,
|
||||
we can define a validity duration for an offer and set a validity date. We would like to be able
|
||||
to set either the duration or the date with one impacting the other.
|
||||
|
||||
To support this Odoo provides the ability to use an ``inverse`` function::
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class TestComputed(models.Model):
|
||||
_name = "test.computed"
|
||||
|
||||
total = fields.Float(compute="_compute_total", inverse="_inverse_total")
|
||||
amount = fields.Float()
|
||||
|
||||
@api.depends("amount")
|
||||
def _compute_total(self):
|
||||
for record in self:
|
||||
record.total = 2.0 * record.amount
|
||||
|
||||
def _inverse_total(self):
|
||||
for record in self:
|
||||
record.amount = record.total / 2.0
|
||||
|
||||
An example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/2ccf0bd0dcb2e232ee894f07f24fdc26c51835f7/addons/crm/models/crm_lead.py#L308-L317>`__.
|
||||
|
||||
A compute method sets the field while an inverse method sets the field's
|
||||
dependencies.
|
||||
|
||||
Note that the ``inverse`` method is called when saving the record, while the
|
||||
``compute`` method is called at each change of its dependencies.
|
||||
|
||||
.. exercise:: Compute a validity date for offers.
|
||||
|
||||
- Add the following fields to the ``estate.property.offer`` model:
|
||||
|
||||
========================= ========================= =========================
|
||||
Field Type Default
|
||||
========================= ========================= =========================
|
||||
validity Integer 7
|
||||
date_deadline Date
|
||||
========================= ========================= =========================
|
||||
|
||||
Where ``date_deadline`` is a computed field which is defined as the sum of two fields from
|
||||
the offer: the ``create_date`` and the ``validity``. Define an appropriate inverse function
|
||||
so that the user can set either the date or the validity.
|
||||
|
||||
Tip: the ``create_date`` is only filled in when the record is created, therefore you will
|
||||
need a fallback to prevent crashing at time of creation.
|
||||
|
||||
- Add the fields in the form view and the list view as depicted on the second image of this section's **Goal**.
|
||||
|
||||
Additional Information
|
||||
----------------------
|
||||
|
||||
Computed fields are **not stored** in the database by default. Therefore it is **not
|
||||
possible** to search on a computed field unless a ``search`` method is defined. This topic is beyond the scope
|
||||
of this training, so we won't cover it. An example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/f011c9aacf3a3010c436d4e4f408cd9ae265de1b/addons/event/models/event_event.py#L188>`__.
|
||||
|
||||
Another solution is to store the field with the ``store=True`` attribute. While this is
|
||||
usually convenient, pay attention to the potential computation load added to your model. Lets re-use
|
||||
our example::
|
||||
|
||||
description = fields.Char(compute="_compute_description", store=True)
|
||||
partner_id = fields.Many2one("res.partner")
|
||||
|
||||
@api.depends("partner_id.name")
|
||||
def _compute_description(self):
|
||||
for record in self:
|
||||
record.description = "Test for partner %s" % record.partner_id.name
|
||||
|
||||
Every time the partner ``name`` is changed, the ``description`` is automatically recomputed for
|
||||
**all the records** referring to it! This can quickly become prohibitive to recompute when
|
||||
millions of records need recomputation.
|
||||
|
||||
It is also worth noting that a computed field can depend on another computed field. The ORM is
|
||||
smart enough to correctly recompute all the dependencies in the right order... but sometimes at the
|
||||
cost of degraded performance.
|
||||
|
||||
In general performance must always be kept in mind when defining computed fields. The more
|
||||
complex is your field to compute (e.g. with a lot of dependencies or when a computed field
|
||||
depends on other computed fields), the more time it will take to compute. Always take some time to
|
||||
evaluate the cost of a computed field beforehand. Most of the time it is only when your code
|
||||
reaches a production server that you realize it slows down a whole process. Not cool :-(
|
||||
|
||||
Onchanges
|
||||
=========
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:func:`~odoo.api.onchange`:
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, enabling the garden will set a default area of 10 and
|
||||
an orientation to North.
|
||||
|
||||
.. image:: 09_compute_onchange/media/onchange.gif
|
||||
:align: center
|
||||
:alt: Onchange
|
||||
|
||||
In our real estate module, we also want to help the user with data entry. When the 'garden'
|
||||
field is set, we want to give a default value for the garden area as well as the orientation.
|
||||
Additionally, when the 'garden' field is unset we want the garden area to reset to zero and the
|
||||
orientation to be removed. In this case, the value of a given field modifies the value of
|
||||
other fields.
|
||||
|
||||
The 'onchange' mechanism provides a way for the client interface to update a
|
||||
form without saving anything to the database whenever the user has filled in
|
||||
a field value. To achieve this, we define a method where ``self`` represents
|
||||
the record in the form view and decorate it with :func:`~odoo.api.onchange`
|
||||
to specify which field it is triggered by. Any change you make on
|
||||
``self`` will be reflected on the form::
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class TestOnchange(models.Model):
|
||||
_name = "test.onchange"
|
||||
|
||||
name = fields.Char(string="Name")
|
||||
description = fields.Char(string="Description")
|
||||
partner_id = fields.Many2one("res.partner", string="Partner")
|
||||
|
||||
@api.onchange("partner_id")
|
||||
def _onchange_partner_id(self):
|
||||
self.name = "Document for %s" % (self.partner_id.name)
|
||||
self.description = "Default description for %s" % (self.partner_id.name)
|
||||
|
||||
In this example, changing the partner will also change the name and the description values. It is up to
|
||||
the user whether or not to change the name and description values afterwards. Also note that we do not
|
||||
loop on ``self``, this is because the method is only triggered in a form view, where ``self`` is always
|
||||
a single record.
|
||||
|
||||
.. exercise:: Set values for garden area and orientation.
|
||||
|
||||
Create an ``onchange`` in the ``estate.property`` model in order to set values for the
|
||||
garden area (10) and orientation (North) when garden is set to True. When unset, clear the fields.
|
||||
|
||||
Additional Information
|
||||
----------------------
|
||||
|
||||
Onchanges methods can also return a non-blocking warning message
|
||||
(`example <https://github.com/odoo/odoo/blob/cd9af815ba591935cda367d33a1d090f248dd18d/addons/payment_authorize/models/payment.py#L34-L36>`__).
|
||||
|
||||
How to use them?
|
||||
================
|
||||
|
||||
There is no strict rule for the use of computed fields and onchanges.
|
||||
|
||||
In many cases, both computed fields and onchanges may be used to achieve the same result. Always
|
||||
prefer computed fields since they are also triggered outside of the context of a form view. Never
|
||||
ever use an onchange to add business logic to your model. This is a **very bad** idea since
|
||||
onchanges are not automatically triggered when creating a record programmatically; they are only
|
||||
triggered in the form view.
|
||||
|
||||
The usual pitfall of computed fields and onchanges is trying to be 'too smart' by adding too much
|
||||
logic. This can have the opposite result of what was expected: the end user is confused from
|
||||
all the automation.
|
||||
|
||||
Computed fields tend to be easier to debug: such a field is set by a given method, so it's easy to
|
||||
track when the value is set. Onchanges, on the other hand, may be confusing: it is very difficult to
|
||||
know the extent of an onchange. Since several onchange methods may set the same fields, it
|
||||
easily becomes difficult to track where a value is coming from.
|
||||
|
||||
When using stored computed fields, pay close attention to the dependencies. When computed fields
|
||||
depend on other computed fields, changing a value can trigger a large number of recomputations.
|
||||
This leads to poor performance.
|
||||
|
||||
In the :ref:`next chapter<howto/rdtraining/10_actions>`, we'll see how we can trigger some business
|
||||
logic when buttons are clicked.
|
After Width: | Height: | Size: 254 KiB |
After Width: | Height: | Size: 229 KiB |
After Width: | Height: | Size: 46 KiB |
139
content/developer/howtos/rdtraining/10_actions.rst
Normal file
@ -0,0 +1,139 @@
|
||||
.. _howto/rdtraining/10_actions:
|
||||
|
||||
==================================
|
||||
Chapter 10: Ready For Some Action?
|
||||
==================================
|
||||
|
||||
So far we have mostly built our module by declaring fields and views. We just introduced business
|
||||
logic in the :ref:`previous chapter <howto/rdtraining/09_compute_onchange>` thanks to computed fields
|
||||
and onchanges. In any real business scenario, we would want to link some business logic to action buttons.
|
||||
In our real estate example, we would like to be able to:
|
||||
|
||||
- cancel or set a property as sold
|
||||
- accept or refuse an offer
|
||||
|
||||
One could argue that we can already do these things by changing the state manually, but
|
||||
this is not really convenient. Moreover, we want to add some extra processing: when an offer is
|
||||
accepted we want to set the selling price and the buyer for the property.
|
||||
|
||||
Action Type
|
||||
===========
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/actions` and :ref:`reference/exceptions`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section:
|
||||
|
||||
- You should be able to cancel or set a property as sold:
|
||||
|
||||
.. image:: 10_actions/media/property.gif
|
||||
:align: center
|
||||
:alt: Cancel and set to sold
|
||||
|
||||
A canceled property cannot be sold and a sold property cannot be canceled. For the sake of
|
||||
clarity, the ``state`` field has been added on the view.
|
||||
|
||||
- You should be able to accept or refuse an offer:
|
||||
|
||||
.. image:: 10_actions/media/offer_01.gif
|
||||
:align: center
|
||||
:alt: Accept or refuse an offer
|
||||
|
||||
- Once an offer is accepted, the selling price and the buyer should be set:
|
||||
|
||||
.. image:: 10_actions/media/offer_02.gif
|
||||
:align: center
|
||||
:alt: Accept an offer
|
||||
|
||||
In our real estate module, we want to link business logic with some buttons. The most common way to
|
||||
do this is to:
|
||||
|
||||
- Add a button in the view, for example in the ``header`` of the view:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_do_something" type="object" string="Do Something"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
- and link this button to business logic:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class TestAction(models.Model):
|
||||
_name = "test.action"
|
||||
|
||||
name = fields.Char()
|
||||
|
||||
def action_do_something(self):
|
||||
for record in self:
|
||||
record.name = "Something"
|
||||
return True
|
||||
|
||||
By assigning ``type="object"`` to our button, the Odoo framework will execute a Python method
|
||||
with ``name="action_do_something"`` on the given model.
|
||||
|
||||
The first important detail to note is that our method name isn't prefixed with an underscore
|
||||
(``_``). This makes our method a **public** method, which can be called directly from the Odoo
|
||||
interface (through an RPC call). Until now, all methods we created (compute, onchange) were called
|
||||
internally, so we used **private** methods prefixed by an underscore. You should always define your
|
||||
methods as private unless they need to be called from the user interface.
|
||||
|
||||
Also note that we loop on ``self``. Always assume that a method can be called on multiple records; it's
|
||||
better for reusability.
|
||||
|
||||
Finally, a public method should always return something so that it can be called through XML-RPC.
|
||||
When in doubt, just ``return True``.
|
||||
|
||||
There are hundreds of examples in the Odoo source code. One example is this
|
||||
`button in a view <https://github.com/odoo/odoo/blob/cd9af815ba591935cda367d33a1d090f248dd18d/addons/crm/views/crm_lead_views.xml#L9-L11>`__
|
||||
and its
|
||||
`corresponding Python method <https://github.com/odoo/odoo/blob/cd9af815ba591935cda367d33a1d090f248dd18d/addons/crm/models/crm_lead.py#L746-L760>`__
|
||||
|
||||
.. exercise:: Cancel and set a property as sold.
|
||||
|
||||
- Add the buttons 'Cancel' and 'Sold' to the ``estate.property`` model. A canceled property
|
||||
cannot be set as sold, and a sold property cannot be canceled.
|
||||
|
||||
Refer to the first image of the **Goal** for the expected result.
|
||||
|
||||
Tip: in order to raise an error, you can use the :ref:`UserError<reference/exceptions>`
|
||||
function. There are plenty of examples in the Odoo source code ;-)
|
||||
|
||||
- Add the buttons 'Accept' and 'Refuse' to the ``estate.property.offer`` model.
|
||||
|
||||
Refer to the second image of the **Goal** for the expected result.
|
||||
|
||||
Tip: to use an icon as a button, have a look
|
||||
`at this example <https://github.com/odoo/odoo/blob/cd9af815ba591935cda367d33a1d090f248dd18d/addons/event/views/event_views.xml#L521>`__.
|
||||
|
||||
- When an offer is accepted, set the buyer and the selling price for the corresponding property.
|
||||
|
||||
Refer to the third image of the **Goal** for the expected result.
|
||||
|
||||
Pay attention: in real life only one offer can be accepted for a given property!
|
||||
|
||||
Object Type
|
||||
===========
|
||||
|
||||
In :ref:`howto/rdtraining/06_firstui`, we created an action that was linked to a menu.
|
||||
You may be wondering if it is possible to link an action to a button. Good news, it is! One
|
||||
way to do it is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<button type="action" name="%(test.test_model_action)d" string="My Action"/>
|
||||
|
||||
We use ``type="action"`` and we refer to the :term:`external identifier` in the ``name``.
|
||||
|
||||
In the :ref:`next chapter <howto/rdtraining/11_constraints>` we'll see how we can prevent encoding
|
||||
incorrect data in Odoo.
|
After Width: | Height: | Size: 186 KiB |
After Width: | Height: | Size: 120 KiB |
After Width: | Height: | Size: 97 KiB |
129
content/developer/howtos/rdtraining/11_constraints.rst
Normal file
@ -0,0 +1,129 @@
|
||||
.. _howto/rdtraining/11_constraints:
|
||||
|
||||
=======================
|
||||
Chapter 11: Constraints
|
||||
=======================
|
||||
|
||||
The :ref:`previous chapter <howto/rdtraining/10_actions>` introduced the ability to add some
|
||||
business logic to our model. We can now link buttons to business code, but how can we prevent
|
||||
users from entering incorrect data? For example, in our real estate module nothing prevents
|
||||
users from setting a negative expected price.
|
||||
|
||||
Odoo provides two ways to set up automatically verified invariants:
|
||||
:func:`Python constraints <odoo.api.constrains>` and
|
||||
:attr:`SQL constraints <odoo.models.Model._sql_constraints>`.
|
||||
|
||||
SQL
|
||||
===
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/orm/models` and in the `PostgreSQL's documentation`_.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section:
|
||||
|
||||
- Amounts should be (strictly) positive
|
||||
|
||||
.. image:: 11_constraints/media/sql_01.gif
|
||||
:align: center
|
||||
:alt: Constraints on amounts
|
||||
|
||||
- Property types and tags should have a unique name
|
||||
|
||||
.. image:: 11_constraints/media/sql_02.gif
|
||||
:align: center
|
||||
:alt: Constraints on names
|
||||
|
||||
SQL constraints are defined through the model attribute
|
||||
:attr:`~odoo.models.Model._sql_constraints`. This attribute is assigned a list
|
||||
of triples containing strings ``(name, sql_definition, message)``, where ``name`` is a
|
||||
valid SQL constraint name, ``sql_definition`` is a table_constraint_ expression
|
||||
and ``message`` is the error message.
|
||||
|
||||
You can find a simple example
|
||||
`here <https://github.com/odoo/odoo/blob/24b0b6f07f65b6151d1d06150e376320a44fd20a/addons/analytic/models/analytic_account.py#L20-L23>`__.
|
||||
|
||||
.. exercise:: Add SQL constraints.
|
||||
|
||||
Add the following constraints to their corresponding models:
|
||||
|
||||
- A property expected price must be strictly positive
|
||||
- A property selling price must be positive
|
||||
- An offer price must be strictly positive
|
||||
- A property tag name and property type name must be unique
|
||||
|
||||
Tip: search for the ``unique`` keyword in the Odoo codebase for examples of unique names.
|
||||
|
||||
Restart the server with the ``-u estate`` option to see the result. Note that you might have data
|
||||
that prevents a SQL constraint from being set. An error message similar to the following might pop up:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
ERROR rd-demo odoo.schema: Table 'estate_property_offer': unable to add constraint 'estate_property_offer_check_price' as CHECK(price > 0)
|
||||
|
||||
For example, if some offers have a price of zero, then the constraint can't be applied. You can delete
|
||||
the problematic data in order to apply the new constraints.
|
||||
|
||||
Python
|
||||
======
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:func:`~odoo.api.constrains`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, it will not possible to accept an offer
|
||||
lower than 90% of the expected price.
|
||||
|
||||
.. image:: 11_constraints/media/python.gif
|
||||
:align: center
|
||||
:alt: Python constraint
|
||||
|
||||
SQL constraints are an efficient way of ensuring data consistency. However it may be necessary
|
||||
to make more complex checks which require Python code. In this case we need a Python constraint.
|
||||
|
||||
A Python constraint is defined as a method decorated with
|
||||
:func:`~odoo.api.constrains` and is invoked on a recordset. The decorator
|
||||
specifies which fields are involved in the constraint. The constraint is automatically evaluated
|
||||
when any of these fields are modified . The method is expected to
|
||||
raise an exception if its invariant is not satisfied::
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
...
|
||||
|
||||
@api.constrains('date_end')
|
||||
def _check_date_end(self):
|
||||
for record in self:
|
||||
if record.date_end < fields.Date.today():
|
||||
raise ValidationError("The end date cannot be set in the past")
|
||||
# all records passed the test, don't return anything
|
||||
|
||||
A simple example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/3783654b87851bdeb11e32da78bb5b62865b869a/addons/account/models/account_payment_term.py#L104-L108>`__.
|
||||
|
||||
.. exercise:: Add Python constraints.
|
||||
|
||||
Add a constraint so that the selling price cannot be lower than 90% of the expected price.
|
||||
|
||||
Tip: the selling price is zero until an offer is validated. You will need to fine tune your
|
||||
check to take this into account.
|
||||
|
||||
.. warning::
|
||||
|
||||
Always use the :meth:`~odoo.tools.float_utils.float_compare` and
|
||||
:meth:`~odoo.tools.float_utils.float_is_zero` methods when working with floats!
|
||||
|
||||
Ensure the constraint is triggered every time the selling price or the expected price is changed!
|
||||
|
||||
SQL constraints are usually more efficient than Python constraints. When performance matters, always
|
||||
prefer SQL over Python constraints.
|
||||
|
||||
Our real estate module is starting to look good. We added some business logic, and now we make sure
|
||||
the data is consistent. However, the user interface is still a bit rough. Let's see how we can
|
||||
improve it in the :ref:`next chapter <howto/rdtraining/12_sprinkles>`.
|
||||
|
||||
.. _PostgreSQL's documentation:
|
||||
.. _table_constraint:
|
||||
https://www.postgresql.org/docs/current/ddl-constraints.html
|
After Width: | Height: | Size: 316 KiB |
After Width: | Height: | Size: 225 KiB |
After Width: | Height: | Size: 128 KiB |
523
content/developer/howtos/rdtraining/12_sprinkles.rst
Normal file
@ -0,0 +1,523 @@
|
||||
.. _howto/rdtraining/12_sprinkles:
|
||||
|
||||
=============================
|
||||
Chapter 12: Add The Sprinkles
|
||||
=============================
|
||||
|
||||
Our real estate module now makes sense from a business perspective. We created
|
||||
:ref:`specific views <howto/rdtraining/07_basicviews>`, added several
|
||||
:ref:`action buttons <howto/rdtraining/10_actions>` and
|
||||
:ref:`constraints <howto/rdtraining/11_constraints>`. However our user interface is still a bit
|
||||
rough. We would like to add some colors to the list views and make some fields and buttons conditionally
|
||||
disappear. For example, the 'Sold' and 'Cancel' buttons should disappear when the property
|
||||
is sold or canceled since it is no longer allowed to change the state at this point.
|
||||
|
||||
This chapter covers a very small subset of what can be done in the views. Do not hesitate to
|
||||
read the reference documentation for a more complete overview.
|
||||
|
||||
**Reference**: the documentation related to this chapter can be found in
|
||||
:ref:`reference/views`.
|
||||
|
||||
Inline Views
|
||||
============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, a specific list of properties should be added to the property
|
||||
type view:
|
||||
|
||||
.. image:: 12_sprinkles/media/inline_view.png
|
||||
:align: center
|
||||
:alt: Inline list view
|
||||
|
||||
In the real estate module we added a list of offers for a property. We simply added the field
|
||||
``offer_ids`` with:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="offer_ids"/>
|
||||
|
||||
The field uses the specific view for ``estate.property.offer``. In some cases we want to define
|
||||
a specific list view which is only used in the context of a form view. For example, we would like
|
||||
to display the list of properties linked to a property type. However, we only want to display 3
|
||||
fields for clarity: name, expected price and state.
|
||||
|
||||
To do this, we can define *inline* list views. An inline list view is defined directly inside
|
||||
a form view. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class TestModel(models.Model):
|
||||
_name = "test.model"
|
||||
_description = "Test Model"
|
||||
|
||||
description = fields.Char()
|
||||
line_ids = fields.One2many("test.model.line", "model_id")
|
||||
|
||||
|
||||
class TestModelLine(models.Model):
|
||||
_name = "test.model.line"
|
||||
_description = "Test Model Line"
|
||||
|
||||
model_id = fields.Many2one("test.model")
|
||||
field_1 = fields.Char()
|
||||
field_2 = fields.Char()
|
||||
field_3 = fields.Char()
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<form>
|
||||
<field name="description"/>
|
||||
<field name="line_ids">
|
||||
<tree>
|
||||
<field name="field_1"/>
|
||||
<field name="field_2"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>
|
||||
|
||||
In the form view of the ``test.model``, we define a specific list view for ``test.model.line``
|
||||
with fields ``field_1`` and ``field_2``.
|
||||
|
||||
An example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/event/views/event_tag_views.xml#L27-L33>`__.
|
||||
|
||||
.. exercise:: Add an inline list view.
|
||||
|
||||
- Add the ``One2many`` field ``property_ids`` to the ``estate.property.type`` model.
|
||||
- Add the field in the ``estate.property.type`` form view as depicted in the **Goal** of this
|
||||
section.
|
||||
|
||||
Widgets
|
||||
=======
|
||||
|
||||
**Reference**: the documentation related to this section can be found in
|
||||
:ref:`reference/js/widgets`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the state of the property should be displayed using a
|
||||
specific widget:
|
||||
|
||||
.. image:: 12_sprinkles/media/widget.png
|
||||
:align: center
|
||||
:alt: Statusbar widget
|
||||
|
||||
Four states are displayed: New, Offer Received, Offer Accepted and Sold.
|
||||
|
||||
Whenever we've added fields to our models, we've (almost) never had to worry about how
|
||||
these fields would look like in the user interface. For example, a date picker is provided
|
||||
for a ``Date`` field and a ``One2many`` field is automatically displayed as a list. Odoo
|
||||
chooses the right 'widget' depending on the field type.
|
||||
|
||||
However, in some cases, we want a specific representation of a field which can be done thanks to
|
||||
the ``widget`` attribute. We already used it for the ``tag_ids`` field when we used the
|
||||
``widget="many2many_tags"`` attribute. If we hadn't used it, then the field would have displayed as a
|
||||
list.
|
||||
|
||||
Each field type has a set of widgets which can be used to fine tune its display. Some widgets also
|
||||
take extra options. An exhaustive list can be found in :ref:`reference/js/widgets`.
|
||||
|
||||
.. exercise:: Use the status bar widget.
|
||||
|
||||
Use the ``statusbar`` widget in order to display the ``state`` of the ``estate.property`` as
|
||||
depicted in the **Goal** of this section.
|
||||
|
||||
Tip: a simple example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/account/views/account_bank_statement_views.xml#L136>`__.
|
||||
|
||||
.. warning:: Same field multiple times in a view
|
||||
|
||||
Add a field only **once** to a list or a form view. Adding it multiple times is
|
||||
not supported.
|
||||
|
||||
List Order
|
||||
==========
|
||||
|
||||
**Reference**: the documentation related to this section can be found in
|
||||
:ref:`reference/orm/models`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, all lists should display by default in a deterministic
|
||||
order. Property types can be ordered manually.
|
||||
|
||||
During the previous exercises, we created several list views. However, at no point did we specify
|
||||
which order the records had to be listed in by default. This is a very important thing for many business
|
||||
cases. For example, in our real estate module we would want to display the highest offers on top of the
|
||||
list.
|
||||
|
||||
Model
|
||||
-----
|
||||
|
||||
Odoo provides several ways to set a default order. The most common way is to define
|
||||
the ``_order`` attribute directly in the model. This way, the retrieved records will follow
|
||||
a deterministic order which will be consistent in all views including when records are searched
|
||||
programmatically. By default there is no order specified, therefore the records will be
|
||||
retrieved in a non-deterministic order depending on PostgreSQL.
|
||||
|
||||
The ``_order`` attribute takes a string containing a list of fields which will be used for sorting.
|
||||
It will be converted to an order_by_ clause in SQL. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class TestModel(models.Model):
|
||||
_name = "test.model"
|
||||
_description = "Test Model"
|
||||
_order = "id desc"
|
||||
|
||||
description = fields.Char()
|
||||
|
||||
Our records are ordered by descending ``id``, meaning the highest comes first.
|
||||
|
||||
.. exercise:: Add model ordering.
|
||||
|
||||
Define the following orders in their corresponding models:
|
||||
|
||||
=================================== ===================================
|
||||
Model Order
|
||||
=================================== ===================================
|
||||
``estate.property`` Descending ID
|
||||
``estate.property.offer`` Descending Price
|
||||
``estate.property.tag`` Name
|
||||
``estate.property.type`` Name
|
||||
=================================== ===================================
|
||||
|
||||
View
|
||||
----
|
||||
|
||||
Ordering is possible at the model level. This has the advantage of a consistent order everywhere
|
||||
a list of records is retrieved. However, it is also possible to define a specific order directly
|
||||
in a view thanks to the ``default_order`` attribute
|
||||
(`example <https://github.com/odoo/odoo/blob/892dd6860733c46caf379fd36f57219082331b66/addons/crm/report/crm_activity_report_views.xml#L30>`__).
|
||||
|
||||
Manual
|
||||
------
|
||||
|
||||
Both model and view ordering allow flexibility when sorting records, but there is still one case
|
||||
we need to cover: the manual ordering. A user may want to sort records depending on the business
|
||||
logic. For example, in our real estate module we would like to sort the property types manually.
|
||||
It is indeed useful to have the most used types appear at the top of the list. If our real estate
|
||||
agency mainly sells houses, it is more convenient to have 'House' appear before 'Apartment'.
|
||||
|
||||
To do so, a ``sequence`` field is used in combination with the ``handle`` widget. Obviously
|
||||
the ``sequence`` field must be the first field in the ``_order`` attribute.
|
||||
|
||||
.. exercise:: Add manual ordering.
|
||||
|
||||
- Add the following field:
|
||||
|
||||
=================================== ======================= =======================
|
||||
Model Field Type
|
||||
=================================== ======================= =======================
|
||||
``estate.property.type`` Sequence Integer
|
||||
=================================== ======================= =======================
|
||||
|
||||
- Add the sequence to the ``estate.property.type`` list view with the correct widget.
|
||||
|
||||
Tip: you can find an example here:
|
||||
`model <https://github.com/odoo/odoo/blob/892dd6860733c46caf379fd36f57219082331b66/addons/crm/models/crm_stage.py#L36>`__
|
||||
and
|
||||
`view <https://github.com/odoo/odoo/blob/892dd6860733c46caf379fd36f57219082331b66/addons/crm/views/crm_stage_views.xml#L23>`__.
|
||||
|
||||
Attributes and options
|
||||
======================
|
||||
|
||||
It would be prohibitive to detail all the available features which allow fine tuning of the look of a
|
||||
view. Therefore, we'll stick to the most common ones.
|
||||
|
||||
Form
|
||||
----
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the property form view will have:
|
||||
|
||||
- Conditional display of buttons and fields
|
||||
- Tag colors
|
||||
|
||||
.. image:: 12_sprinkles/media/form.gif
|
||||
:align: center
|
||||
:alt: Form view with sprinkles
|
||||
|
||||
|
||||
In our real estate module, we want to modify the behavior of some fields. For example, we don't
|
||||
want to be able to create or edit a property type from the form view. Instead we expect the
|
||||
types to be handled in their appropriate menu. We also want to give tags a color. In order to add these
|
||||
behavior customizations, we can add the ``options`` attribute to several field widgets.
|
||||
|
||||
.. exercise:: Add widget options.
|
||||
|
||||
- Add the appropriate option to the ``property_type_id`` field to prevent the creation and the
|
||||
editing of a property type from the property form view. Have a look at the
|
||||
:ref:`Many2one widget documentation <reference/js/widgets>` for more info.
|
||||
|
||||
- Add the following field:
|
||||
|
||||
=================================== ======================= =======================
|
||||
Model Field Type
|
||||
=================================== ======================= =======================
|
||||
``estate.property.tag`` Color Integer
|
||||
=================================== ======================= =======================
|
||||
|
||||
Then add the appropriate option to the ``tag_ids`` field to add a color picker on the tags.
|
||||
Have a look at the :ref:`FieldMany2ManyTags widget documentation <reference/js/widgets>`
|
||||
for more info.
|
||||
|
||||
In :ref:`howto/rdtraining/06_firstui`, we saw that reserved fields were used for
|
||||
specific behaviors. For example, the ``active`` field is used to automatically filter out
|
||||
inactive records. We added the ``state`` as a reserved field as well. It's now time to use it!
|
||||
A ``state`` field is used in combination with a ``states`` attribute in the view to display
|
||||
buttons conditionally.
|
||||
|
||||
.. exercise:: Add conditional display of buttons.
|
||||
|
||||
Use the ``states`` attribute to display the header buttons conditionally as depicted
|
||||
in this section's **Goal** (notice how the 'Sold' and 'Cancel' buttons change when the state is modified).
|
||||
|
||||
Tip: do not hesitate to search for ``states=`` in the Odoo XML files for some examples.
|
||||
|
||||
More generally, it is possible to make a field ``invisible``, ``readonly`` or ``required`` based
|
||||
on the value of other fields thanks to the ``attrs`` attribute. Note that ``invisible`` can also be applied
|
||||
to other elements of the view such as ``button`` or ``group``.
|
||||
|
||||
The ``attrs`` is a dictionary with the property as a key and a domain as a value. The domain gives
|
||||
the conditon in which the property applies. For example:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<form>
|
||||
<field name="description" attrs="{'invisible': [('is_partner', '=', False)]}"/>
|
||||
<field name="is_partner" invisible="1"/>
|
||||
</form>
|
||||
|
||||
This means that the ``description`` field is invisible when ``is_partner`` is ``False``. It is
|
||||
important to note that a field used in an ``attrs`` **must** be present in the view. If it
|
||||
should not be displayed to the user, we can use the ``invisible`` attribute to hide it.
|
||||
|
||||
.. exercise:: Use ``attrs``.
|
||||
|
||||
- Make the garden area and orientation invisible in the ``estate.property`` form view when
|
||||
there is no garden.
|
||||
- Make the 'Accept' and 'Refuse' buttons invisible once the offer state is set.
|
||||
- Do not allow adding an offer when the property state is 'Offer Accepted', 'Sold' or
|
||||
'Canceled'. To do this use the ``readonly`` ``attrs``.
|
||||
|
||||
.. warning::
|
||||
|
||||
Using a (conditional) ``readonly`` attribute in the view can be useful to prevent data entry
|
||||
errors, but keep in mind that it doesn't provide any level of security! There is no check done
|
||||
server-side, therefore it's always possible to write on the field through a RPC call.
|
||||
|
||||
List
|
||||
----
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the property and offer list views should have color decorations.
|
||||
Additionally, offers and tags will be editable directly in the list, and the availability date will be
|
||||
hidden by default.
|
||||
|
||||
.. image:: 12_sprinkles/media/decoration.png
|
||||
:align: center
|
||||
:alt: List view with decorations and optional field
|
||||
|
||||
.. image:: 12_sprinkles/media/editable_list.gif
|
||||
:align: center
|
||||
:alt: Editable list
|
||||
|
||||
When the model only has a few fields, it can be useful to edit records directly through the list
|
||||
view and not have to open the form view. In the real estate example, there is no need to open a form view
|
||||
to add an offer or create a new tag. This can be achieved thanks to the ``editable`` attribute.
|
||||
|
||||
.. exercise:: Make list views editable.
|
||||
|
||||
Make the ``estate.property.offer`` and ``estate.property.tag`` list views editable.
|
||||
|
||||
On the other hand, when a model has a lot of fields it can be tempting to add too many fields in the
|
||||
list view and make it unclear. An alternative method is to add the fields, but make them optionally
|
||||
hidden. This can be achieved thanks to the ``optional`` attribute.
|
||||
|
||||
.. exercise:: Make a field optional.
|
||||
|
||||
Make the field ``date_availability`` on the ``estate.property`` list view optional and hidden by
|
||||
default.
|
||||
|
||||
Finally, color codes are useful to visually emphasize records. For example, in the real estate
|
||||
module we would like to display refused offers in red and accepted offers in green. This can be achieved
|
||||
thanks to the ``decoration-{$name}`` attribute (see :ref:`reference/js/widgets` for a
|
||||
complete list):
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<tree decoration-success="is_partner==True">
|
||||
<field name="name">
|
||||
<field name="is_partner" invisible="1">
|
||||
</tree>
|
||||
|
||||
The records where ``is_partner`` is ``True`` will be displayed in green.
|
||||
|
||||
.. exercise:: Add some decorations.
|
||||
|
||||
On the ``estate.property`` list view:
|
||||
|
||||
- Properties with an offer received are green
|
||||
- Properties with an offer accepted are green and bold
|
||||
- Properties sold are muted
|
||||
|
||||
On the ``estate.property.offer`` list view:
|
||||
|
||||
- Refused offers are red
|
||||
- Accepted offers are green
|
||||
- The state should not be visible anymore
|
||||
|
||||
Tips:
|
||||
|
||||
- Keep in mind that **all** fields used in attributes must be in the view!
|
||||
- If you want to test the color of the "Offer Received" and "Offer Accepted" states, add the
|
||||
field in the form view and change it manually (we'll implement the business logic for this later).
|
||||
|
||||
Search
|
||||
------
|
||||
|
||||
**Reference**: the documentation related to this section can be found in
|
||||
:ref:`reference/views/search` and :ref:`reference/views/search/defaults`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the available properties will be filtered by default,
|
||||
and searching on the living area returns results where the area is larger than the given
|
||||
number.
|
||||
|
||||
.. image:: 12_sprinkles/media/search.gif
|
||||
:align: center
|
||||
:alt: Default filters and domains
|
||||
|
||||
Last but not least, there are some tweaks we would like to apply when searching. First of all, we
|
||||
want to have our 'Available' filter applied by default when we access the properties. To make this happen, we
|
||||
need to use the ``search_default_{$name}`` action context, where ``{$name}`` is the filter name.
|
||||
This means that we can define which filter(s) will be activated by default at the action level.
|
||||
|
||||
Here is an example of an
|
||||
`action <https://github.com/odoo/odoo/blob/6decc32a889b46947db6dd4d42ef995935894a2a/addons/crm/report/crm_opportunity_report_views.xml#L115>`__
|
||||
with its
|
||||
`corresponding filter <https://github.com/odoo/odoo/blob/6decc32a889b46947db6dd4d42ef995935894a2a/addons/crm/report/crm_opportunity_report_views.xml#L68>`__.
|
||||
|
||||
.. exercise:: Add a default filter.
|
||||
|
||||
Make the 'Available' filter selected by default in the ``estate.property`` action.
|
||||
|
||||
Another useful improvement in our module would be the ability to search efficiently by living area.
|
||||
In practice, a user will want to search for properties of 'at least' the given area. It is unrealistic
|
||||
to expect users would want to find a property of an exact living area. It is always
|
||||
possible to make a custom search, but that's inconvenient.
|
||||
|
||||
Search view ``<field>`` elements can have a ``filter_domain`` that overrides
|
||||
the domain generated for searching on the given field. In the given domain,
|
||||
``self`` represents the value entered by the user. In the example below, it is
|
||||
used to search on both ``name`` and ``description`` fields.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<search string="Test">
|
||||
<field name="description" string="Name and description"
|
||||
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
|
||||
</group>
|
||||
</search>
|
||||
|
||||
.. exercise:: Change the living area search.
|
||||
|
||||
Add a ``filter_domain`` to the living area to include properties with an area equal to or
|
||||
greater than the given value.
|
||||
|
||||
Stat Buttons
|
||||
============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, there will be a stat button on the property type form view
|
||||
which shows the list of all offers related to properties of the given type when it is clicked on.
|
||||
|
||||
.. image:: 12_sprinkles/media/stat_button.gif
|
||||
:align: center
|
||||
:alt: Stat button
|
||||
|
||||
If you've already used some functional modules in Odoo, you've probably already encountered a 'stat
|
||||
button'. These buttons are displayed on the top right of a form view and give a quick access to
|
||||
linked documents. In our real estate module, we would like to have a quick link to the offers
|
||||
related to a given property type as depicted in the **Goal** of this section.
|
||||
|
||||
At this point of the tutorial we have already seen most of the concepts to do this. However,
|
||||
there is not a single solution and it can still be confusing if you don't know where to start from.
|
||||
We'll describe a step-by-step solution in the exercise. It can always be useful to find some
|
||||
examples in the Odoo codebase by looking for ``oe_stat_button``.
|
||||
|
||||
The following exercise might be a bit more difficult than the previous ones since it assumes you
|
||||
are able to search for examples in the source code on your own. If you are stuck there is probably
|
||||
someone nearby who can help you ;-)
|
||||
|
||||
The exercise introduces the concept of :ref:`reference/fields/related`. The easiest way to
|
||||
understand it is to consider it as a specific case of a computed field. The following definition
|
||||
of the ``description`` field:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
...
|
||||
|
||||
partner_id = fields.Many2one("res.partner", string="Partner")
|
||||
description = fields.Char(related="partner_id.name")
|
||||
|
||||
is equivalent to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
...
|
||||
|
||||
partner_id = fields.Many2one("res.partner", string="Partner")
|
||||
description = fields.Char(compute="_compute_description")
|
||||
|
||||
@api.depends("partner_id.name")
|
||||
def _compute_description(self):
|
||||
for record in self:
|
||||
record.description = record.partner_id.name
|
||||
|
||||
Every time the partner name is changed, the description is modified.
|
||||
|
||||
.. exercise:: Add a stat button to property type.
|
||||
|
||||
- Add the field ``property_type_id`` to ``estate.property.offer``. We can define it as a
|
||||
related field on ``property_id.property_type_id`` and set it as stored.
|
||||
|
||||
Thanks to this field, an offer will be linked to a property type when it's created. You can add
|
||||
the field to the list view of offers to make sure it works.
|
||||
|
||||
- Add the field ``offer_ids`` to ``estate.property.type`` which is the One2many inverse of
|
||||
the field defined in the previous step.
|
||||
|
||||
- Add the field ``offer_count`` to ``estate.property.type``. It is a computed field that counts
|
||||
the number of offers for a given property type (use ``offer_ids`` to do so).
|
||||
|
||||
At this point, you have all the information necessary to know how many offers are linked to
|
||||
a property type. When in doubt, add ``offer_ids`` and ``offer_count`` directly to the view.
|
||||
The next step is to display the list when clicking on the stat button.
|
||||
|
||||
- Create a stat button on ``estate.property.type`` pointing to the ``estate.property.offer``
|
||||
action. This means you should use the ``type="action"`` attribute (go back to the end of
|
||||
:ref:`howto/rdtraining/10_actions` if you need a refresher).
|
||||
|
||||
At this point, clicking on the stat button should display all offers. We still need to filter out the
|
||||
offers.
|
||||
|
||||
- On the ``estate.property.offer`` action, add a domain that defines ``property_type_id``
|
||||
as equal to the ``active_id`` (= the current record,
|
||||
`here is an example <https://github.com/odoo/odoo/blob/df37ce50e847e3489eb43d1ef6fc1bac6d6af333/addons/event/views/event_views.xml#L162>`__)
|
||||
|
||||
Looking good? If not, don't worry, the :ref:`next chapter <howto/rdtraining/13_inheritance>` doesn't
|
||||
require stat buttons ;-)
|
||||
|
||||
.. _order_by:
|
||||
https://www.postgresql.org/docs/current/queries-order.html
|
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 276 KiB |
BIN
content/developer/howtos/rdtraining/12_sprinkles/media/form.gif
Normal file
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 |
244
content/developer/howtos/rdtraining/13_inheritance.rst
Normal file
@ -0,0 +1,244 @@
|
||||
.. _howto/rdtraining/13_inheritance:
|
||||
|
||||
=======================
|
||||
Chapter 13: Inheritance
|
||||
=======================
|
||||
|
||||
A powerful aspect of Odoo is its modularity. A module is dedicated to a business need, but
|
||||
modules can also interact with one another. This is useful for extending the functionality of an existing
|
||||
module. For example, in our real estate scenario we want to display the list of a salesperson's properties
|
||||
directly in the regular user view.
|
||||
|
||||
But before going through the specific Odoo module inheritance, let's see how we can alter the
|
||||
behavior of the standard CRUD (Create, Retrieve, Update or Delete) methods.
|
||||
|
||||
Python Inheritance
|
||||
==================
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section:
|
||||
|
||||
- It should not be possible to delete a property which is not new or canceled.
|
||||
|
||||
.. image:: 13_inheritance/media/unlink.gif
|
||||
:align: center
|
||||
:alt: Unlink
|
||||
|
||||
- When an offer is created, the property state should change to 'Offer Received'
|
||||
- It should not be possible to create an offer with a lower price than an existing offer
|
||||
|
||||
.. image:: 13_inheritance/media/create.gif
|
||||
:align: center
|
||||
:alt: Create
|
||||
|
||||
In our real estate module, we never had to develop anything specific to be able to do the
|
||||
standard CRUD actions. The Odoo framework provides the necessary
|
||||
tools to do them. In fact, such actions are already included in our model thanks to classical
|
||||
Python inheritance::
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class TestModel(models.Model):
|
||||
_name = "test.model"
|
||||
_description = "Test Model"
|
||||
|
||||
...
|
||||
|
||||
Our ``class TestModel`` inherits from :class:`~odoo.models.Model` which provides
|
||||
:meth:`~odoo.models.Model.create`, :meth:`~odoo.models.Model.read`, :meth:`~odoo.models.Model.write`
|
||||
and :meth:`~odoo.models.Model.unlink`.
|
||||
|
||||
These methods (and any other method defined on :class:`~odoo.models.Model`) can be extended to add
|
||||
specific business logic::
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class TestModel(models.Model):
|
||||
_name = "test.model"
|
||||
_description = "Test Model"
|
||||
|
||||
...
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
# Do some business logic, modify vals...
|
||||
...
|
||||
# Then call super to execute the parent method
|
||||
return super().create(vals)
|
||||
|
||||
The decorator :func:`~odoo.api.model` is necessary for the :meth:`~odoo.models.Model.create`
|
||||
method because the content of the recordset ``self`` is not relevant in the context of creation,
|
||||
but it is not necessary for the other CRUD methods.
|
||||
|
||||
In Python 3, ``super()`` is equivalent to ``super(TestModel, self)``. The latter may be necessary
|
||||
when you need to call the parent method with a modified recordset.
|
||||
|
||||
.. danger::
|
||||
|
||||
- It is very important to **always** call ``super()`` to avoid breaking the flow. There are
|
||||
only a few very specific cases where you don't want to call it.
|
||||
- Make sure to **always** return data consistent with the parent method. For example, if
|
||||
the parent method returns a ``dict()``, your override must also return a ``dict()``.
|
||||
|
||||
.. exercise:: Add business logic to the CRUD methods.
|
||||
|
||||
- Prevent deletion of a property if its state is not 'New' or 'Canceled'
|
||||
|
||||
Tip: override :meth:`~odoo.models.Model.unlink` and remember that ``self`` can be a recordset
|
||||
with more than one record.
|
||||
|
||||
- At offer creation, set the property state to 'Offer Received'. Also raise an error if the user
|
||||
tries to create an offer with a lower amount than an existing offer.
|
||||
|
||||
Tip: the ``property_id`` field is available in the ``vals``, but it is an ``int``. To
|
||||
instantiate an ``estate.property`` object, use ``self.env[model_name].browse(value)``
|
||||
(`example <https://github.com/odoo/odoo/blob/136e4f66cd5cafe7df450514937c7218c7216c93/addons/gamification/models/badge.py#L57>`__)
|
||||
|
||||
Model Inheritance
|
||||
=================
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/orm/inheritance`.
|
||||
|
||||
In our real estate module, we would like to display the list of properties linked to a salesperson
|
||||
directly in the Settings / Users & Companies / Users form view. To do this, we need to add a field to
|
||||
the ``res.users`` model and adapt its view to show it.
|
||||
|
||||
Odoo provides two *inheritance* mechanisms to extend an existing model in a modular way.
|
||||
|
||||
The first inheritance mechanism allows modules to modify the behavior of a model defined in an
|
||||
another module by:
|
||||
|
||||
- adding fields to the model,
|
||||
- overriding the definition of fields in the model,
|
||||
- adding constraints to the model,
|
||||
- adding methods to the model,
|
||||
- overriding existing methods in the model.
|
||||
|
||||
The second inheritance mechanism (delegation) allows every record of a model to be linked
|
||||
to a parent model's record and provides transparent access to the
|
||||
fields of this parent record.
|
||||
|
||||
.. image:: 13_inheritance/media/inheritance_methods.png
|
||||
:align: center
|
||||
:alt: Inheritance Methods
|
||||
|
||||
In Odoo, the first mechanism is by far the most used. In our case, we want to add a field to an
|
||||
existing model, which means we will use the first mechanism. For example::
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class InheritedModel(models.Model):
|
||||
_inherit = "inherited.model"
|
||||
|
||||
new_field = fields.Char(string="New Field")
|
||||
|
||||
A practical example where two fields are added to
|
||||
a model can be found
|
||||
`here <https://github.com/odoo/odoo/blob/60e9410e9aa3be4a9db50f6f7534ba31fea3bc29/addons/account_fleet/models/account_move.py#L39-L47>`__.
|
||||
|
||||
By convention, each inherited model is defined in its own Python file. In our example, it would be
|
||||
``models/inherited_model.py``.
|
||||
|
||||
.. exercise:: Add a field to Users.
|
||||
|
||||
- Add the following field to ``res.users``:
|
||||
|
||||
===================== ======================================================
|
||||
Field Type
|
||||
===================== ======================================================
|
||||
property_ids One2many inverse of ``user_id`` to ``estate.property``
|
||||
===================== ======================================================
|
||||
|
||||
- Add a domain to the field so it only lists the available properties.
|
||||
|
||||
Now let's add the field to the view and check that everything is working well!
|
||||
|
||||
View Inheritance
|
||||
================
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/views/inheritance`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, the list of available properties linked
|
||||
to a salesperson should be displayed in their user form view
|
||||
|
||||
.. image:: 13_inheritance/media/users.png
|
||||
:align: center
|
||||
:alt: Users
|
||||
|
||||
Instead of modifying existing views in place (by overwriting them), Odoo
|
||||
provides view inheritance where children 'extension' views are applied on top of
|
||||
root views. These extension can both add and remove content from their parent view.
|
||||
|
||||
An extension view references its parent using the ``inherit_id`` field.
|
||||
Instead of a single view, its ``arch`` field contains a number of
|
||||
``xpath`` elements that select and alter the content of their parent view:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="inherited_model_view_form" model="ir.ui.view">
|
||||
<field name="name">inherited.model.form.inherit.test</field>
|
||||
<field name="model">inherited.model</field>
|
||||
<field name="inherit_id" ref="inherited.inherited_model_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- find field description and add the field
|
||||
new_field after it -->
|
||||
<xpath expr="//field[@name='description']" position="after">
|
||||
<field name="new_field"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
``expr``
|
||||
An XPath_ expression selecting a single element in the parent view.
|
||||
Raises an error if it matches no element or more than one
|
||||
``position``
|
||||
Operation to apply to the matched element:
|
||||
|
||||
``inside``
|
||||
appends ``xpath``'s body to the end of the matched element
|
||||
``replace``
|
||||
replaces the matched element with the ``xpath``'s body, replacing any ``$0`` node occurrence
|
||||
in the new body with the original element
|
||||
``before``
|
||||
inserts the ``xpath``'s body as a sibling before the matched element
|
||||
``after``
|
||||
inserts the ``xpaths``'s body as a sibling after the matched element
|
||||
``attributes``
|
||||
alters the attributes of the matched element using the special
|
||||
``attribute`` elements in the ``xpath``'s body
|
||||
|
||||
When matching a single element, the ``position`` attribute can be set directly
|
||||
on the element to be found. Both inheritances below have the same result.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<xpath expr="//field[@name='description']" position="after">
|
||||
<field name="idea_ids" />
|
||||
</xpath>
|
||||
|
||||
<field name="description" position="after">
|
||||
<field name="idea_ids" />
|
||||
</field>
|
||||
|
||||
An example of a view inheritance extension can be found
|
||||
`here <https://github.com/odoo/odoo/blob/691d1f087040f1ec7066e485d19ce3662dfc6501/addons/account_fleet/views/account_move_views.xml#L3-L17>`__.
|
||||
|
||||
.. exercise:: Add fields to the Users view.
|
||||
|
||||
Add the ``property_ids`` field to the ``base.view_users_form`` in a new notebook page.
|
||||
|
||||
Tip: an example an inheritance of the users' view can be found
|
||||
`here <https://github.com/odoo/odoo/blob/691d1f087040f1ec7066e485d19ce3662dfc6501/addons/gamification/views/res_users_views.xml#L5-L14>`__.
|
||||
|
||||
Inheritance is extensively used in Odoo due to its modular concept. Do not hesitate to read
|
||||
the corresponding documentation for more info!
|
||||
|
||||
In the :ref:`next chapter <howto/rdtraining/14_other_module>`, we will learn how to interact with
|
||||
other modules.
|
||||
|
||||
.. _XPath: https://w3.org/TR/xpath
|
After Width: | Height: | Size: 356 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 224 KiB |
After Width: | Height: | Size: 54 KiB |
172
content/developer/howtos/rdtraining/14_other_module.rst
Normal file
@ -0,0 +1,172 @@
|
||||
.. _howto/rdtraining/14_other_module:
|
||||
|
||||
=======================================
|
||||
Chapter 14: Interact With Other Modules
|
||||
=======================================
|
||||
|
||||
In the :ref:`previous chapter <howto/rdtraining/13_inheritance>`, we used inheritance to modify
|
||||
the behavior of a module. In our real estate scenario, we would like to go a step further
|
||||
and be able to generate invoices for our customers. Odoo provides an Invoicing module, so it
|
||||
would be neat to create an invoice directly from our real estate module, i.e. once a property
|
||||
is set to 'Sold', an invoice is created in the Invoicing application.
|
||||
|
||||
Concrete Example: Account Move
|
||||
==============================
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section:
|
||||
|
||||
- A new module ``estate_account`` should be created
|
||||
- When a property is sold, an invoice should be issued for the buyer
|
||||
|
||||
.. image:: 14_other_module/media/create_inv.gif
|
||||
:align: center
|
||||
:alt: Invoice creation
|
||||
|
||||
Any time we interact with another module, we need to keep in mind the modularity. If we intend
|
||||
to sell our application to real estate agencies, some may want the invoicing feature but
|
||||
others may not want it.
|
||||
|
||||
Link Module
|
||||
-----------
|
||||
|
||||
The common approach for such use cases is to create a 'link' module. In our case, the module
|
||||
would depend on ``estate`` and ``account`` and would include the invoice creation logic
|
||||
of the estate property. This way the real estate and the accounting modules can be installed
|
||||
independently. When both are installed, the link module provides the new feature.
|
||||
|
||||
.. exercise:: Create a link module.
|
||||
|
||||
Create the ``estate_account`` module, which depends on the ``estate`` and ``account`` modules.
|
||||
For now, it will be an empty shell.
|
||||
|
||||
Tip: you already did this at the
|
||||
:ref:`beginning of the tutorial <howto/rdtraining/03_newapp>`. The process is very similar.
|
||||
|
||||
When the ``estate_account`` module appears in the list, go ahead and install it! You'll notice that
|
||||
the Invoicing application is installed as well. This is expected since your module depends on it.
|
||||
If you uninstall the Invoicing application, your module will be uninstalled as well.
|
||||
|
||||
.. _howto/rdtraining/14_other_module/create:
|
||||
|
||||
Invoice Creation
|
||||
----------------
|
||||
|
||||
It's now time to generate the invoice. We want to add functionality to the
|
||||
``estate.property`` model, i.e. we want to add some extra logic for when a property is sold.
|
||||
Does that sound familiar? If not, it's a good idea to go back to the
|
||||
:ref:`previous chapter <howto/rdtraining/13_inheritance>` since you might have missed something ;-)
|
||||
|
||||
As a first step, we need to extend the action called when pressing the
|
||||
:ref:`'Sold' button <howto/rdtraining/10_actions>` on a property. To do so, we need to create a
|
||||
:ref:`model inheritance <howto/rdtraining/13_inheritance>` in the ``estate_account`` module
|
||||
for the ``estate.property`` model. For now, the overridden action will simply return the ``super``
|
||||
call. Maybe an example will make things clearer::
|
||||
|
||||
from odoo import models
|
||||
|
||||
class InheritedModel(models.Model):
|
||||
_inherit = "inherited.model"
|
||||
|
||||
def inherited_action(self):
|
||||
return super().inherited_action()
|
||||
|
||||
A practical example can be found
|
||||
`here <https://github.com/odoo/odoo/blob/f1f48cdaab3dd7847e8546ad9887f24a9e2ed4c1/addons/event_sale/models/account_move.py#L7-L16>`__.
|
||||
|
||||
.. exercise:: Add the first step of invoice creation.
|
||||
|
||||
- Create a ``estate_property.py`` file in the correct folder of the ``estate_account`` module.
|
||||
- ``_inherit`` the ``estate.property`` model.
|
||||
- Override the ``action_sold`` method (you might have named it differently) to return the ``super``
|
||||
call.
|
||||
|
||||
Tip: to make sure it works, add a ``print`` or a debugger breakpoint in the overridden method.
|
||||
|
||||
Is it working? If not, maybe check that all Python files are correctly imported.
|
||||
|
||||
If the override is working, we can move forward and create the invoice. Unfortunately, there
|
||||
is no easy way to know how to create any given object in Odoo. Most of the time, it is necessary
|
||||
to have a look at its model to find the required fields and provide appropriate values.
|
||||
|
||||
A good way to learn is to look at how other modules already do what you want to do. For example, one of
|
||||
the basic flows of Sales is the creation of an invoice from a sales order. This looks like a good
|
||||
starting point since it does exactly what we want to do. Take some time to read and understand the
|
||||
`_create_invoices <https://github.com/odoo/odoo/blob/f1f48cdaab3dd7847e8546ad9887f24a9e2ed4c1/addons/sale/models/sale.py#L610-L717>`__
|
||||
method. When you are done crying because this simple task looks awfully complex, we can move
|
||||
forward in the tutorial.
|
||||
|
||||
To create an invoice, we need the following information:
|
||||
|
||||
- a ``partner_id``: the customer
|
||||
- a ``move_type``: it has several `possible values <https://github.com/odoo/odoo/blob/f1f48cdaab3dd7847e8546ad9887f24a9e2ed4c1/addons/account/models/account_move.py#L138-L147>`__
|
||||
- a ``journal_id``: the accounting journal
|
||||
|
||||
This is enough to create an empty invoice.
|
||||
|
||||
.. exercise:: Add the second step of invoice creation.
|
||||
|
||||
Create an empty ``account.move`` in the override of the ``action_sold`` method:
|
||||
|
||||
- the ``partner_id`` is taken from the current ``estate.property``
|
||||
- the ``move_type`` should correspond to a 'Customer Invoice'
|
||||
- the ``journal_id`` must be a ``sale`` journal (when in doubt, have a look
|
||||
`here <https://github.com/odoo/odoo/blob/f1f48cdaab3dd7847e8546ad9887f24a9e2ed4c1/addons/sale/models/sale.py#L534>`__)
|
||||
|
||||
Tips:
|
||||
|
||||
- to create an object, use ``self.env[model_name].create(values)``, where ``values``
|
||||
is a ``dict``.
|
||||
- the ``create`` method doesn't accept recordsets as field values.
|
||||
|
||||
When a property is set to 'Sold', you should now have a new customer invoice created in
|
||||
Invoicing / Customers / Invoices.
|
||||
|
||||
Obviously we don't have any invoice lines so far. To create an invoice line, we need the following
|
||||
information:
|
||||
|
||||
- ``name``: a description of the line
|
||||
- ``quantity``
|
||||
- ``price_unit``
|
||||
|
||||
Moreover, an invoice line needs to be linked to an invoice. The easiest and most efficient way
|
||||
to link a line to an invoice is to include all lines at invoice creation. To do this, the
|
||||
``invoice_line_ids`` field is included in the ``account.move`` creation, which is a
|
||||
:class:`~odoo.fields.One2many`. One2many and Many2many use special 'commands' described in
|
||||
:ref:`reference/orm/models/crud`. This format is a list of triplets executed sequentially, where
|
||||
each triplet is a command to execute on the set of records. Here is a simple example to include
|
||||
a One2many field ``line_ids`` at creation of a ``test.model``::
|
||||
|
||||
def inherited_action(self):
|
||||
self.env["test.model"].create(
|
||||
{
|
||||
"name": "Test",
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"field_1": "value_1",
|
||||
"field_2": "value_2",
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
return super().inherited_action()
|
||||
|
||||
.. exercise:: Add the third step of invoice creation.
|
||||
|
||||
Add two invoice lines during the creation of the ``account.move``. Each property sold will
|
||||
be invoiced following these conditions:
|
||||
|
||||
- 6% of the selling price
|
||||
- an additional 100.00 from administrative fees
|
||||
|
||||
Tip: Add the ``invoice_line_ids`` at creation following the example above.
|
||||
For each line, we need a ``name``, ``quantity`` and ``price_unit``.
|
||||
|
||||
This chapter might be one of the most difficult that has been covered so far, but it is the closest
|
||||
to what Odoo development will be in practice. In the :ref:`next chapter <howto/rdtraining/15_qwebintro>`,
|
||||
we will introduce the templating mechanism used in Odoo.
|
After Width: | Height: | Size: 513 KiB |
133
content/developer/howtos/rdtraining/15_qwebintro.rst
Normal file
@ -0,0 +1,133 @@
|
||||
.. _howto/rdtraining/15_qwebintro:
|
||||
|
||||
===================================
|
||||
Chapter 15: A Brief History Of QWeb
|
||||
===================================
|
||||
|
||||
So far the interface design of our real estate module has been rather limited. Building
|
||||
a list view is straightforward since only the list of fields is necessary. The same holds true
|
||||
for the form view: despite the use of a few tags such as ``<group>`` or ``<page>``, there
|
||||
is very little to do in terms of design.
|
||||
|
||||
However, if we want to give a unique look to our application, it is necessary to go a step
|
||||
further and be able to design new views. Moreover, other features such as PDF reports or
|
||||
website pages need another tool to be created with more flexibility: a templating_ engine.
|
||||
|
||||
You might already be familiar with existing engines such as Jinja (Python), ERB (Ruby) or
|
||||
Twig (PHP). Odoo comes with its own built-in engine: :ref:`reference/qweb`.
|
||||
QWeb is the primary templating engine used by Odoo. It is an XML templating engine and used
|
||||
mostly to generate HTML fragments and pages.
|
||||
|
||||
You probably already have come across the `kanban board`_ in Odoo where the records are
|
||||
displayed in a card-like structure. We will build such a view for our real estate module.
|
||||
|
||||
Concrete Example: A Kanban View
|
||||
===============================
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`reference/views/kanban`.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section a Kanban view of the properties should be created:
|
||||
|
||||
.. image:: 15_qwebintro/media/kanban.png
|
||||
:align: center
|
||||
:alt: Kanban view
|
||||
|
||||
In our estate application, we would like to add a Kanban view to display our properties. Kanban
|
||||
views are a standard Odoo view (like the form and list views), but their structure is much more
|
||||
flexible. In fact, the structure of each card is a mix of form elements (including basic HTML)
|
||||
and QWeb. The definition of a Kanban view is similar to the definition of the list and form
|
||||
views, except that their root element is ``<kanban>``. In its simplest form, a Kanban view
|
||||
looks like:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
|
||||
Let's break down this example:
|
||||
|
||||
- ``<templates>``: defines a list of :ref:`reference/qweb` templates. Kanban views *must* define at
|
||||
least one root template ``kanban-box``, which will be rendered once for each record.
|
||||
- ``<t t-name="kanban-box">``: ``<t>`` is a placeholder element for QWeb directives. In this case,
|
||||
it is used to set the ``name`` of the template to ``kanban-box``
|
||||
- ``<div class="oe_kanban_global_click">``: the ``oe_kanban_global_click`` makes the ``<div>``
|
||||
clickable to open the record.
|
||||
- ``<field name="name"/>``: this will add the ``name`` field to the view.
|
||||
|
||||
.. exercise:: Make a minimal kanban view.
|
||||
|
||||
Using the simple example provided, create a minimal Kanban view for the properties. The
|
||||
only field to display is the ``name``.
|
||||
|
||||
Tip: you must add ``kanban`` in the ``view_mode`` of the corresponding
|
||||
``ir.actions.act_window``.
|
||||
|
||||
Once the Kanban view is working, we can start improving it. If we want to display an element
|
||||
conditionally, we can use the ``t-if`` directive (see :ref:`reference/qweb/conditionals`).
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<kanban>
|
||||
<field name="state"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
<div t-if="record.state.raw_value == 'new'">
|
||||
This is new!
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
|
||||
We added a few things:
|
||||
|
||||
- ``t-if``: the ``<div>`` element is rendered if the condition is true.
|
||||
- ``record``: an object with all the requested fields as its attributes. Each field has
|
||||
two attributes ``value`` and ``raw_value``. The former is formatted according to current
|
||||
user parameters and the latter is the direct value from a :meth:`~odoo.models.Model.read`.
|
||||
|
||||
In the above example, the field ``name`` was added in the ``<templates>`` element, but ``state``
|
||||
is outside of it. When we need the value of a field but don't want to display it in the view,
|
||||
it is possible to add it outside of the ``<templates>`` element.
|
||||
|
||||
.. exercise:: Improve the Kanban view.
|
||||
|
||||
Add the following fields to the Kanban view: expected price, best price, selling price and
|
||||
tags. Pay attention: the best price is only displayed when an offer is received, while the
|
||||
selling price is only displayed when an offer is accepted.
|
||||
|
||||
Refer to the **Goal** of the section for a visual example.
|
||||
|
||||
Let's give the final touch to our view: the properties must be grouped by type by default. You
|
||||
might want to have a look at the various options described in :ref:`reference/views/kanban`.
|
||||
|
||||
.. exercise:: Add default grouping.
|
||||
|
||||
Use the appropriate attribute to group the properties by type by default. You must also prevent
|
||||
drag and drop.
|
||||
|
||||
Refer to the **Goal** of the section for a visual example.
|
||||
|
||||
Kanban views are a typical example of how it is always a good idea to start from an existing
|
||||
view and fine tune it instead of starting from scratch. There are many options and classes
|
||||
available, so... read and learn!
|
||||
|
||||
It is now time to add the
|
||||
:ref:`final touches to our application and submit it on GitHub <howto/rdtraining/16_guidelines_pr>`!
|
||||
|
||||
.. _templating:
|
||||
https://en.wikipedia.org/wiki/Template_processor
|
||||
.. _kanban board:
|
||||
https://en.wikipedia.org/wiki/Kanban_board
|
After Width: | Height: | Size: 28 KiB |
168
content/developer/howtos/rdtraining/16_guidelines_pr.rst
Normal file
@ -0,0 +1,168 @@
|
||||
.. _howto/rdtraining/16_guidelines_pr:
|
||||
|
||||
===============================================
|
||||
Chapter 16: Polish Your Code And Submit Your PR
|
||||
===============================================
|
||||
|
||||
In the previous chapters we saw how to create a fully functional business application. Now we will
|
||||
prepare our code to be shared and learn how to share it.
|
||||
|
||||
Coding guidelines
|
||||
=================
|
||||
|
||||
We will start refactoring the code to match to the Odoo coding guidelines. The guidelines aim
|
||||
to improve the quality of the Odoo Apps code.
|
||||
|
||||
|
||||
**Reference**: you will find the Odoo coding guidelines in :ref:`reference/guidelines`.
|
||||
|
||||
.. exercise:: Polish your code.
|
||||
|
||||
Refactor your code to respect the coding guidelines. Don't forget to respect the module
|
||||
structure, the variable names, the method name convention, the model attribute order and the
|
||||
xml ids.
|
||||
|
||||
Your first Pull Request (PR)
|
||||
============================
|
||||
|
||||
**The following is intended to be used by Odoo staff members only. All repositories mentioned are
|
||||
not accessible for third-parties.**
|
||||
|
||||
Now that your code respects the Odoo code guidelines and is polished up, let's share it with others.
|
||||
|
||||
Configure development repository
|
||||
--------------------------------
|
||||
|
||||
We will start by creating a git development environment for the 'custom' folder, like we did in the
|
||||
beginning for 'odoo' and 'enterprise'. The PR will target the `odoo/technical-training-sandbox` repository.
|
||||
|
||||
To do this the first step is to
|
||||
`fork <https://guides.github.com/activities/forking/>`__ in GitHub
|
||||
`this repository <https://github.com/odoo/technical-training-sandbox/>`__
|
||||
to create your own development repository.
|
||||
|
||||
After you successfully created your development repository, we will configure your existing `$HOME/src/custom`
|
||||
folder to be able to commit your work.
|
||||
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd $HOME/src/custom
|
||||
$ git init
|
||||
$ git remote add training git@github.com:odoo/technical-training-sandbox.git
|
||||
$ git remote add training-dev git@github.com:xyz-odoo/technical-training-sandbox.git #Don't forget to change xyz-odoo to your own GitHub account
|
||||
$ git remote set-url --push training no_push #Avoid pushing directly into the main repository
|
||||
$ git fetch --all --prune
|
||||
$ git branch master training/master
|
||||
$ git checkout master #Two new files will be downloaded README.md and .gitignore
|
||||
|
||||
Note that if your work is already in a :ref:`Git and GitHub configured folder <howto/rdtraining/02_setup/development_repository>`
|
||||
the previous steps are not needed. This is the case when you start working in the
|
||||
'odoo' or 'enterprise' folders.
|
||||
|
||||
Branch, Commit & Push
|
||||
---------------------
|
||||
|
||||
Before creating the PR, a new working branch has to be created and used to commit the code. Afterwards
|
||||
the branch will be pushed to the development repository.
|
||||
|
||||
Create your working branch:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ git checkout -b master-my_first_branch-xyz
|
||||
|
||||
Your branch name must follow the following name strucutre : <targetVersion>-<feature>-<trigram>
|
||||
|
||||
Example: The branch master-sale-fixes-abc on odoo-dev/odoo is a branch containing fixes for the
|
||||
sales app in the odoo/odoo repository, to be deployed in master and done by ABC.
|
||||
|
||||
Commit your code:
|
||||
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ git add .
|
||||
$ git commit
|
||||
|
||||
|
||||
**Everyone reads your commit messages!**
|
||||
|
||||
The commit message is very important, follow the :ref:`Developer guidelines <reference/guidelines/git>`.
|
||||
|
||||
|
||||
Push your new branch to your development repository:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ git push -u master-my_first_branch-xyz
|
||||
|
||||
.. warning:: NEVER use `git push --force` (or `git push -f`) with a branch that does not belong to you
|
||||
or in a stable branch. Never, never, never!
|
||||
|
||||
.. tip:: Remember that you can use `git status` at any point to check the current status of your branch
|
||||
.. tip:: You can create git aliases to shorten some of these commands. Ask your colleagues
|
||||
if they use git aliases and get inspired by their work.
|
||||
|
||||
Create your first PR
|
||||
--------------------
|
||||
|
||||
After pushing your branch to your development repository you will see an output similar to:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
Enumerating objects: 24, done.
|
||||
Counting objects: 100% (24/24), done.
|
||||
Compressing objects: 100% (20/20), done.
|
||||
Writing objects: 100% (23/23), 6.67 KiB | 525.00 KiB/s, done.
|
||||
Total 23 (delta 1), reused 0 (delta 0)
|
||||
remote: Resolving deltas: 100% (1/1), done.
|
||||
remote:
|
||||
remote: Create a pull request for 'master-my_first_branch-xyz' on GitHub by visiting:
|
||||
remote: https://github.com/xyz-odoo/technical-training-sandbox/pull/new/master-my_first_branch-xyz
|
||||
remote:
|
||||
To github.com:xyz-odoo/technical-training-sandbox.git
|
||||
* [new branch] master-my_first_branch-xyz -> master-my_first_branch-xyz
|
||||
Branch 'master-my_first_branch-xyz' set up to track remote branch 'master-my_first_branch-xyz' from 'training-dev'.
|
||||
|
||||
There are two ways to create the PR:
|
||||
|
||||
- Click on the link displayed in the output of the push command.
|
||||
- Open a browser in your development repository `https://github.com/xyz-odoo/technical-training-sandbox`.
|
||||
There will be a button to create a new pull request.
|
||||
|
||||
.. image:: 16_guidelines_pr/media/pr_from_branch.png
|
||||
|
||||
You will notice that your commit message is used as the pull request message. This occurs if you have only 1 commit.
|
||||
If you have multiple commits, you can either make a summary of the commits as the PR message or if there aren't that many
|
||||
commits you can just copy/paste your commit messages as the PR message.
|
||||
|
||||
.. image:: 16_guidelines_pr/media/pr_message.png
|
||||
|
||||
Additional Info for Odoo Staff
|
||||
==============================
|
||||
|
||||
Now that you know the basics, here are some more useful pages if you haven't seen them already:
|
||||
|
||||
- Our `welcome <https://github.com/odoo/enterprise/wiki/Welcome>`__ page has extra info about
|
||||
working at Odoo as well as additional
|
||||
`tutorials <https://github.com/odoo/enterprise/wiki/Welcome#3-technical-training>`__ that are
|
||||
both language and Odoo specific.
|
||||
- Once you are ready to start doing tasks, it is important to note that Odoo follows a
|
||||
`rebasing workflow instead of merging <https://www.atlassian.com/git/tutorials/merging-vs-rebasing>`__
|
||||
for your PRs.
|
||||
`This cheatsheet <https://github.com/odoo/enterprise/wiki/GIT-Cheatsheet#pull-request-flow>`__ describes
|
||||
how to do this flow. If you want to become more of a rebasing master, then
|
||||
`this tutorial <https://www.atlassian.com/git/tutorials/rewriting-history>`__ is extra useful.
|
||||
|
||||
Test on the runbot
|
||||
==================
|
||||
|
||||
Odoo has its own :abbr:`CI (Continuous integration)` server named `runbot <https://runbot.odoo.com/>`__. All
|
||||
commits, branches and PR will be tested to avoid regressions or breaking of the stable versions.
|
||||
All the runs that pass the tests are deployed on their own server with demo data.
|
||||
|
||||
.. exercise:: Play with the runbot.
|
||||
|
||||
Feel free to go to the runbot website and open the last stable version of Odoo to check out all the available
|
||||
applications and functionalities.
|
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 126 KiB |
9
content/developer/howtos/rdtraining/A_i18n.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/A_i18n:
|
||||
|
||||
================================
|
||||
Advanced A: Internationalization
|
||||
================================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
528
content/developer/howtos/rdtraining/B_acl_irrules.rst
Normal file
@ -0,0 +1,528 @@
|
||||
.. _howto/rdtraining/B_acl_irrules:
|
||||
|
||||
================================
|
||||
Advanced B: ACL and Record Rules
|
||||
================================
|
||||
|
||||
.. warning::
|
||||
|
||||
This tutorial assumes you have completed the :ref:`Core Training
|
||||
<howto/rdtraining>`.
|
||||
|
||||
To follow the exercise, it is recommended that you fetch the branch
|
||||
14.0-core from the repository XXX, it
|
||||
contains a version of the module created during the core training we can use
|
||||
as a starting point.
|
||||
|
||||
So far we have mostly concerned ourselves with implementing useful features.
|
||||
However in most business scenarios *security* quickly becomes a concern:
|
||||
currently,
|
||||
|
||||
* Any employee (which is what ``group_user`` stands for) can create, read,
|
||||
update or delete properties, property types, or property tags.
|
||||
* If ``estate_account`` is installed then only agents allowed to interact
|
||||
with invoicing can confirm sales as that's necessary to :ref:`create an
|
||||
invoice <howto/rdtraining/14_other_module/create>`.
|
||||
|
||||
However:
|
||||
|
||||
* We do not want third parties to be able to access properties directly.
|
||||
* Not all our employees may be real-estate agents (e.g. administrative
|
||||
personnel, property managers, ...), we don't want non-agents to see the
|
||||
available properties.
|
||||
* Real-estate agents don't need or get to decide what property types or tags are
|
||||
*available*.
|
||||
* Real-estate agents can have *exclusive* properties, we do not want one agent
|
||||
to be able to manage another's exclusivities.
|
||||
* All real-estate agents should be able to confirm the sale of a property they
|
||||
can manage, but we do not want them to be able to validate or mark as paid
|
||||
any invoice in the system.
|
||||
|
||||
.. note::
|
||||
|
||||
We may actually be fine with some or most of these for a small business.
|
||||
|
||||
Because it's easier for users to disable unnecessary security rules than it
|
||||
is to create them from nothing, it's better to err on the side of caution
|
||||
and limiting access: users can relax that access if necessary or convenient.
|
||||
|
||||
Groups
|
||||
======
|
||||
|
||||
.. seealso::
|
||||
|
||||
The documentation related to this topic can be found in :ref:`the security
|
||||
reference <reference/security>`.
|
||||
|
||||
:ref:`The guidelines <reference/guidelines>` document the format and
|
||||
location of master data items.
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section,
|
||||
|
||||
- We can make employees *real-estate agents* or *real-estate managers*.
|
||||
- The ``admin`` user is a real-estate manager.
|
||||
- We have a new *real-estate agent* employee with no access to invoicing
|
||||
or administration.
|
||||
|
||||
It would not be practical to attach individual security rules to employees any
|
||||
time we need a change so *groups* link security rules and users. They correspond
|
||||
to roles that can be assigned to employees.
|
||||
|
||||
For most Odoo applications [#app]_ a good baseline is to have *user* and
|
||||
*manager* (or administrator) roles: the manager can change the configuration of
|
||||
the application and oversee the entirety of its use while the user can well,
|
||||
use the application [#appuser]_.
|
||||
|
||||
This baseline seems sufficient for us:
|
||||
|
||||
* Real estate managers can configure the system (manage available types and
|
||||
tags) as well as oversee every property in the pipeline.
|
||||
* Real estate agents can manage the properties under their care, or properties
|
||||
which are not specifically under the care of any agent.
|
||||
|
||||
In keeping with Odoo's data-driven nature, a group is no more than a record of
|
||||
the ``res.groups`` model. They are normally part of a module's :ref:`master data
|
||||
<howto/rdtraining/C_data>`, defined in one of the module's data files.
|
||||
|
||||
As simple example `can be found here <https://github.com/odoo/odoo/blob/532c083cbbe0ee6e7a940e2bdc9c677bd56b62fa/addons/hr/security/hr_security.xml#L9-L14>`_.
|
||||
|
||||
.. admonition:: what is the *category_id*?
|
||||
|
||||
``category_id`` a *module category*, it is automatically generated from the
|
||||
:ref:`category defined in module manifest files <howto/rdtraining/03_newapp>`.
|
||||
|
||||
.. todo:: the demo module needs to have the category set, to
|
||||
*Real Estate/Brokerage*, ideally the newapp part would already do
|
||||
that
|
||||
|
||||
.. exercise::
|
||||
|
||||
Create the ``security.xml`` file in the appropriate folder and add it to the
|
||||
``__manifest__.py`` file.
|
||||
|
||||
Add a record creating a group with the id ``estate_group_user``, the name
|
||||
"Agent" and the category ``base.module_category_real_estate_brokerage``.
|
||||
|
||||
Below that, add a record creating a group with the id
|
||||
``estate_group_manager``, the name "Manager" and the category
|
||||
``base.module_category_real_estate_brokerage``. The ``estate_group_manager``
|
||||
group needs to imply ``estate_group_user``.
|
||||
|
||||
.. tip::
|
||||
|
||||
Since we modified data files, remember to restart Odoo and update the
|
||||
module using ``-u estate``.
|
||||
|
||||
If you go to :menuselection:`Settings --> Manage Users` and open the
|
||||
``admin`` user ("Mitchell Admin"), you should see a new section:
|
||||
|
||||
.. figure:: B_acl_irrules/groups.png
|
||||
|
||||
Set the admin user to be a *Real Estate manager*.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Via the web interface, create a new user with only the "real estate agent"
|
||||
access. The user should not have any Invoicing or Administration access.
|
||||
|
||||
Use a private tab or window to log in with the new user (remember to set
|
||||
a password), as the real-estate agent you should only see the real estate
|
||||
application, and possibly the Discuss (chat) application:
|
||||
|
||||
.. figure:: B_acl_irrules/agent.png
|
||||
|
||||
Access Rights
|
||||
=============
|
||||
|
||||
.. seealso:: The documentation related to this topic can be found at
|
||||
:ref:`reference/security/acl`.
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section,
|
||||
|
||||
- Employees who are not at least real-estate agents will not see the
|
||||
real-estate application.
|
||||
- Real-estate agents will not be able to update the property types or tags.
|
||||
|
||||
Access rights were first introduced in :ref:`howto/rdtraining/05_securityintro`.
|
||||
|
||||
Access rights are a way to give users access to models *via* groups: associate
|
||||
an access right to a group, then all users with that group will have the access.
|
||||
|
||||
For instance we don't want real-estate agents to be able to modify what property
|
||||
types are available, so we would not link that access to the "user" group.
|
||||
|
||||
Access rights can only give access, they can't remove it: when access is
|
||||
checked, the system looks to see if *any* access right associated with the user
|
||||
(via any group) grants that access.
|
||||
|
||||
====== ====== ==== ====== ======
|
||||
group create read update delete
|
||||
------ ------ ---- ------ ------
|
||||
A X X
|
||||
B X
|
||||
C X
|
||||
====== ====== ==== ====== ======
|
||||
|
||||
A user with the groups A and C will be able to do anything but delete the object
|
||||
while one with B and C will be able to read or update, but not search or read.
|
||||
|
||||
.. note::
|
||||
|
||||
* The group of an access right can be ommitted, this means the ACL applies
|
||||
to *every user*, this is a useful but risky fallback as depending on the
|
||||
applications installed it can grant even non-users access to the model.
|
||||
* If no access right applies to a user, they are not granted access
|
||||
(default-deny).
|
||||
* If a menu item points to a model to which a user doesn't have acces and
|
||||
has no submenus which the user can see, the menu will not be displayed.
|
||||
|
||||
.. exercise:: Update the access rights file to:
|
||||
|
||||
* Give full access to all objects to your Real Estate Manager group.
|
||||
* Give agents (real estate users) only read access to types and tags.
|
||||
* Give nobody the right to delete properties.
|
||||
* Check that your agent user is not able to alter types or tags, or to
|
||||
delete properties, but that they can otherwise create or update
|
||||
properties.
|
||||
|
||||
.. warning::
|
||||
|
||||
Remember to give different xids to your ``ir.model.access`` records
|
||||
otherwise they will overwrite one another.
|
||||
|
||||
Since the "demo" user was not made a real-estate agent or manager, they should
|
||||
not even be able to see the real-estate application. Use a private tab or window
|
||||
to check for this (the "demo" user has the password "demo").
|
||||
|
||||
Access Rules
|
||||
============
|
||||
|
||||
.. seealso:: The documentation related to this topic can be found at
|
||||
:ref:`reference/security/rules`.
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, agents will not be able to see the properties
|
||||
exclusive to their colleagues; but managers will still be able to see
|
||||
everything.
|
||||
|
||||
Access rights can grant access to an entire model but often we need to be
|
||||
more specific: while an agent can interact with properties in general we may not
|
||||
want them to update or even see properties managed by one of their colleagues.
|
||||
|
||||
Access *rules* provide that precision: they can grant or reject access to
|
||||
individual records:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="rule_id" model="ir.rule">
|
||||
<field name="name">A description of the rule's role</field>
|
||||
<field name="model_id" ref="model_to_manage"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="domain_force">[
|
||||
'|', ('user_id', '=', user.id),
|
||||
('user_id', '=', False)
|
||||
]</field>
|
||||
</record>
|
||||
|
||||
The :ref:`reference/orm/domains` is how access is managed: if the record passes
|
||||
then access is granted, otherwise access is rejected.
|
||||
|
||||
.. tip::
|
||||
|
||||
Because rules tends to be rather complex and not created in bulk, they're
|
||||
usually created in XML rather than the CSV used for access rights.
|
||||
|
||||
The rule above:
|
||||
|
||||
* Only applies to the "create", "update" (write) and "delete" (unlink)
|
||||
operations: here we want every employee to be able to see other users' records
|
||||
but only the author / assignee can update a record.
|
||||
* Is :ref:`non-global <reference/security/rules/global>` so we can provide an
|
||||
additional rule for e.g. managers.
|
||||
* Allows the operation if the current user (``user.id``) is set (e.g. created,
|
||||
or is assigned) on the record, or if the record has no associated user at all.
|
||||
|
||||
.. note::
|
||||
|
||||
If no rule is defined or applies to a model and operation, then the
|
||||
operation is allowed (*default-allow*), this can have odd effects
|
||||
if access rights are not set up correctly (are too permissive).
|
||||
|
||||
.. exercise::
|
||||
|
||||
Define a rule which limits agents to only being able to see or modify
|
||||
properties which have no salesperson, or for which they are the salesperson.
|
||||
|
||||
You may want to create a second real-estate agent user, or create a few
|
||||
properties for which the salesperson is a manager or some other user.
|
||||
|
||||
Verify that your real estate manager(s) can still see all properties. If
|
||||
not, why not? Remember:
|
||||
|
||||
The ``estate_group_manager`` group needs to imply ``estate_group_user``.
|
||||
|
||||
Security Override
|
||||
=================
|
||||
|
||||
Bypassing Security
|
||||
------------------
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, agents should be able to confirm property sales
|
||||
without needing invoicing access.
|
||||
|
||||
If you try to mark a property as "sold" as the real estate agent, you should get
|
||||
an access error:
|
||||
|
||||
.. figure:: B_acl_irrules/error.png
|
||||
|
||||
This happens because ``estate_account`` tries to create an invoice during the
|
||||
process, but creating an invoice requires the right to all invoice management.
|
||||
|
||||
We want agents to be able to confirm a sale without them having full invoicing
|
||||
access, which means we need to *bypass* the normal security checks of Odoo in
|
||||
order to create an invoice *despite* the current user not having the right to
|
||||
do so.
|
||||
|
||||
There are two main ways to bypass existing security checks in Odoo, either
|
||||
wilfully or as a side-effect:
|
||||
|
||||
* The ``sudo()`` method will create a new recorset in "sudo mode", this ignores
|
||||
all access rules and access rights (although hard-coded group and user checks
|
||||
may still apply).
|
||||
* Performing raw SQL queries will bypass access rules and access rights as a
|
||||
side-effect of bypassing the ORM itself.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Update ``estate_account`` to bypass access rights and rules when creating
|
||||
the invoice.
|
||||
|
||||
.. danger::
|
||||
|
||||
These features should generally be avoided, and only used with extreme care,
|
||||
after having checked that the current user and operation should be able to
|
||||
bypass normal access rights validation.
|
||||
|
||||
Operations performed in such modes should also rely on user input as little
|
||||
as possible, and should validate it to the maximum extent they can.
|
||||
|
||||
Programmatically checking security
|
||||
----------------------------------
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, the creation of the invoice should be resilient
|
||||
to security issues regardless to changes to ``estate``.
|
||||
|
||||
In Odoo, access rights and access rules are only checked *when performing data
|
||||
access via the ORM* e.g. creating, reading, searching, writing, or unlinking a
|
||||
record via ORM methods. Other methods do *not* necessarily check against any
|
||||
sort of access rights.
|
||||
|
||||
In the previous section, we bypassed the access rules when creating the invoice
|
||||
in ``action_sold``. This bypass can be reached by any user without any access
|
||||
right being checked:
|
||||
|
||||
- Add a print to ``action_sold`` in ``estate_account`` before the creation of
|
||||
the invoice (as creating the invoice accesses the property, therefore triggers
|
||||
an ACL check) e.g.::
|
||||
|
||||
print(" reached ".center(100, '=')
|
||||
|
||||
- Execute ``bypass.py`` in ``estate_account``, giving it the name of your
|
||||
database, and the name of your version of ``action_sold`` (unless you named it
|
||||
``action_sold`` then it's fine)
|
||||
|
||||
You should see ``reached`` in your Odoo log, followed by an access error.
|
||||
|
||||
.. danger:: Just because you're already in Python code does not mean any access
|
||||
right or rule has or will be checked.
|
||||
|
||||
*Currently* the accesses are implicitly checked by accessing data on ``self`` as
|
||||
well as calling ``super()`` (which does the same and *updates* ``self``),
|
||||
triggering access errors and cancelling the transaction "uncreating" our
|
||||
invoice.
|
||||
|
||||
*However* if this changes in the future, or we add side-effects to the method
|
||||
(e.g. reporting the sale to a government agency), or bugs are introduced in
|
||||
``estate``, ... it would be possible for non-agents to trigger operations they
|
||||
should not have access to.
|
||||
|
||||
Therefore when performing non-CRUD operations, or legitimately bypassing the
|
||||
ORM or security, or when triggering other side-effects, it is extremely
|
||||
important to perform *explicit security checks*.
|
||||
|
||||
Explicit security checks can be performed by:
|
||||
|
||||
* Checking who the current user is (``self.env.user``) and match them against
|
||||
specific models or records.
|
||||
* Checking that the current user has specific groups hard-coded to allow or deny
|
||||
an operation (``self.env.user.has_group``).
|
||||
* Calling the ``check_access_rights(operation)`` method on a recorset, this
|
||||
verifies whether the current user has access to the model itself.
|
||||
* Calling ``check_access_rule(operations)`` on a non-empty recorset, this
|
||||
verifies that the current user is allowed to perform the operation on *every*
|
||||
record of the set.
|
||||
|
||||
.. warning:: Checking access rights and checking access rules are separate
|
||||
operations, if you're checking access rules you usually want to
|
||||
also check access rights beforehand.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Before creating the invoice, use ``check_access_rights`` and
|
||||
``check_access_rule`` to ensure that the current user can update properties
|
||||
in general, and this specific property in particular.
|
||||
|
||||
Re-run the bypass script, check that the error occurs before the print.
|
||||
|
||||
.. _howto/rdtraining/B_acl_irrules/multicompany:
|
||||
|
||||
Multi-company security
|
||||
======================
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`reference/howtos/company` for an overview of multi-company facilities
|
||||
in general, and :ref:`multi-company security rules <howto/company/security>`
|
||||
this in particular.
|
||||
|
||||
Documentation on rules in general can, again, be found at
|
||||
:ref:`reference/security/rules`.
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, agents should only have access to properties
|
||||
of their agency (or agencies).
|
||||
|
||||
For one reason or an other we might need to manage our real-estate business
|
||||
as multiple companies e.g. we might have largely autonomous agencies, or a
|
||||
franchise setup, or multiple brands (possibly from having acquired other
|
||||
real-estate businesses) which remain legally or financially separate from one
|
||||
another.
|
||||
|
||||
Odoo can be used to manage multiple companies inside the same system, however
|
||||
the actual handling is up to individual modules: Odoo itself provides the tools
|
||||
to manage the issue like company-dependent fields and *multi-company rules*,
|
||||
which is what we're going to concern outselves with.
|
||||
|
||||
We want different agencies to be "siloed" from one another, with properties
|
||||
belonging to a given agency and users (whether agents or managers) only able to
|
||||
see properties linked to their agency.
|
||||
|
||||
As before, because this is based on non-trivial records it's easier for a user
|
||||
to relax rules than to tighten them so it makes sense to default to a
|
||||
relatively stronger security model.
|
||||
|
||||
Multi-company rules are simply access rules based on the ``company_ids`` or
|
||||
``company_id`` fields:
|
||||
|
||||
* ``company_ids`` is all the companies to which the current user has access
|
||||
* ``company_id`` is the currently active company (the one the user is currently
|
||||
working in / for).
|
||||
|
||||
Multi-company rules will *usually* use the former i.e. check if the record is
|
||||
associated with *one* of the companies the user has access to:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.rule" id="hr_appraisal_plan_comp_rule">
|
||||
<field name="name">Appraisal Plan multi-company</field>
|
||||
<field name="model_id" ref="model_hr_appraisal_plan"/>
|
||||
<field name="domain_force">[
|
||||
'|', ('company_id', '=', False),
|
||||
('company_id', 'in', company_ids)
|
||||
]</field>
|
||||
</record>
|
||||
|
||||
.. danger::
|
||||
|
||||
Multi-company rules are usually :ref:`global <reference/security/rules/global>`,
|
||||
otherwise there is a high risk that additional rules would allow bypassing
|
||||
the muti-company rules.
|
||||
|
||||
.. exercise::
|
||||
|
||||
* Add a ``company_id`` field to ``estate.property``, it should be required
|
||||
(we don't want agency-less properties), and should default to the current
|
||||
user's current company.
|
||||
* Create a new company, with a new estate agent in that company.
|
||||
* The manager should be a member of both companies.
|
||||
* The old agent should only be a member of the old company.
|
||||
* Create a few properties in each company (either use the company selector
|
||||
as the manager or use the agents). Unset the default salesman to avoid
|
||||
triggering *that* rule.
|
||||
* All agents can see all companies, which is not desirable, add the record
|
||||
rule restricting this behaviour.
|
||||
|
||||
.. warning:: remember to ``--update`` your module when you change its model or
|
||||
data
|
||||
|
||||
Visibility != security
|
||||
======================
|
||||
|
||||
.. admonition:: **Goal**
|
||||
|
||||
At the end of this section, real-estate agents should not see the Settings
|
||||
menu of the rea-estate application, but should still be able to set the
|
||||
property type or tags.
|
||||
|
||||
Specific Odoo models can be associated directly with groups (or companies, or
|
||||
users). It is important to figure out whether this association is a *security*
|
||||
or a *visibility* feature before using it:
|
||||
|
||||
* *Visibility* features mean a user can still access the model or record
|
||||
otherwise, either through an other part of the interface or by :doc:`perform
|
||||
operations remotely using RPC <../../webservices/odoo>`, things might just not be
|
||||
visible in the web interface in some contexts.
|
||||
* *Security* features mean a user can not access records, fields or operations.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
* Groups on *model fields* (in Python) are a security feature, users outside the
|
||||
group will not be able to retrieve the field, or even know it exists.
|
||||
|
||||
Example: in server actions, `only system users can see or update Python code
|
||||
<https://github.com/odoo/odoo/blob/7058e338a980268df1c502b8b2860bdd8be9f727/odoo/addons/base/models/ir_actions.py#L414-L417>`_.
|
||||
* Groups on *view elements* (in XML) are a visibility feature, users outside the
|
||||
group will not be able to see the element or its content in the form but they
|
||||
will otherwise be able to interact with the object (including that field).
|
||||
|
||||
Example: `only managers have an immediate filter to see their teams' leaves
|
||||
<https://github.com/odoo/odoo/blob/8e19904bcaff8300803a7b596c02ec45fcf36ae6/addons/hr_holidays/report/hr_leave_reports.xml#L16>`_.
|
||||
* Groups on menus and actions are visibility features, the menu or action will
|
||||
not be shown in the interface but that doesn't prevent directly interacting
|
||||
with the underlying object.
|
||||
|
||||
Example: `only system administrators can see the elearning settings menu
|
||||
<https://github.com/odoo/odoo/blob/ff828a3e0c5386dc54e6a46fd71de9272ef3b691/addons/website_slides/views/website_slides_menu_views.xml#L64-L69>`_.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Real Estate agents can not add property types or tags, and can see their
|
||||
options from the Property form view when creating it.
|
||||
|
||||
The Settings menu just adds noise to their interface, it should only be
|
||||
visible to managers.
|
||||
|
||||
Despite not having access to the Property Types and Property Tags menus anymore,
|
||||
agents can still access the underlying objects since they can still select
|
||||
tags or a type to set on their properties.
|
||||
|
||||
.. [#app] An Odoo Application is a group of related modules covering a business
|
||||
area or field, usually composed of a base module and a number of
|
||||
expansions on that base to add optional or specific features, or link
|
||||
to other business areas.
|
||||
|
||||
.. [#appuser] For applications which would be used by most or every employees,
|
||||
the "application user" role might be done away with and its
|
||||
abilities granted to all employees directly e.g. generally all
|
||||
employees can submit expenses or take time off.
|
BIN
content/developer/howtos/rdtraining/B_acl_irrules/agent.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
content/developer/howtos/rdtraining/B_acl_irrules/error.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
content/developer/howtos/rdtraining/B_acl_irrules/groups.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
425
content/developer/howtos/rdtraining/C_data.rst
Normal file
@ -0,0 +1,425 @@
|
||||
.. _howto/rdtraining/C_data:
|
||||
|
||||
================================
|
||||
Advanced C: Master and Demo Data
|
||||
================================
|
||||
|
||||
.. tip:: This tutorial assumes you followed the Core Training.
|
||||
|
||||
To do the exercise, fetch the branch 14.0-core from the repository XXX.
|
||||
It contains a basic module we will use as a starting point
|
||||
|
||||
Data Types
|
||||
==========
|
||||
|
||||
Master Data
|
||||
-----------
|
||||
|
||||
Master data is usually part of the technical or business requirements for the module. In other
|
||||
words, such data is often necessary for the module to work properly. This data will always be
|
||||
installed when installing the module.
|
||||
|
||||
We already met technical data previously since we have defined
|
||||
:ref:`security rules<howto/rdtraining/N_security>`, :ref:`views<reference/views>` and
|
||||
:ref:`actions<reference/actions>`. Those are one kind of master data.
|
||||
|
||||
On top of technical data, business data can be defined: countries, currencies, units of measure but
|
||||
also complete country localization (legal reports, tax definitions, chart of account), and much
|
||||
more...
|
||||
|
||||
Demo Data
|
||||
---------
|
||||
|
||||
Next to master data which are requirements for the modules to work properly, we also like having
|
||||
data for demonstration purpose setups:
|
||||
|
||||
* Help the sales representatives to make their demos quickly.
|
||||
* Have a set of working data for developers to test the new features and see what it looks like
|
||||
with something they might not have created without it.
|
||||
* Test that the data is loaded correctly, without raising an error.
|
||||
* Be ready to use most of the features quickly when creating a new database.
|
||||
|
||||
Demo data is automatically loaded when you start the server if you didn't say explicitly you don't
|
||||
want it. This can be done in the database manager or with the command line.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./odoo-bin -h
|
||||
Usage: odoo-bin [options]
|
||||
|
||||
Options:
|
||||
--version show program's version number and exit
|
||||
-h, --help show this help message and exit
|
||||
|
||||
Common options:
|
||||
[...]
|
||||
--without-demo=WITHOUT_DEMO
|
||||
disable loading demo data for modules to be installed
|
||||
(comma-separated, use "all" for all modules). Requires
|
||||
-d and -i. Default is none
|
||||
[...]
|
||||
|
||||
$ ./odoo-bin --addons-path=... -d db -i account --without-demo=all
|
||||
|
||||
Data Declaration
|
||||
================
|
||||
|
||||
Manifest
|
||||
--------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`Module Manifests<reference/module/manifest>`.
|
||||
|
||||
The data is declared either in CSV either in XML.
|
||||
Each file containing data must be added in the manifest for them to be loaded.
|
||||
|
||||
The keys to use in the manifest to add new data are ``data`` for the master data and ``demo`` for
|
||||
the demo data. Both values should be a list of strings representing the relative path to the files
|
||||
declaring the data.
|
||||
|
||||
Usually, the demo data is set in a ``demo`` folder, the views and actions are put in a ``views``
|
||||
folder, the security related data is put in a ``security`` folder, and the other data is set in a
|
||||
``data`` folder.
|
||||
|
||||
If your work tree looks like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
estate
|
||||
├── data
|
||||
│ └── master_data.xml
|
||||
├── demo
|
||||
│ └── demo_data.xml
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── security
|
||||
│ └── ir.model.access.csv
|
||||
├── views
|
||||
│ └── estate_property_offer_views.xml
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
Your manifest should look like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
"name": "Real Estate",
|
||||
"depends": [
|
||||
...
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv", # CSV and XML files are loaded at the same place
|
||||
"views/estate_property_offer_views.xml", # Views are data too
|
||||
"data/master_data.xml", # Split the data in multiple files depending on the model
|
||||
],
|
||||
"demo": [
|
||||
"demo/demo_data.xml",
|
||||
]
|
||||
"application": True,
|
||||
}
|
||||
|
||||
CSV
|
||||
---
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`CSV data files<reference/data/csvdatafiles>`.
|
||||
|
||||
The easiest way to declare simple data is by using the CSV format. This is however limited in terms
|
||||
of features: use it for long lists of simple models, but prefer XML in the other cases.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
id,field_a,field_b,related_id:id
|
||||
id1,valueA1,valueB1,module.relatedid
|
||||
id2,valueA2,valueB2,module.relatedid
|
||||
|
||||
.. tip:: Your IDE has probably an extension to have a syntax highlighting of the CSV files
|
||||
|
||||
* `Atom <https://atom.io/packages/rainbow-csv>`__.
|
||||
* `PyCharm/IntelliJ <https://plugins.jetbrains.com/plugin/10037-csv-plugin>`__.
|
||||
* `Vim <https://github.com/mechatroner/rainbow_csv>`__.
|
||||
* `Visual Studio <https://marketplace.visualstudio.com/items?itemName=mechatroner.rainbow-csv>`__.
|
||||
|
||||
.. exercise:: Add some standard Real Estate Property Types for the `estate` module: Residential,
|
||||
Commercial, Industrial and Land. These should always be installed.
|
||||
|
||||
XML
|
||||
---
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`Data Files<reference/data>`.
|
||||
|
||||
When the data to create is a bit more complex it can be useful, or even needed, to do it in XML.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="field_a">valueA1</field>
|
||||
<field name="field_b">valueB1</field>
|
||||
</record>
|
||||
|
||||
<record id="id2" model="tutorial.example">
|
||||
<field name="field_a">valueA2</field>
|
||||
<field name="field_b">valueB2</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. exercise:: Create some demo data for the `estate` module.
|
||||
|
||||
================== ==================== ======================
|
||||
Field Values Values
|
||||
================== ==================== ======================
|
||||
name Big Villa Trailer home
|
||||
state New Canceled
|
||||
description A nice and big villa Home in a trailer park
|
||||
postcode 12345 54321
|
||||
date_availability 2020-02-02 1970-01-01
|
||||
expected_price 1,600,000 100,000
|
||||
selling_price 120,000
|
||||
bedrooms 6 1
|
||||
living_area 100 10
|
||||
facades 4 4
|
||||
garage True False
|
||||
garden True
|
||||
garden_area 100000
|
||||
garden_orientation South
|
||||
================== ==================== ======================
|
||||
|
||||
Data Extension
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
During the Core Training, we saw in the :ref:`howto/rdtraining/13_inheritance` chapter we could
|
||||
inherit (extend) an existing view. This was a special case of data extension: any data can be
|
||||
extended in a module.
|
||||
|
||||
When you are adding new fields to an existing model in a new module, you might want to populate
|
||||
those fields on the records created in the modules you are depending on. This is done by giving the
|
||||
`xml_id` of the record you want to extend. It won't replace it, in this case we will set the
|
||||
``field_c`` to the given value for both records.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="field_c">valueC1</field>
|
||||
</record>
|
||||
|
||||
<record id="id2" model="tutorial.example">
|
||||
<field name="field_c">valueC2</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
||||
``ref``
|
||||
~~~~~~~
|
||||
|
||||
Related fields can be set using the ``ref`` key. The value of that key is the ``xml_id`` of the
|
||||
record you want to link. Remember the ``xml_id`` is composed of the name of the module where the
|
||||
data is first declared, followed by a dot, followed by the ``id`` of the record (just the ``id``
|
||||
works too if you are in the module declaring it).
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="related_id" ref="module.relatedid"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. exercise:: Create some demo data offers for the properties you created.
|
||||
|
||||
Create offers using the partners defined in ``base``
|
||||
|
||||
============== ========= ======= ========
|
||||
Partner Estate Price Validity
|
||||
============== ========= ======= ========
|
||||
Azure Interior Big Villa 10000 14
|
||||
Azure Interior Big Villa 1500000 14
|
||||
Deco Addict Big Villa 1500001 14
|
||||
============== ========= ======= ========
|
||||
|
||||
.. exercise:: Both properties should be Residential properties.
|
||||
|
||||
``eval``
|
||||
~~~~~~~~
|
||||
|
||||
The value to assign to a field is not always a simple string and you might need to compute it.
|
||||
It can also be used to optimize the insertion of related values, or because a constraint forces you
|
||||
to add the related values in batch. See ::ref:`Add X2many fields <howto/rdtraining/C_data/x2m>`.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="year" eval="datetime.now().year+1"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. exercise:: The offers you added should always be in a date relative to the installation of the
|
||||
module.
|
||||
|
||||
``search``
|
||||
~~~~~~~~~~
|
||||
|
||||
Sometimes, you need to call the ORM to do a ``search``. This is not feasible with the CSV format.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="account.move.line">
|
||||
<field name="account_id" search="[
|
||||
('user_type_id', '=', ref('account.data_account_type_direct_costs')),
|
||||
('company_id', '=', obj().env.company.id)]
|
||||
"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
In this code snippet, it is needed because the master data actually depends on the localization
|
||||
installed.
|
||||
|
||||
``function``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
You might also need to execute python code when loading the data.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<function model="tutorial.example" name="action_validate">
|
||||
<value eval="[ref('demo_invoice_1')]"/>
|
||||
</function>
|
||||
|
||||
.. exercise:: Validate one of the demo data offers by using the "Accept Offer" button. Refuse the
|
||||
others.
|
||||
|
||||
|
||||
.. _howto/rdtraining/C_data/x2m:
|
||||
|
||||
Add X2many fields
|
||||
-----------------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`Common ORM methods<reference/orm/models/crud>`.
|
||||
|
||||
If you need to add related data in a One2many or a Many2many field, you can do so by using the
|
||||
common ORM methods.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="related_ids" eval="[
|
||||
(0, 0, {
|
||||
'name': 'My name',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Your name',
|
||||
}),
|
||||
(4, ref('model.xml_id')),
|
||||
]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
id,parent_id:id,name
|
||||
"child1","module.parent","Name1"
|
||||
"child2","module.parent","Name2"
|
||||
"child3","module.parent","Name3"
|
||||
|
||||
.. exercise:: Create one new Property, but this time with some offers created directly inside the
|
||||
One2many field linking to the Offers.
|
||||
|
||||
Accessing the data
|
||||
==================
|
||||
|
||||
.. warning:: You should never access demo data outside of the demo data declaration, not even in
|
||||
tests.
|
||||
|
||||
There are multiple ways to access the master/demo data.
|
||||
|
||||
In python code, you can use the ``env.ref(self, xml_id, raise_if_not_found=True)`` method. It
|
||||
returns the recordset linked to the ``xml_id`` you specify.
|
||||
|
||||
In XML, you can use the `ref` key like this
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<record id="id1" model="tutorial.example">
|
||||
<field name="related_id" ref="module.relatedid"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
It will call the ref method, and store the id of the record returned on the field ``related_id`` of
|
||||
the record of type ``tutorial.example`` with id ``id1``.
|
||||
|
||||
In CSV, the title of the column must be suffixed with ``:id`` or ``/id``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
id,parent_id:id,name
|
||||
"child1","module.parent","Name1"
|
||||
"child2","module.parent","Name2"
|
||||
"child3","module.parent","Name3"
|
||||
|
||||
In SQL, it is more complicated, see :ref:`the advanced section<howto/rdtraining/C_data/xml_id>`.
|
||||
|
||||
.. warning:: Data can always be deleted by the user. Always code defensively, taking this into
|
||||
account.
|
||||
|
||||
|
||||
|
||||
|
||||
Advanced
|
||||
========
|
||||
|
||||
.. _howto/rdtraining/C_data/xml_id:
|
||||
|
||||
What is the XML id?
|
||||
-------------------
|
||||
|
||||
Because we don't want a column ``xml_id`` in every single SQL table of the database, we need a
|
||||
mechanism to store it. This is done with the ``ir.model.data`` model.
|
||||
|
||||
It contains the name of the record (the ``xml_id``) along with the module in which it is defined,
|
||||
the model defining it, and the id of it.
|
||||
|
||||
No update
|
||||
---------
|
||||
|
||||
The records created with the ``noupdate`` flag won't be updated when upgrading the module that
|
||||
created them, but it will be created if it didn't exist yet.
|
||||
|
||||
.. note:: ``odoo-bin -i module`` will bypass this setting and always load the data. But normally
|
||||
one shouldn't do this on a production database.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo noupdate="1">
|
||||
<record id="id1" model="model">
|
||||
<field name="fieldA" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
||||
Import as SQL
|
||||
-------------
|
||||
|
||||
In some cases, it makes sense to do the import directly in SQL. This is however discouraged as it
|
||||
bypasses all the features of the ORM, computed fields (including metadata) and python constraints.
|
||||
|
||||
.. note:: Generally using raw SQL also bypasses ACLs and increases the risks of injections.
|
||||
|
||||
**Reference**: :ref:`Security in Odoo<reference/security>`
|
||||
|
||||
* It can help to speed the import time by a lot
|
||||
`with huge files <https://github.com/odoo/enterprise/blob/d46cceef8c594b9056d0115edb7169e207a5986f/product_unspsc/hooks.py#L19>`__.
|
||||
* For more complex imports like for the
|
||||
`translations <https://github.com/odoo/odoo/blob/e1f8d549895cd9c459e6350430f30d541d02838a/odoo/addons/base/models/ir_translation.py#L24>`__.
|
||||
* It can be necessary to
|
||||
`initialize the database <https://github.com/odoo/odoo/blob/e1f8d549895cd9c459e6350430f30d541d02838a/odoo/addons/base/data/base_data.sql>`__.
|
9
content/developer/howtos/rdtraining/D_mixins.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/D_mixins:
|
||||
|
||||
==================
|
||||
Advanced D: Mixins
|
||||
==================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
293
content/developer/howtos/rdtraining/E_unittest.rst
Normal file
@ -0,0 +1,293 @@
|
||||
.. _howto/rdtraining/E_unittest:
|
||||
|
||||
=============================
|
||||
Advanced E: Python Unit Tests
|
||||
=============================
|
||||
|
||||
.. tip:: This tutorial assumes you followed the Core Training.
|
||||
|
||||
To do the exercise, fetch the branch 14.0-core from the repository XXX.
|
||||
It contains a basic module we will use as a starting point
|
||||
|
||||
**Reference**:
|
||||
`Odoo's Test Framework: Learn Best Practices <https://www.youtube.com/watch?v=JEIscps0OOQ>`__
|
||||
(Odoo Experience 2020) on Youtube.
|
||||
|
||||
Writing tests is a necessity for multiple reasons. Here is a non exhaustive list:
|
||||
|
||||
* Ensure it will not be broken in the future
|
||||
* Define the scope of your code
|
||||
* Give examples of use cases
|
||||
* It is one way to technically document the code
|
||||
* Help you develop by defining your goal before working towards it
|
||||
|
||||
Running Tests
|
||||
=============
|
||||
|
||||
Before knowing how to write tests, we need to know how to run them.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo-bin -h
|
||||
Usage: odoo-bin [options]
|
||||
|
||||
Options:
|
||||
--version show program's version number and exit
|
||||
-h, --help show this help message and exit
|
||||
|
||||
[...]
|
||||
|
||||
Testing Configuration:
|
||||
--test-file=TEST_FILE
|
||||
Launch a python test file.
|
||||
--test-enable Enable unit tests.
|
||||
--test-tags=TEST_TAGS
|
||||
Comma-separated list of spec to filter which tests to
|
||||
execute. Enable unit tests if set.
|
||||
A filter spec has the format:
|
||||
[-][tag][/module][:class][.method]
|
||||
The '-' specifies if we want to include or exclude
|
||||
tests matching this spec. The
|
||||
tag will match tags added on a class with a @tagged
|
||||
decorator. By default tag value is 'standard' when not
|
||||
given on include mode. '*' will match all tags. Tag
|
||||
will also match module name (deprecated, use /module)
|
||||
The module, class, and method will respectively match
|
||||
the module name, test class name and test method name.
|
||||
examples: :TestClass.test_func,/test_module,external
|
||||
--screencasts=DIR Screencasts will go in DIR/{db_name}/screencasts.
|
||||
--screenshots=DIR Screenshots will go in DIR/{db_name}/screenshots.
|
||||
Defaults to /tmp/odoo_tests.
|
||||
|
||||
$ # run all the tests of account, and modules installed by account
|
||||
$ # the dependencies already installed are not tested
|
||||
$ # this takes some time because you need to install the modules, but at_install
|
||||
$ # and post_install are respected
|
||||
$ odoo-bin -i account --test-enable
|
||||
$ # run all the tests in this file
|
||||
$ odoo-bin --test-file=addons/account/tests/test_account_move_entry.py
|
||||
$ # test tags can help you filter quite easily
|
||||
$ odoo-bin --test-tags=/account:TestAccountMove.test_custom_currency_on_account_1
|
||||
|
||||
Integration Bots
|
||||
================
|
||||
|
||||
.. note:: This section is only for Odoo employees and people that are contributing to
|
||||
`github.com/odoo`. We highly recommend having your own CI if it is not the case.
|
||||
|
||||
When a test is written, it is important to make sure it always passes when modifications are
|
||||
applied to the source code. To automatize this task, we use a development practice called
|
||||
Continuous Integration (CI). This is why we have some bots running all the tests at different
|
||||
moments.
|
||||
Whether you are working at Odoo or not, if you are trying to merge something inside `odoo/odoo`,
|
||||
`odoo/enterprise`, `odoo/upgrade` or on odoo.sh, you will have to go through the CI. If you are
|
||||
working on another project, you should think of adding your own CI.
|
||||
|
||||
Runbot
|
||||
------
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
`Runbot FAQ <https://runbot.odoo.com/doc>`__.
|
||||
|
||||
Most of the tests are run on `Runbot <https://runbot.odoo.com>`__ every time a commit is pushed on
|
||||
GitHub.
|
||||
|
||||
You can see the state of a commit/branch by filtering on the runbot dashboard.
|
||||
|
||||
A **bundle** is created for each branch. A bundle consists of a configuration and contains the
|
||||
batches.
|
||||
|
||||
A **batch** is a set of builds, depending on the parameters of the bundle.
|
||||
A batch is green (i.e. passes the tests) if all the builds are green.
|
||||
|
||||
A **build** is when we launch a server. It can be divided in sub-builds. Usually there are builds
|
||||
for the community version, the enterprise version (only if there is an enterprise branch but you
|
||||
can force the build), and the migration of the branch.
|
||||
A build is green if every sub-build is green.
|
||||
|
||||
A **sub-build** only does some parts of what a full build does. It is used to speed up the CI
|
||||
process. Generally it is used to split the post install tests in 4 parallel instances.
|
||||
A sub-build is green if all the tests are passing and there are no errors/warnings logged.
|
||||
|
||||
.. note::
|
||||
* All tests are run whatever the modifications done. Correcting a typo in an error message or
|
||||
refactoring a whole module triggers the same tests. It will install all the modules. This means
|
||||
something might not work if Runbot green but your changes depend on something you don't depend
|
||||
on.
|
||||
* The localization modules (i.e. country-specific modules) are not installed on Runbot (except
|
||||
the generic one), some modules with external dependencies can be excluded also.
|
||||
* There is a nightly build running additional tests, like module operations, localization, single
|
||||
module installs, multi-builds for nondeterministic bugs, etc.
|
||||
These are not kept in the standard CI to shorten the time of execution.
|
||||
|
||||
You can also login on a build built by Runbot. There are 3 users usable: `admin`, `demo` and
|
||||
`portal`. The password is the same as the login. This is useful to quickly test things on different
|
||||
versions without having to build it locally. The full logs are also available; these are used for
|
||||
monitoring.
|
||||
|
||||
Robodoo
|
||||
-------
|
||||
|
||||
You will most likely have to gain a little bit more experience before having the rights to summon
|
||||
robodoo, but here are a few remarks anyways.
|
||||
|
||||
Robodoo is the guy spamming the CI status as tags on your PRs, but he is also the guy that kindly
|
||||
integrates your commits on the main repositories.
|
||||
|
||||
When the last batch is green, the reviewer can ask robodoo to merge your PR (actually it is more
|
||||
a `rebase` than a `merge`). It will then go to the mergebot.
|
||||
|
||||
Mergebot
|
||||
--------
|
||||
|
||||
`Mergebot <https://mergebot.odoo.com>`__ is the last testing phase before merging a PR.
|
||||
|
||||
It will take the commits in your branch not yet present on the target, stage it and rerun the tests
|
||||
one more time, including the enterprise version even if you are only changing something in
|
||||
community.
|
||||
|
||||
This step can fail with a `Staging failed` error message. This could be due to
|
||||
|
||||
* a nondeterministic bug that is already on the target. If you are an Odoo employee, you can check
|
||||
those here: https://runbot.odoo.com/runbot/errors
|
||||
* a nondeterministic bug that you introduced but wasn't detected in the CI before
|
||||
* an incompatibility with another commit merged right before and what you are trying to merge
|
||||
* an incompatibility with the enterprise repository if you only did changes in the community repo
|
||||
|
||||
Always check that the issue does not come from you before asking the merge bot to retry: rebase
|
||||
your branch on the target and rerun the tests locally.
|
||||
|
||||
Modules
|
||||
=======
|
||||
|
||||
Because Odoo is modular, the tests need to be modular also. This means the modules are defined in
|
||||
the module that adds the functionality you add; and that tests cannot depend on functionality
|
||||
coming from modules your module doesn't depend on.
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
:ref:`Special Tags<reference/testing/tags>`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
# The CI will run these tests after all the modules are installed,
|
||||
# not right after installing the one defining it.
|
||||
@tagged('post_install', '-at_install') # add `post_install` and remove `at_install`
|
||||
class PostInstallTestCase(SavepointCase):
|
||||
def test_01(self):
|
||||
...
|
||||
|
||||
@tagged('at_install') # this is the default
|
||||
class AtInstallTestCase(SavepointCase):
|
||||
def test_01(self):
|
||||
...
|
||||
|
||||
|
||||
If the behavior you want to test can be changed by the installation of another module, you need to
|
||||
ensure that the tag `at_install` is set; otherwise you can use the tag `post_install` to speed up
|
||||
the CI, and ensure it is not changed if it shouldn't.
|
||||
|
||||
Writing a test
|
||||
==============
|
||||
|
||||
**Reference**: the documentation related to this topic can be found in
|
||||
`Python unittest <https://docs.python.org/3/library/unittest.html>`__.
|
||||
and :ref:`Testing Odoo<reference/testing>`.
|
||||
|
||||
Here are a few things to take into consideration before writing a test
|
||||
|
||||
* The tests should be independent from the data currently in the database (including demo data)
|
||||
* Tests should not impact the database by leaving/changing residual data. This is usually done by
|
||||
the test framework by doing a rollback. This is why you must never call ``cr.commit`` in a test
|
||||
(nor anywhere else in the business code).
|
||||
* For a bug fix, the test should fail before applying the fix and pass after.
|
||||
* Don't test something that is already tested elsewhere; you can trust the ORM. Most of the tests
|
||||
in business modules should only test the business flows.
|
||||
* You shouldn't need to flush data into the database.
|
||||
|
||||
.. note:: Remember that ``onchange`` only applies in the Form views, not by changing the attributes
|
||||
in python. This also applies in the tests. If you want to emulate a Form view, you can use
|
||||
``odoo.tests.common.Form``.
|
||||
|
||||
The tests should be located in a ``tests`` folder in the root of your module. Each test file name
|
||||
should start with `test_` and be imported in the ``__init__.py`` of the test folder. You shouldn't
|
||||
import the test folder/module in the ``__init__.py`` of the module.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
estate
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── tests
|
||||
│ ├── test_*.py
|
||||
│ └── __init__.py
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
.. note:: Some older tests are extending ``odoo.tests.common.TransactionCase``, but they are less
|
||||
scalable. The difference is that the setup is done per test method and not per test class.
|
||||
The data changed are rollbacked between each test in `SavepointCase` to have the same behavior as
|
||||
in `TransactionCase`.
|
||||
|
||||
All the tests should extend ``odoo.tests.common.SavepointCase``. You usually define a
|
||||
``setUpClass``, and the tests. After doing the `setUpClass`, you have an `env` available on the
|
||||
class and can start interacting with the ORM.
|
||||
|
||||
These test classes are built on top of the ``unittest`` python module.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
# The CI will run these tests after all the modules are installed,
|
||||
# not right after installing the one defining it.
|
||||
@tagged('post_install', '-at_install')
|
||||
class EstateTestCase(SavepointCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# add env on cls and many other things
|
||||
super(EstateTestCase, cls).setUpClass()
|
||||
|
||||
# create the data for each tests. By doing it in the setUpClass instead
|
||||
# of in a setUp or in each test case, we reduce the testing time and
|
||||
# the duplication of code.
|
||||
cls.properties = cls.env['estate.property'].create([...])
|
||||
|
||||
def test_creation_area(self):
|
||||
"""Test that the total_area is computed like it should."""
|
||||
self.properties.living_area = 20
|
||||
self.assertRecordValues(self.properties, [
|
||||
{'name': ..., 'total_area': ...},
|
||||
{'name': ..., 'total_area': ...},
|
||||
])
|
||||
|
||||
|
||||
def test_action_sell(self):
|
||||
"""Test that everything behaves like it should when selling a property."""
|
||||
self.properties.action_sold()
|
||||
self.assertRecordValues(self.properties, [
|
||||
{'name': ..., 'state': ...},
|
||||
{'name': ..., 'state': ...},
|
||||
])
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
self.properties.forbidden_action_on_sold_property()
|
||||
|
||||
.. note:: For more readability, split your tests into multiple files depending on the scope of the
|
||||
tests. You can also have a Common class that most of the tests should inherit from; that common
|
||||
class can define the whole set up for the module. For instance in
|
||||
`account <https://github.com/odoo/odoo/blob/14.0/addons/account/tests/common.py>`__.
|
||||
|
||||
.. exercise:: Ensure no one can create an offer for a sold Property, and create a test for it.
|
||||
|
||||
|
||||
.. exercise:: Someone keeps breaking the reset of Garden Area and Orientation when you uncheck the
|
||||
Garden checkbox. Make sure it doesn't happen again.
|
||||
|
||||
.. tip:: Tip: remember the note about `Form` a little bit above.
|
9
content/developer/howtos/rdtraining/F_jstour.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/F_jstour:
|
||||
|
||||
====================
|
||||
Advanced F: JS Tours
|
||||
====================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
9
content/developer/howtos/rdtraining/G_website.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/G_website:
|
||||
|
||||
=================================
|
||||
Advanced G: Controllers & Website
|
||||
=================================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
9
content/developer/howtos/rdtraining/H_adv_views.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/H_adv_views:
|
||||
|
||||
==========================
|
||||
Advanced H: Advanced Views
|
||||
==========================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
9
content/developer/howtos/rdtraining/I_jswidget.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/I_jswidget:
|
||||
|
||||
============================
|
||||
Advanced I: Custom JS Widget
|
||||
============================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
313
content/developer/howtos/rdtraining/J_reports.rst
Normal file
@ -0,0 +1,313 @@
|
||||
.. _howto/rdtraining/J_reports:
|
||||
|
||||
=======================
|
||||
Advanced J: PDF Reports
|
||||
=======================
|
||||
|
||||
.. warning::
|
||||
|
||||
This tutorial assumes you have completed the :ref:`Core Training <howto/rdtraining>`
|
||||
and have installed :ref:`wkhtmltopdf <howto/rdtraining/02_setup/install-wkhtmltopdf>`.
|
||||
|
||||
To follow the exercise, it is recommended that you fetch the branch
|
||||
14.0-core from the repository XXX, it
|
||||
contains a version of the module created during the core training we can use
|
||||
as a starting point.
|
||||
|
||||
We were previously :ref:`introduced to QWeb <howto/rdtraining/15_qwebintro>`
|
||||
in the Core Training where it was used to build a kanban view. Now we will expand on one of QWeb's
|
||||
other main uses: creating PDF reports. A common business requirement is the ability to create documents
|
||||
to send to customers and to use internally. These reports can be used to summarize and display
|
||||
information in an organized template to support the business in different ways. Odoo
|
||||
can additionally add our company's header and footer to our reports with minimal extra effort.
|
||||
|
||||
The documentation related to this topic can be found in :ref:`reference/qweb`,
|
||||
:ref:`reference/reports/report` and the :ref:`reference/actions/report`
|
||||
section of the Actions reference.
|
||||
|
||||
File Structure
|
||||
==============
|
||||
|
||||
The bulk of a PDF report is its QWeb template. It also typically needs a corresponding
|
||||
``ir.actions.report`` to include the report within a module's business logic.
|
||||
There is no strict rule for the file names or where they are located, but these two parts are
|
||||
typically stored in 2 separate files within a ``report`` folder in the top level of your module's
|
||||
directory. If a module has many or multiple long report templates, then they are often organized in
|
||||
a logical manner across different files named after the report(s) they contain. All actions
|
||||
for the reports are usually stored in the same file ending with ``_report_views.xml`` regardless of the
|
||||
number of reports it contains.
|
||||
|
||||
Therefore it is expected that your work tree will look something like:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
estate
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── report
|
||||
│ ├── estate_report_templates.xml
|
||||
│ └── estate_report_views.xml
|
||||
├── security
|
||||
│ └── ir.model.access.csv
|
||||
├── views
|
||||
│ └── *.xml
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
Note that you will often see other non-QWeb and non-XML files containing "report" in their name also within
|
||||
the report folder. These are unrelated to the reports covered in this tutorial and are covered in
|
||||
:ref:`another advanced topic <howto/rdtraining/K_dashboard>`. For now you can think of them as customized views
|
||||
that use direct SQL queries (sometimes referred to as SQL Views).
|
||||
|
||||
Don't forget to add whatever files your template and action view will be located in to your ``__manifest__.py``.
|
||||
In this case you will want to add the files to the ``data`` list and remember that the files listed in a manifest
|
||||
are loaded sequentially!
|
||||
|
||||
Basic Report
|
||||
============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will can print a report that displays all offers for a
|
||||
property.
|
||||
|
||||
.. image:: J_reports/simple_report.png
|
||||
:align: center
|
||||
:alt: Simple PDF report
|
||||
|
||||
In our real estate example there are many useful reports that we could create. One simple report we
|
||||
can create is one that displays all of a property's offers.
|
||||
|
||||
Report Data
|
||||
-----------
|
||||
|
||||
Before we do anything we first need some data to populate our reports or else this tutorial
|
||||
won't be very interesting. When creating reports, you will need some data to test your report code
|
||||
and check that the resulting look is as expected. It is a good idea to test with data that will cover most
|
||||
or all of your expected use cases. A good representation set for our simple report is:
|
||||
|
||||
* At least 3 properties where 1 is "sold", 1 is "offer received" and 1 is "new".
|
||||
* At least 2-3 offers for our "sold" and "offer received" properties
|
||||
|
||||
If you don't have a set of data like this already, you can either:
|
||||
|
||||
* Complete :ref:`howto/rdtraining/C_data` (if you haven't done so already) and add the extra
|
||||
cases to your demo data (you may need to create a new database to load in the demo data).
|
||||
* Manually create the data in your database.
|
||||
* Copy this `data file <https://github.com/odoo/technical-training-solutions/blob/14.0-J_reports/estate/data/estate_demo.xml>`__
|
||||
into a new directory (data) in your estate module and copy
|
||||
`these lines <https://github.com/odoo/technical-training-solutions/blob/14.0-J_reports/estate/__manifest__.py#L21-L23>`__
|
||||
into your __manifest__.py file (you may need to create a new database to load in the demo data).
|
||||
|
||||
Before continuing, click through your data in your database and make sure your data is as expected.
|
||||
Of course you can add the data after you write your report code, but then you will not be able to
|
||||
incrementally test portions of your code as you write it. This can make checking for mistakes and
|
||||
debugging your code more difficult in the long run for complicated reports.
|
||||
|
||||
Minimal Template
|
||||
----------------
|
||||
|
||||
A minimal viable template is viewable under the "Minimal viable template" section of the
|
||||
:ref:`reference/reports/templates` documentation. We can modify this example to build
|
||||
our minimal property offers template file:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="report_property_offers">
|
||||
<t t-foreach="docs" t-as="property">
|
||||
<t t-call="web.html_container">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2>
|
||||
<span t-field="property.name"/>
|
||||
</h2>
|
||||
<div>
|
||||
<strong>Expected Price: </strong>
|
||||
<span t-field="property.expected_price"/>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-set="offers" t-value="property.mapped('offer_ids')"/>
|
||||
<tr t-foreach="offers" t-as="offer">
|
||||
<td>
|
||||
<span t-field="offer.price"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
Most of the Odoo specific (i.e. non-HTML) items in our file are explained in the minimal viable template section.
|
||||
Some additional features in our template are:
|
||||
|
||||
* The use of the ``class="table"`` attribute so our table has some nice formatting. Twitter Bootstrap
|
||||
(we're using its table class in this case) and FontAwesome (useful for adding icons) classes can
|
||||
be used in your report template.
|
||||
* The use of ``t-set``, ``t-value``, ``t-foreach`` and ``t-as`` so that we can loop over all of the ``offer_ids``.
|
||||
|
||||
If you are already familiar with website templating engines, then the QWeb directives (i.e. the `t-` commands)
|
||||
probably don't need much explanation and you can just look at its :ref:`documentation <reference/qweb>` and
|
||||
skip ahead to the next subsection.
|
||||
|
||||
Otherwise you are encouraged to read more about them (
|
||||
`Wikipedia <https://en.wikipedia.org/wiki/Template_processor>`__ has a good high level description), but
|
||||
the general idea is that QWeb provides the ability to dynamically generate web code based on Odoo data and
|
||||
simple commands. I.e. QWeb can access recordset data (and methods) and process simple programming operations
|
||||
such as setting and accessing temporary variables. For example, in the above example:
|
||||
|
||||
* ``t-set`` creates a temporary variable called "offers" that has its value set by ``t-value`` to the current
|
||||
``estate.property`` recordset's ``offer_ids``.
|
||||
* the ``t-foreach`` and ``t-as`` usage is the equivalent to the Python:
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
for offer in offers:
|
||||
|
||||
Report Action
|
||||
-------------
|
||||
|
||||
Now that we have a template, we need to make it accessible in our app via a ``ir.actions.report``.
|
||||
A practical example of ``ir.actions.report`` is
|
||||
`here <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/event/report/event_event_reports.xml#L20-L30>`__
|
||||
corresponding to
|
||||
`this template <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/event/report/event_event_templates.xml#L5>`__.
|
||||
Its contents are all explained in :ref:`the documentation <reference/actions/report>`.
|
||||
|
||||
An ``ir.actions.report`` is primarily used via the Print menu of a model's view. In the practical
|
||||
example, the ``binding_model_id`` specifies which model's views the report should show in and Odoo
|
||||
will auto-magically add it for you. Another common use case of the report action is to link it to
|
||||
a button like we learned in :ref:`howto/rdtraining/10_actions`. This is handy for reports that
|
||||
only make sense under specific conditions. For example, if we wanted to make a "Final Sale" report,
|
||||
then we can link it to a "Print Sale Info" button that appears in the form view only when the
|
||||
property is "Sold".
|
||||
|
||||
.. image:: J_reports/print_menu.png
|
||||
:align: center
|
||||
:alt: Print Menu Button
|
||||
|
||||
You may have noticed or are wondered why our report template loops through a recordset. When our
|
||||
template is passed more than one record, it can produce one PDF report for all of the records.
|
||||
Using the Print menu in the list view with multiple records selected will demonstrate this.
|
||||
|
||||
Make a Report
|
||||
---------------
|
||||
|
||||
Finally, you now know where to create your files and how the content of the files should look. Happy report making!
|
||||
|
||||
.. exercise:: Make a report.
|
||||
|
||||
- Add the property offers report from the minimal template subsection to the Print menu of the Property views.
|
||||
|
||||
- Improve the report by adding more data. Refer to the **Goal** of this section to see what additional
|
||||
data you can add and feel free to add even more.
|
||||
|
||||
- Bonus: Make an extra flexible report by adding in some logic so that when there are no offers on a property
|
||||
then we don't create a table and instead write something about how there are no offers yet. Hint: you will
|
||||
need to use ``t-if`` and ``t-else``.
|
||||
|
||||
Remember to check that your PDF reports match your data as expected.
|
||||
|
||||
|
||||
Sub-templates
|
||||
=============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will have a sub-template that we use in 2 reports.
|
||||
|
||||
.. image:: J_reports/report_subtemplate.png
|
||||
:align: center
|
||||
:alt: Report using a subtemplate
|
||||
|
||||
There are two main reasons for using sub-templates. One is to make the code easier to read when working with
|
||||
extra long or complicated templates. The other is to reuse code where possible. Our simple property offers
|
||||
report is useful, but listing property offers information can be useful for more than just one report template.
|
||||
One example is a report that lists all of a salesman's properties' offers.
|
||||
|
||||
See if you can understand how to call a sub-template by reading the
|
||||
:ref:`documentation <reference/qweb/sub-templates>` on it and/or by looking at an
|
||||
`example <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/portal/static/src/xml/portal_chatter.xml#L147-L160>`__
|
||||
(remember QWeb uses the same control flows regardless if it is for a report or a view in Odoo.)
|
||||
|
||||
.. exercise:: Create and use a sub-template.
|
||||
|
||||
- Split the table portion of the offers into its own template. Remember to check that your
|
||||
original report still prints correctly afterwards.
|
||||
|
||||
- Add a new report for ``res.users`` that allows you to print all of the Real Estate Properties
|
||||
that are visible in their form view (i.e. in the "Settings" app). Include the offers for each
|
||||
of those saleman's properties in the same report. Hint: since the ``binding_model_id`` in this
|
||||
case will not be within the estate module, you will need to use ``ref="base.model_res_users"``.
|
||||
|
||||
Your end result should look similar to the image in the **Goal** of this section.
|
||||
|
||||
Remember to check that your reports match your data as expected!
|
||||
|
||||
Report Inheritance
|
||||
==================
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will inherit the property report in the ``estate_account``
|
||||
module.
|
||||
|
||||
.. image:: J_reports/inherited_report.png
|
||||
:align: center
|
||||
:alt: An inherited report
|
||||
|
||||
Inheritance in QWeb uses the same ``xpath`` elements as :ref:`views inheritance <reference/views/inheritance>`.
|
||||
A QWeb template refers to its parent template in a different way though. It is even easier to do by just adding
|
||||
the ``inherit_id`` attribute to the ``template`` element and setting it equal to the *module.parent_template_id*.
|
||||
|
||||
We didn't add any new fields to any of the estate models in `estate_account`, but we can still add information
|
||||
to our existing property report. For example, we know that any "Sold" properties will already have an invoice
|
||||
created for them, so we can add this information to our report.
|
||||
|
||||
.. exercise:: Inherit a report.
|
||||
|
||||
- Extend the property report to include some information about the invoice. You can look at the **Goal** of this
|
||||
section for inspiration (i.e. print a line when the property is Done, otherwise print nothing).
|
||||
|
||||
Again, remember to check that your reports match your data as expected!
|
||||
|
||||
Additional Features
|
||||
===================
|
||||
|
||||
All of the following extra features are described further in the :ref:`reference/reports/report`
|
||||
documentation, including how to implement each of them.
|
||||
|
||||
Translations
|
||||
------------
|
||||
|
||||
We all know Odoo is used in multiple languages thanks to automated and manual translating. QWeb reports are no
|
||||
exception! Note that sometimes the translations do not work properly if there are unnecessary spaces in your
|
||||
template's text content so try to avoid them when possible (especially leading spaces).
|
||||
|
||||
Reports are web pages
|
||||
---------------------
|
||||
|
||||
You probably are tired of hearing that QWeb creates HTML, but we're saying it again! One of the
|
||||
neat features about reports being written in QWeb is they can be viewed within the web browser.
|
||||
This can be useful if you want to embed a hyperlink that leads to a specific report. Note that
|
||||
the usual security checks will still apply to prevent unauthorized users from accessing the reports.
|
||||
|
||||
Barcodes
|
||||
--------
|
||||
|
||||
Odoo has a built-in barcode image creator that allows for barcodes to be embedded in your reports.
|
||||
Check out the corresponding
|
||||
`code <https://github.com/odoo/odoo/blob/0e12fa135882cd5095dbf15fe2f64231c6a84336/addons/web/controllers/main.py#L2044-L2046>`__
|
||||
to see all the supported barcode types.
|
After Width: | Height: | Size: 33 KiB |
BIN
content/developer/howtos/rdtraining/J_reports/print_menu.png
Normal file
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 57 KiB |
BIN
content/developer/howtos/rdtraining/J_reports/simple_report.png
Normal file
After Width: | Height: | Size: 46 KiB |
354
content/developer/howtos/rdtraining/K_dashboard.rst
Normal file
@ -0,0 +1,354 @@
|
||||
.. _howto/rdtraining/K_dashboard:
|
||||
|
||||
======================
|
||||
Advanced K: Dashboards
|
||||
======================
|
||||
|
||||
.. warning::
|
||||
|
||||
This tutorial assumes you have completed the :ref:`Core Training <howto/rdtraining>` and have
|
||||
access to Odoo Enterprise features.
|
||||
|
||||
To follow the exercise, it is recommended that you fetch the branch
|
||||
14.0-core from the repository XXX, it
|
||||
contains a version of the module created during the core training we can use
|
||||
as a starting point.
|
||||
|
||||
The term "Dashboard" is used in Odoo for objects that display data, but involves different
|
||||
implementations. This tutorial will only focus on the Enterprise view that is used to provide
|
||||
aggregated data visualization. They can be added as a ``view_mode`` to an existing model (i.e. a
|
||||
view you can switch to via the view buttons in the top right of a view), but they are also often
|
||||
used as a view for to a special model customized for data visualization. You may hear these
|
||||
special views referred to as SQL views.
|
||||
|
||||
It is useful to note there is a "Dashboard" app in Odoo Community. This app allows users to create
|
||||
their own customized view of data, but the customization is only visible to each user and can
|
||||
only be viewed within the "Dashboard" app. Technically it is possible to make global dashboards
|
||||
using this ``board`` module, but it is much easier to do as an Enterprise view. Plus it looks nicer
|
||||
and has extra features not available in ``board``. Some other dashboards within Odoo also exist,
|
||||
but they are custom made and are beyond the scope of this tutorial.
|
||||
|
||||
The documentation related to this topic can be found in :ref:`reference/views/dashboard`.
|
||||
|
||||
File Structure
|
||||
==============
|
||||
|
||||
You probably have already guessed that since dashboard views are an Enterprise view, they must have
|
||||
a dependency on an Enterprise module. The Enterprise module is ``web_dashboard``. Don't forget to
|
||||
add it to your manifest file! It is standard to add dashboards intended to be used as a
|
||||
``view_mode`` for one of your module's models (in the ``model`` folder``) to the views directory
|
||||
(i.e. the same file that contains the other views for the same model).
|
||||
|
||||
It is standard to create a separate Enterprise module to add extra Enterprise views and features to
|
||||
a Community module. This is done in a similar manner as the module link technique covered within
|
||||
:ref:`howto/rdtraining/14_other_module`. The difference is that instead of linking 2 different
|
||||
modules, we are extending our `estate` module. We do this by creating a new module and adding both
|
||||
the Community module and its necessary Enterprise module dependencies to its manifest. You will
|
||||
commonly see "enterprise" in the module's directory name. To keep this tutorial simple, we will
|
||||
add dashboards to our existing ``estate`` module.
|
||||
|
||||
SQL Views have 2 parts: their xml file (don't forget to add it to your manifest file) and their
|
||||
Python file (don't forget to add it to the appropriate ``__init.py__`` files). The former is the
|
||||
same format as the ``view_mode`` xml while the latter contains a custom model and SQL code to
|
||||
populate its fields. It is standard to add SQL view files to the ``report/`` directory. It
|
||||
is also common to include "report" in the name of the SQL view's files. You may be
|
||||
wondering why do we put the files in a report directory? We saw earlier that the dashboard is
|
||||
for data visualization, therefore it is not editable. You can think of dashboards as interactive
|
||||
reports where you can click on statistics, graphs, and charts to see the specific data contributing
|
||||
to them. Note it is also standard to store the xml code for
|
||||
:ref:`PDF report templates <howto/rdtraining/J_reports>` in the report directory.
|
||||
|
||||
It is expected that your work tree will look something like:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
estate
|
||||
├── models
|
||||
│ ├── *.py
|
||||
│ └── __init__.py
|
||||
├── report
|
||||
│ ├── __init__.py
|
||||
│ ├── estate_report.py
|
||||
│ └── estate_report_views.xml
|
||||
├── security
|
||||
│ └── ir.model.access.csv
|
||||
├── views
|
||||
│ ├── *.xml
|
||||
│ └── estate_property_views.xml
|
||||
├── __init__.py
|
||||
└── __manifest__.py
|
||||
|
||||
Dashboard View
|
||||
==============
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will have a new dashboard view that displays
|
||||
different property statistics.
|
||||
|
||||
.. image:: K_dashboard/simple_dashboard.png
|
||||
:align: center
|
||||
:alt: Basic Dashboard view
|
||||
|
||||
Dashboards can display data in different ways, including:
|
||||
|
||||
* showing an ``aggregate`` of a field
|
||||
* using aggregated fields in a ``formula``
|
||||
* using a ``widget``
|
||||
* using another ``view`` as a subview
|
||||
|
||||
There are many useful statistics and visuals we can provide for our real estate example using
|
||||
these options. A full example to reference while doing the exercises in this section is
|
||||
`viewable here <https://github.com/odoo/enterprise/blob/6fd3244ae168dc73c348a9c1870796e89d8ef594/crm_enterprise/views/crm_lead_views.xml#L106-L133>`__
|
||||
(restricted github repository link).
|
||||
|
||||
Data
|
||||
----
|
||||
To fully enjoy our dashboard view, we will need good test data to populate it. Test data will
|
||||
allow us to check that the resulting look and statistics are correct. It is a good idea to test
|
||||
with data that will cover most or all of your expected use cases, but is also easy to verify with
|
||||
that your statistics are correct. In our goal's case we are looking at count, sum, average,
|
||||
minimum, and maximum statistics, therefore a good representation set for our dashboard is:
|
||||
|
||||
* At least 3 properties with different property types, expected prices, and average living area.
|
||||
* At least 1 sold property and at least 1 canceled property
|
||||
|
||||
If you don't have a set of data like this already, you can either:
|
||||
|
||||
* Complete :ref:`howto/rdtraining/C_data` (if you haven't done so already) and add the extra
|
||||
cases to your demo data (you may need to create a new database to load in the demo data).
|
||||
* Manually create the data in your database.
|
||||
* Copy this `data file <https://github.com/odoo/technical-training-solutions/blob/14.0-K_dashboard/estate/data/estate_demo.xml>`__
|
||||
into a new directory called ``data`` in your estate module and copy
|
||||
`these lines <https://github.com/odoo/technical-training-solutions/blob/14.0-K_dashboard/estate/__manifest__.py#L21-L23>`__
|
||||
into your __manifest__.py file (you may need to create a new database to load in the demo data).
|
||||
|
||||
Click through your database data and make sure it is what you expect. Of course you can add the
|
||||
data after you write your dashboard code and then test that your view is working as expected.
|
||||
|
||||
Aggregations
|
||||
------------
|
||||
Building a dashboard view is very similar to what you have previously done in
|
||||
:ref:`howto/rdtraining/07_basicviews`. For the dashboard view, we use the `dashboard` root element
|
||||
and choose from its possible tags (see all the possibilities and their attributes in the
|
||||
:ref:`reference/views/dashboard` documentation). So a simple dashboard example is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<dashboard>
|
||||
<group>
|
||||
<aggregate name="min_expected_price" string="Min Expected Price" field="expected_price"
|
||||
group_operator="min" help="Lowest expected price."/>
|
||||
</group>
|
||||
</dashboard>
|
||||
|
||||
In this example, ``<group>`` adds styling and ``<aggregate>`` declares an aggregation. We
|
||||
indicate which ``field`` we want to aggregate, what ``string`` to display with the value, and
|
||||
how to aggregate it with the `group_operator` attribute. The `group_operator` can use any valid
|
||||
PostgreSQL aggregate function plus the special Odoo defined ``count_distinct``.
|
||||
|
||||
Hopefully you remember how to add views to a window action `view_mode` (hint, it was
|
||||
covered in :ref:`howto/rdtraining/06_firstui`). Now let's make some dashboards!
|
||||
|
||||
.. exercise:: Make a dashboard view.
|
||||
|
||||
- Create a dashboard of aggregated values for the ``estate.property`` model. You can
|
||||
look at the **Goal** of this section for some inspiration. Remember to check that your
|
||||
statistics are calculating as you expect and note that the calculated values take into
|
||||
consideration any applied view filters!
|
||||
|
||||
- Bonus: Add in some aggregations that need a `domain` to make sense (remember domains were
|
||||
also covered in :ref:`howto/rdtraining/07_basicviews`).
|
||||
|
||||
Pie Charts
|
||||
----------
|
||||
Adding pie charts to dashboards is a piece of cake using the `<widget>` element. An example is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<dashboard>
|
||||
<group>
|
||||
<widget name="pie_chart" title="Property Types" attrs="{'groupby': 'property_type_id'}"/>
|
||||
</group>
|
||||
</dashboard>
|
||||
|
||||
In this example, we indicate that we're using the `pie_chart` widget with the `name` attribute,
|
||||
the ``title`` for the pie chart, and that we're grouping it by property type.
|
||||
|
||||
.. exercise:: Add some pie charts.
|
||||
|
||||
- Add the pie charts from the **Goal** of this section to your dashboard. Hint: you will need
|
||||
to add `'measure': selling_price` to your pie chart `attrs` if you want to show selling
|
||||
prices grouped by property type.
|
||||
|
||||
- Hover over and click on the pie charts to check your charts counts values and don't forget
|
||||
that filters will also apply to the charts.
|
||||
|
||||
- Bonus: Add a domain to your selling price pie chart to only include "sold" properties (i.e.
|
||||
not "offer_accepted" ones). Note that the `'` will need to be escaped since it is declared
|
||||
as part of the `attrs`.
|
||||
|
||||
Subviews
|
||||
--------
|
||||
Similar to how we can use the list view within the form view (we saw this automatically happen for
|
||||
One2many relationships in :ref:`howto/rdtraining/08_relations`), we can add other views within our
|
||||
dashboard view. The most commonly added are the pivot and graph views, but the cohort view is
|
||||
also an option. These views are covered in more depth in :ref:`howto/rdtraining/H_adv_views`. For
|
||||
this topic you are only required to know their names. A dashboard with only subviews is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<dashboard>
|
||||
<view type="graph"/>
|
||||
<view type="pivot"/>
|
||||
</dashboard>
|
||||
|
||||
The `ref` attribute can be added to `<view>` elements to use a specific xml id for that view. If
|
||||
no xml id is provided for a graph or pivot view then the default view will be used.
|
||||
The cohort view will not work in the dashboard without a specific xml id. If you have already
|
||||
created some of these views then you are welcome to add them to your dashboard! Sample graph and
|
||||
pivot views are included in the
|
||||
`solution code <https://github.com/odoo/technical-training-solutions/blob/14.0-K_dashboard/estate/views/estate_property_views.xml#L169-L191>`__
|
||||
that you are welcome to use as well.
|
||||
|
||||
.. exercise:: Add subviews.
|
||||
|
||||
- Add in a graph and a pivot view to your dashboard. Try playing around with the layout of
|
||||
your subviews in relation to your pie charts and aggregated values and refer to the **Goal**
|
||||
of this section for an often used layout. Remember to check that your subviews are
|
||||
displaying your data as expected (and yes, they are also affected by the filters!).
|
||||
|
||||
SQL Views
|
||||
=========
|
||||
|
||||
.. warning::
|
||||
|
||||
This section expects you to have a basic knowledge of SQL. If you have little to no SQL
|
||||
knowledge then `this is a good tutorial to start with <https://selectstarsql.com/>`__
|
||||
and these `exercises <https://www.pgexercises.com/>`__ are good for those who need
|
||||
a refresher or extra practice.
|
||||
|
||||
.. note::
|
||||
|
||||
**Goal**: at the end of this section, we will have a new SQL view that displays different
|
||||
property statistics.
|
||||
|
||||
.. image:: K_dashboard/report_dashboard.png
|
||||
:align: center
|
||||
:alt: SQL view
|
||||
|
||||
|
||||
Occasionally we want to show data that goes beyond what our model already has in it. We could add
|
||||
a lot of stored computed or related fields (non-stored fields cannot be aggregated
|
||||
or displayed in pie charts), but it would be impractical to store a bunch of fields only for this
|
||||
purpose. We can instead add a custom SQL view to minimize the computational load and keep our
|
||||
model clean of unnecessary fields.
|
||||
|
||||
Model
|
||||
-----
|
||||
We will start with the more difficult part: our special report model. This file starts the same as
|
||||
any other model except that we add 2 attributes ``_auto`` and ``_rec_name``::
|
||||
|
||||
from odoo import fields, models, tools
|
||||
|
||||
|
||||
class EstateReport(models.Model):
|
||||
_name = 'estate.report'
|
||||
_description = "Stock Report"
|
||||
_rec_name = 'id'
|
||||
_auto = False
|
||||
|
||||
``_auto = False`` indicates that we do not want to store the model in the database and we will
|
||||
create a custom table by overriding the ``BaseModel.init()`` method. ``_rec_name`` indicates
|
||||
which of the model's fields represents a record's name (i.e. the name that will be used in the
|
||||
navigation breadcrumb when opening a record's form view). In this case I left it as 'id' because
|
||||
our property offers don't have a name. We will need the `tools` import later (i.e.
|
||||
``odoo/odoo/tools``, which is full of all sort of useful helper methods you will probably use in
|
||||
the future). Note that it is standard to include ``report`` in the model name.
|
||||
|
||||
Remember your new model will need to be added to your security file like you learned in
|
||||
:ref:`howto/rdtraining/05_securityintro`!
|
||||
|
||||
Then we define the fields we need for our dashboard the same way as any other model (like you
|
||||
learned in :ref:`howto/rdtraining/04_basicmodel`), except that every field is
|
||||
``readonly=True``. After all, our model is for readonly purposes only.
|
||||
|
||||
Now we override the ``BaseModel.init()`` method mentioned earlier::
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||
self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
|
||||
SELECT
|
||||
%s
|
||||
FROM
|
||||
%s
|
||||
)""" % (self._table, self._select(), self._from()))
|
||||
|
||||
We use ``tools.drop_view_if_exists`` to ensure that we don't create a conflicting view and then
|
||||
execute the SQL query. It is standard to separate the different parts of the query to
|
||||
allow for easier model extension. Exactly how the query is split up across methods is not
|
||||
standardized, but you will often see at minimum ``_select`` and ``_from`` methods [or something
|
||||
similar] and of course all of these methods will return strings. The columns from the SELECT
|
||||
will populate our model's fields so ensure that your column names match your field names
|
||||
or use alias names that match.
|
||||
|
||||
.. exercise:: Create report model.
|
||||
|
||||
- Create a report model with the following fields:
|
||||
|
||||
========================= ========================= =========================
|
||||
Field Type Note
|
||||
========================= ========================= =========================
|
||||
id Integer Corresponds to ``id`` of ``estate.property.offer``
|
||||
offer_state Selection Equals ``state`` choices of ``estate.property.offer``
|
||||
property_id Many2one ``estate.property``
|
||||
property_state Selection Equals ``state`` choices of ``estate.property``
|
||||
property_type_id Many2one ``estate.property.type``
|
||||
========================= ========================= =========================
|
||||
|
||||
and write the SQL query necessary to populate the fields (hint, you will need 2 JOINs).
|
||||
|
||||
You won't be able to check if your model is correct until we create a view for it, but you are
|
||||
welcome to check your query directly in your database to see if the results are as you expect.
|
||||
If you struggle with this exercise, then
|
||||
`here is an example <https://github.com/odoo/odoo/blob/7417d8fc138b9de550bc631435bcc08628c29bed/addons/crm/report/crm_activity_report.py>`__
|
||||
to reference.
|
||||
|
||||
View
|
||||
----
|
||||
Now that we have our model, we can make its dashboard view. There is no difference to how its made
|
||||
except that its file is located in the ``report`` folder. Since it is a new model not linked to
|
||||
any other model, we will also have to add a new menuitem to view our dashboard. Typically SQL views
|
||||
are added under a first level menu called ``Reporting` (because it's a report, surprise!). Do you
|
||||
remember how to add a ``menuitem``? If not, revisit :ref:`howto/rdtraining/06_firstui`) again.
|
||||
|
||||
.. exercise:: Create report view.
|
||||
|
||||
- Recreate the dashboard in the **Goal** of this section. Hint: it uses the ``formula`` element,
|
||||
which we did not need for our previous dashboard.
|
||||
|
||||
- Bonus: Create ``list`` and ``form`` views for your new report model so we don't have to see the ugly
|
||||
defaults when you click on your pie charts.
|
||||
|
||||
Extra Tips
|
||||
----------
|
||||
**Tip 1** A common mistake in SQL views is to not take into account the duplication of certain data
|
||||
due to table JOINs. For example, in our **Goal** we have a pie chart of the offers' property types.
|
||||
We may be tempted to add a similar pie chart with a domain to only include canceled properties,
|
||||
so we think we are only counting the number of canceled properties by property type. In reality we
|
||||
are still looking at all the offers per property so any property with more than 1 offer will be
|
||||
counted per offer. This example is easily double checked by clicking on the pie chart to see its
|
||||
list view:
|
||||
|
||||
.. image:: K_dashboard/report_list_detail.png
|
||||
:align: center
|
||||
:alt: Pie chart list view
|
||||
|
||||
But for cases such as average aggregations or using a subview such as the pivot view, it is easy to
|
||||
miss this mistake. It is also easy to miss this mistake when you have insufficient test data.
|
||||
In order to add a number of properties canceled by property type pie chart to this
|
||||
report, we would either have to do a hack (too advanced for this tutorial) or simply exclude it
|
||||
from this report.
|
||||
|
||||
**Tip 2** If you have a field that you do not want as a measure (i.e. in your pivot or
|
||||
graph views), then you can add ``store=False`` to it and it will not show.
|
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 45 KiB |
9
content/developer/howtos/rdtraining/L_cron.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/L_cron:
|
||||
|
||||
======================================
|
||||
Advanced L: Scheduled & Server Actions
|
||||
======================================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
9
content/developer/howtos/rdtraining/M_migration.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/M_migration:
|
||||
|
||||
======================
|
||||
Advanced M: Migrations
|
||||
======================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
9
content/developer/howtos/rdtraining/N_security.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/N_security:
|
||||
|
||||
====================
|
||||
Advanced N: Security
|
||||
====================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
9
content/developer/howtos/rdtraining/O_perf.rst
Normal file
@ -0,0 +1,9 @@
|
||||
:orphan:
|
||||
|
||||
.. _howto/rdtraining/O_perf:
|
||||
|
||||
========================
|
||||
Advanced O: Performances
|
||||
========================
|
||||
|
||||
Hopefully, this topic will be written soon :-)
|
@ -260,6 +260,8 @@ section of the view, and allowing a few *optional* attributes:
|
||||
.. deprecated act_window & report
|
||||
.. ignored url, act_window and ir_set
|
||||
|
||||
.. _reference/data/csvdatafiles:
|
||||
|
||||
CSV data files
|
||||
==============
|
||||
|
||||
|
@ -272,6 +272,8 @@ to create. The value to set can be provided in two ways:
|
||||
|
||||
.. _reference/qweb/call:
|
||||
|
||||
.. _reference/qweb/sub-templates:
|
||||
|
||||
calling sub-templates
|
||||
=====================
|
||||
|
||||
|
@ -12,63 +12,158 @@ Both mechanisms are linked to specific users through *groups*: a user belongs
|
||||
to any number of groups, and security mechanisms are associated to groups,
|
||||
thus applying security mechanisms to users.
|
||||
|
||||
.. class:: res.groups
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
serves as user-readable identification for the group (spells out the
|
||||
role / purpose of the group)
|
||||
|
||||
.. attribute:: category_id
|
||||
|
||||
The *module category*, serves to associate groups with an Odoo App
|
||||
(~a set of related business models) and convert them into an exclusive
|
||||
selection in the user form.
|
||||
|
||||
.. todo:: clarify & document special cases & relationship between
|
||||
groups & categories better
|
||||
|
||||
.. attribute:: implied_ids
|
||||
|
||||
Other groups to set on the user alongside this one. This is a
|
||||
convenience pseudo-inheritance relationship: it's possible to
|
||||
explicitely remove implied groups from a user without removing the
|
||||
implier.
|
||||
|
||||
.. attribute:: comment
|
||||
|
||||
Additional notes on the group e.g.
|
||||
|
||||
.. _reference/security/acl:
|
||||
|
||||
Access Control
|
||||
==============
|
||||
Access Rights
|
||||
=============
|
||||
|
||||
Managed by the ``ir.model.access`` records, defines access to a whole model.
|
||||
*Grants* access to an entire model for a given set of operations. If no access
|
||||
rights matches an operation on a model for a user (through their group), the
|
||||
user doesn't have access.
|
||||
|
||||
Each access control has a model to which it grants permissions, the
|
||||
permissions it grants and optionally a group.
|
||||
Access rights are additive, a user's accesses are the union of the accesses
|
||||
they get through all their groups e.g. given a user who is part of group A
|
||||
granting read and create access and a group B granting update access, the user
|
||||
will have all three of create, read, and update.
|
||||
|
||||
Access controls are additive, for a given model a user has access all
|
||||
permissions granted to any of its groups: if the user belongs to one group
|
||||
which allows writing and another which allows deleting, they can both write
|
||||
and delete.
|
||||
.. class:: ir.model.access
|
||||
|
||||
If no group is specified, the access control applies to all users, otherwise
|
||||
it only applies to the members of the given group.
|
||||
.. attribute:: name
|
||||
|
||||
Available permissions are creation (``perm_create``), searching and reading
|
||||
(``perm_read``), updating existing records (``perm_write``) and deleting
|
||||
existing records (``perm_unlink``)
|
||||
The purpose or role of the group.
|
||||
|
||||
.. attribute:: model_id
|
||||
|
||||
The model whose access the ACL controls.
|
||||
|
||||
.. attribute:: group_id
|
||||
|
||||
The :class:`res.groups` to which the accesses are granted, an empty
|
||||
:attr:`group_id` means the ACL is granted to *every user*
|
||||
(non-employees e.g. portal or public users).
|
||||
|
||||
The :samp:`perm_{method}` attributes grant the corresponding CRUD access
|
||||
when set, they are all unset by default.
|
||||
|
||||
.. attribute:: perm_create
|
||||
.. attribute:: perm_read
|
||||
.. attribute:: perm_write
|
||||
.. attribute:: perm_unlink
|
||||
|
||||
.. _reference/security/rules:
|
||||
|
||||
Record Rules
|
||||
Access Rules
|
||||
============
|
||||
|
||||
Record rules are conditions that records must satisfy for an operation
|
||||
(create, read, update or delete) to be allowed. It is applied record-by-record
|
||||
after access control has been applied.
|
||||
Record rules are *conditions* which must be satisfied in order for an operation
|
||||
to be allowed. Record rules are evaluated record-by-record, following access
|
||||
rights.
|
||||
|
||||
A record rule has:
|
||||
Access rules are default-allow: if access rights grant access and no rule
|
||||
applies to the operation and model for the user, the access is granted.
|
||||
|
||||
* a model on which it applies
|
||||
* a set of permissions to which it applies (e.g. if ``perm_read`` is set, the
|
||||
rule will only be checked when reading a record)
|
||||
* a set of user groups to which the rule applies, if no group is specified
|
||||
the rule is *global*
|
||||
* a :ref:`domain <reference/orm/domains>` used to check whether a given record
|
||||
matches the rule (and is accessible) or does not (and is not accessible).
|
||||
The domain is evaluated with two variables in context: ``user`` is the
|
||||
current user's record and ``time`` is the `time module`_
|
||||
.. class:: ir.rule
|
||||
|
||||
Global rules and group rules (rules restricted to specific groups versus
|
||||
groups applying to all users) are used quite differently:
|
||||
.. attribute:: name
|
||||
|
||||
* Global rules are subtractive, they *must all* be matched for a record to be
|
||||
accessible
|
||||
* Group rules are additive, if *any* of them matches (and all global rules
|
||||
match) then the record is accessible
|
||||
The description of the rule.
|
||||
|
||||
This means the first *group rule* restricts access, but any further
|
||||
*group rule* expands it, while *global rules* can only ever restrict access
|
||||
(or have no effect).
|
||||
.. attribute:: model_id
|
||||
|
||||
.. warning:: record rules do not apply to the Superuser account
|
||||
:class: aphorism
|
||||
The model to which the rule applies.
|
||||
|
||||
.. attribute:: groups
|
||||
|
||||
The :class:`res.groups` to which access is granted (or not). Multiple
|
||||
groups can be specified. If no group is specified, the rule is *global*
|
||||
which is treated differently than "group" rules (see below).
|
||||
|
||||
.. attribute:: global
|
||||
|
||||
Computed on the basis of :attr:`groups`, provides easy access to the
|
||||
global status (or not) of the rule.
|
||||
|
||||
.. attribute:: domain_force
|
||||
|
||||
A predicate specified as a :ref:`domain <reference/orm/domains>`, the
|
||||
rule allows the selected operations if the domain matches the record,
|
||||
and forbids it otherwise.
|
||||
|
||||
The domain is a *python expression* which can use the following
|
||||
variables:
|
||||
|
||||
``time``
|
||||
Python's :mod:`python:time` module.
|
||||
``user``
|
||||
The current user, as a singleton recordset.
|
||||
``company_id``
|
||||
The current user's currently selected company as a single company id
|
||||
(not a recordset).
|
||||
``company_ids``
|
||||
All the companies to which the current user has access as a list of
|
||||
company ids (not a recordset), see
|
||||
:ref:`howto/company/security` for more details.
|
||||
|
||||
The :samp:`perm_{method}` have completely different semantics than for
|
||||
:class:`ir.model.access`: for rules, they specify which operation the rules
|
||||
applies *for*. If an operation is not selected, then the rule is not checked
|
||||
for it, as if the rule did not exist.
|
||||
|
||||
All operations are selected by default.
|
||||
|
||||
.. attribute:: perm_create
|
||||
.. attribute:: perm_read
|
||||
.. attribute:: perm_write
|
||||
.. attribute:: perm_unlink
|
||||
|
||||
.. _reference/security/rules/global:
|
||||
|
||||
Global rules versus group rules
|
||||
-------------------------------
|
||||
|
||||
There is a large difference between global and group rules in how they compose
|
||||
and combine:
|
||||
|
||||
* Global rules *intersect*, if two global rules apply then *both* must be
|
||||
satisfied for the access to be granted, this means adding global rules always
|
||||
restricts access further.
|
||||
* Group rules *unify*, if two group rules apply then *either* can be
|
||||
satisfied for the access to be granted. This means adding group rules can
|
||||
expand access, but not beyond the bounds defined by global rules.
|
||||
* The global and group rulesets *intersect*, which means the first group rule
|
||||
being added to a given global ruleset will restrict access.
|
||||
|
||||
.. danger::
|
||||
|
||||
Creating multiple global rules is risky as it's possible to create
|
||||
non-overlapping rulesets, which will remove all access.
|
||||
|
||||
.. _reference/security/fields:
|
||||
|
||||
|
@ -194,6 +194,8 @@ they're not going to get run:
|
||||
class SmallTest(unittest.TestCase):
|
||||
...
|
||||
|
||||
.. _reference/testing/tags:
|
||||
|
||||
Special tags
|
||||
^^^^^^^^^^^^
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) 2015-TODAY, Odoo S.A.
|
||||
# This file is distributed under the same license as the Odoo package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo 11.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-10-19 10:03+0200\n"
|
||||
"PO-Revision-Date: 2019-10-03 09:46+0000\n"
|
||||
"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ar\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||
|
||||
#: ../../applications.rst:3
|
||||
msgid "Applications"
|
||||
msgstr ""
|