[IMP] tutorials/server_framework_101: rewrite the tutorial

The Server Framework 101 (formerly Getting Started) is generally seen as
an interesting and rewarding tutorial, but also somewhat outdated and
too limited for beginners in Odoo development, as it fails to introduce
key concepts of the server framework (e.g., controllers, tests, etc.).
The instructions are also too directive for the reader to try and search
by themselves and learn from it.

With this commit, all of the content of the tutorial is rewritten while
keeping the objective of building a real estate module. The setup guide
for tutorials is also improved to ensure smoother onboarding for Odoo
employees and community members alike.

task-3802536
This commit is contained in:
Antoine Vandevenne (anv) 2024-05-06 17:52:16 +02:00
parent 0ae856bcf6
commit 32cba451c1
88 changed files with 3025 additions and 141 deletions

View File

@ -44,8 +44,8 @@ Git
basic knowledge of Git commands to proceed.
To clone a Git repository, choose between cloning with HTTPS or SSH. In most cases, the best option
is HTTPS. However, choose SSH to contribute to Odoo source code or when following the :doc:`Getting
Started developer tutorial </developer/tutorials/server_framework_101>`.
is HTTPS. However, choose SSH to contribute to Odoo source code or when following the
:doc:`/developer/tutorials/server_framework_101` tutorial.
.. tabs::

View File

@ -161,7 +161,10 @@ navigate to the directory where you installed Odoo from sources and follow the g
#. Select **<your_github_account>/odoo** or **<your_github_account>/enterprise** for the head
repository. Replace `<your_github_account>` with the name of the GitHub account on which you
created the fork or by **odoo-dev** if you work at Odoo.
#. Review your changes and click on the :guilabel:`Create pull request` button.
#. Click on :guilabel:`Create pull request` to create the :abbr:`PR (Pull Request)` and
automatically request a review from the code maintainers. If you wish to double-check your
changes first, or if you work at Odoo and follow an internal process for reviews, click on
:guilabel:`Create draft pull request`.
#. Tick the :guilabel:`Allow edits from maintainer` checkbox. Skip this step if you work at Odoo.
#. Complete the description and click on the :guilabel:`Create pull request` button again.

View File

@ -25,9 +25,13 @@ These guidelines should be applied to every new module and to all new developmen
going under major changes. In that case first do a **move** commit then apply
the changes related to the feature.
.. _contributing/coding_guidelines/module_structure:
Module structure
================
.. _contributing/coding_guidelines/module_structure/directories:
Directories
-----------
@ -46,6 +50,7 @@ Other optional directories compose the module.
- *report/* : contains the printable reports and models based on SQL views. Python objects and XML views are included in this directory
- *tests/* : contains the Python tests
.. _contributing/coding_guidelines/module_structure/file_naming:
File naming
-----------

View File

@ -2,6 +2,8 @@
Git guidelines
==============
.. _contributing/git_guidelines/configure_git:
Configure your git
------------------
@ -17,6 +19,8 @@ way towards making your commits more helpful:
- Be sure to add your full name to your Github profile here. Please feel fancy
and add your team, avatar, your favorite quote, and whatnot ;-)
.. _contributing/git_guidelines/message_structure:
Commit message structure
------------------------
@ -42,6 +46,8 @@ description. Try to follow the preferred structure for your commit messages
Closes #123 (close related PR on Github)
opw-123 (related to ticket)
.. _contributing/git_guidelines/commit_tag_module:
Tag and module name
-------------------
@ -73,6 +79,8 @@ various to tell it is cross-modules. Unless really required or easier avoid
modifying code across several modules in the same commit. Understanding module
history may become difficult.
.. _contributing/git_guidelines/commit_header:
Commit message header
---------------------
@ -86,6 +94,8 @@ Commit message header should make a valid sentence once concatenated with
archive users linked to active partners`` is correct as it makes a valid sentence
``if applied, this commit will prevent users to archive...``.
.. _contributing/git_guidelines/commit_body:
Commit message full description
-------------------------------

View File

@ -76,6 +76,8 @@ following attributes:
Requires an :term:`external id`, defaults to ``True``.
.. _reference/data/field:
``field``
---------
@ -203,6 +205,8 @@ Because some important structural models of Odoo are complex and involved,
data files provide shorter alternatives to defining them using
:ref:`record tags <reference/data/record>`:
.. _reference/data/shortcuts/menuitem:
``menuitem``
------------

View File

@ -213,12 +213,20 @@ These helpers are also available by importing `odoo.tools.date_utils`.
Relational Fields
~~~~~~~~~~~~~~~~~
.. _reference/fields/many2one:
.. autoclass:: Many2one()
.. _reference/fields/one2many:
.. autoclass:: One2many()
.. _reference/fields/many2many:
.. autoclass:: Many2many()
.. _reference/fields/command:
.. autoclass:: Command()
:members:
:undoc-members:

View File

@ -516,8 +516,8 @@ Structural components
Structural components provide structure or "visual" features with little logic. They are used as
elements or sets of elements in form views.
Form views accept the following children structural components: :ref:`group
<reference/view_architectures/form/group>`, :ref:`sheet <reference/view_architectures/form/sheet>`,
Form views accept the following children structural components: :ref:`sheet
<reference/view_architectures/form/sheet>`, :ref:`group <reference/view_architectures/form/group>`,
:ref:`notebook <reference/view_architectures/form/notebook>`,
:ref:`notebook <reference/view_architectures/form/notebook>`,
:ref:`newline <reference/view_architectures/form/newline>`,
@ -529,6 +529,24 @@ Form views accept the following children structural components: :ref:`group
Placeholders are denoted in all caps.
.. _reference/view_architectures/form/sheet:
`sheet`: make the layout responsive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `sheet` element can be used as a direct child of the :ref:`form
<reference/view_architectures/form>` root element for a narrower and more responsive form layout
(centered page, margin...). It usually contains :ref:`group
<reference/view_architectures/form/group>` elements.
.. code-block:: xml
<form>
<sheet>
...
</sheet>
</form>
.. _reference/view_architectures/form/group:
`group`: define columns layouts
@ -615,24 +633,6 @@ The `group` element can have the following attributes:
</group>
</group>
.. _reference/view_architectures/form/sheet:
`sheet`: make the layout responsive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `sheet` element can be used as a direct child of the :ref:`form
<reference/view_architectures/form>` root element for a narrower and more responsive form layout
(centered page, margin...). It usually contains :ref:`group
<reference/view_architectures/form/group>` elements.
.. code-block:: xml
<form>
<sheet>
...
</sheet>
</form>
.. _reference/view_architectures/form/notebook:
`notebook` & `page`: add tabbed sections

View File

@ -191,7 +191,7 @@ When the data to create is more complex it can be useful, or even necessary, to
Data Extension
~~~~~~~~~~~~~~
During the Core Training, we saw in the :doc:`server_framework_101/12_inheritance` chapter we
During the Core Training, we saw in the :doc:`server_framework_101_legacy/12_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.

View File

@ -7,7 +7,7 @@ Build PDF Reports
completed it and use the `estate` module you have built as a base for the exercises in this
tutorial.
We were previously :doc:`introduced to QWeb <server_framework_101/14_qwebintro>`
We were previously :doc:`introduced to QWeb <server_framework_101_legacy/14_qwebintro>`
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
@ -178,7 +178,7 @@ Its contents are all explained in :ref:`the documentation <reference/actions/rep
An ``ir.actions.report`` is primarily used via the Print menu of a model's view. In the practical
example, the ``binding_model_id`` specifies which model's views the report should show, and Odoo
will auto-magically add it for you. Another common use case of the report action is to link it to
a button as we learned in :doc:`server_framework_101/09_actions`. This is handy for reports
a button as we learned in :doc:`server_framework_101_legacy/09_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".

View File

@ -15,7 +15,7 @@ currently,
update or delete properties, property types, or property tags.
* If ``estate_account`` is installed then only agents allowed to interact
with invoicing can confirm sales as that's necessary to :ref:`create an
invoice <tutorials/server_framework_101/13_other_module/create>`.
invoice <tutorials/server_framework_101_legacy/13_other_module/create>`.
However:
@ -140,8 +140,6 @@ Access Rights
real-estate application.
- Real-estate agents will not be able to update the property types or tags.
Access rights were first introduced in :doc:`server_framework_101/04_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.

View File

@ -1,4 +1,5 @@
:show-content:
:hide-page-toc:
====================
Server framework 101
@ -10,16 +11,23 @@ Server framework 101
server_framework_101/*
Welcome to the Server framework 101 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.
**Welcome to the Server framework 101 tutorial!**
The goal of this tutorial is for you to get an insight of the most important parts of the Odoo
development framework while developing your own Odoo module to manage real estate assets. 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.
Are you eager to learn about the server framework of Odoo? Perhaps you have recently joined Odoo in
a technical role. Either way, this tutorial will level you up as an Odoo developer and show you the
ropes of working with the server framework.
You will embark on a step-by-step journey, building a real estate asset management application as
you explore the core concepts of the server framework. Each chapter of the tutorial builds upon the
previous one, so make sure to follow them in order.
.. todo: insert picture of the final view
.. note::
Throughout this tutorial, you will encounter exercises designed to reinforce your learning. There
will often be multiple ways to approach these exercises, so feel free to explore your own ideas
and solutions. This is the secret to secure your understanding and develop your problem-solving
skills.
.. important::
Before going further, make sure you have prepared your development environment with the
@ -27,18 +35,13 @@ one.
Ready? Let's get started!
* :doc:`server_framework_101/01_architecture`
* :doc:`server_framework_101/02_newapp`
* :doc:`server_framework_101/03_basicmodel`
* :doc:`server_framework_101/04_securityintro`
* :doc:`server_framework_101/05_firstui`
* :doc:`server_framework_101/06_basicviews`
* :doc:`server_framework_101/07_relations`
* :doc:`server_framework_101/08_compute_onchange`
* :doc:`server_framework_101/09_actions`
* :doc:`server_framework_101/10_constraints`
* :doc:`server_framework_101/11_sprinkles`
* :doc:`server_framework_101/12_inheritance`
* :doc:`server_framework_101/13_other_module`
* :doc:`server_framework_101/14_qwebintro`
* :doc:`server_framework_101/15_final_word`
- :doc:`server_framework_101/01_architecture_overview`
- :doc:`server_framework_101/02_lay_the_foundations`
- :doc:`server_framework_101/03_build_user_interface`
- :doc:`server_framework_101/04_relational_fields`
- :doc:`server_framework_101/05_business_logic`
- :doc:`server_framework_101/06_security`
- :doc:`server_framework_101/07_advanced_views`
- :doc:`server_framework_101/08_inheritance`
- :doc:`server_framework_101/09_portal`
- :doc:`server_framework_101/10_unit_testing`

View File

@ -0,0 +1,72 @@
================================
Chapter 1: Architecture overview
================================
Before we start building our app, let's take a high level glance at the architecture of Odoo.
.. _tutorials/server_framework_101/multitier_app:
Multitier application
=====================
Odoo leverages a `multitier architecture <https://en.wikipedia.org/wiki/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_overview/three-tier-architecture.svg
:align: center
:alt: Overview of a three-tier application
The presentation tier of Odoo 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 :abbr:`RDBMS
(Relational Database Management System Software)`.
Depending on the scope of your Odoo development, it 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. There are plenty of tutorials that are freely
accessible, so we cannot recommend one over another since it depends on your background. For
reference, this is the official `Python tutorial <https://docs.python.org/3/tutorial/>`_.
.. _tutorials/server_framework_101/modules:
Odoo modules
============
Odoo relies on modular components called **modules** to extend its functionality. These modules are
essentially self-contained packages of code and data that serve a specific purpose within the
system. You can think of them as building blocks.
Modules offer two main ways to customize Odoo:
- Adding new functionality: You can create entirely new features with modules, such as a real-time
bus fleet visualization module.
- Extending existing functionality: Modules can also be used to modify or enhance existing Odoo
features, like adding your country's accounting rules to the generic accounting support.
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 are
not Apps.
- *Modules* may also be referred to as *addons*.
In practice, modules are represented by directories. They are placed in a designated location called
the **addons path**, which the server scans to discover available modules.
In the :doc:`setup guide <../setup_guide>`, we cloned the `odoo/tutorials` repository and included
it in the `addons-path` argument when starting the server. The directories present in the repository
are all modules that can be installed on your Odoo server.
.. exercise::
In your file explorer, navigate to the `odoo/tutorials` repository and inspect the available
modules. You should find the `real_estate` module in which we will build our real estate
application throughout this tutorial.
You must have noticed that the module directories are not empty; they all contain at least two
essential files: :file:`__init__.py` and :file:`__manifest__.py`. These files are what makes a
simple directory an Odoo module. We'll get back to it in the next chapter.
----
Ready to start? Let's now :doc:`lay the foundations <02_lay_the_foundations>` of our first Odoo app!

View File

@ -0,0 +1,573 @@
==============================
Chapter 2: Lay the foundations
==============================
In this chapter, we'll focus on the fundamental building blocks of Odoo's data structure: models,
fields, and records. We'll use them together to lay the foundations of our real estate application.
.. _tutorials/server_framework_101/install_app:
Install the app
===============
If you followed the :doc:`setup guide <../setup_guide>` carefully, you should now have a running
Odoo server with the `odoo/tutorials` repository in the `addons-path`, and be logged in as an
administrator.
**Let's install our real estate app!** In your browser, open Odoo and navigate to
:menuselection:`Apps`. Search for :guilabel:`Real Estate` and click :guilabel:`Activate`.
Nothing has changed? That's normal; the `real_estate` module you just installed is currently an
empty shell. It doesn't even have a menu entry. Currently, it only contains three files:
- An empty :file:`__init__.py` file to make Python treat the :file:`real_estate` directory as a
package.
- The :file:`__manifest__.py` file that declares the :file:`real_estate` Python package as an Odoo
module.
- The optional :file:`static/description/icon.png` file that serves as the app icon.
The first two files are the minimum requirements for a directory to be considered an Odoo module.
.. exercise::
Search for each key of the :file:`real_estate/__manifest__.py` file in the :ref:`reference
documentation <reference/module/manifest>` and understand the base metadata that defines the
`real_estate` module.
.. seealso::
`The manifest file of the Sales app <{GITHUB_PATH}/addons/sale/__manifest__.py>`_
.. _tutorials/server_framework_101/create_first_model:
Create the first model
======================
Now that our module is recognized by Odoo, it's time to build towards the business features. Data is
essential for any real-world application, and our real estate application won't be an exception.
To store data effectively, we need two things: a way to define the structure of that data, and a
system to store and manipulate it. Fortunately, the server framework of Odoo comes equipped with the
perfect tool: the :abbr:`ORM (Object Relational Mapper)` layer. It's the ORM that takes care of
preparing and executing SQL queries for you when you manipulate data in Python.
The ORM simplifies data access and manipulation by allowing you to define **models**. Models act as
blueprints for your data; they define the structure and organization of information within your
module. You can see them as templates that specify what kind of data your module will handle. For
example, real estate properties, owners, or tenants. In Odoo, models are represented by Python
classes that inherit from the `odoo.models.Model` class provided by the ORM, and they are identified
by their `_name` attribute. In addition to `_name`, models also accept the `_description` attribute
to define a human-readable description of the model.
Each model is made of smaller components called **fields**. You can see them as the individual
characteristics that describe your data. Fields allow you to define the relevant data your
application needs to capture and manage. In the case of real estate properties, some example fields
could be the property name, the type (house, apartment, etc.), and the floor area. Within model
classes, fields are defined as class attributes. Each field is an instance of a class from the
`odoo.fields` package. For example, `Char`, `Float`, `Boolean`, each designed to handle different
types of data. When defining a field, developers can pass various arguments to finely control how
data is handled and presented in Odoo. For example, `string` defines the label for the field in the
user interface, `help` provides a tooltip when hovering the field in the user interface, and
`required` makes filling in the field mandatory.
Individual data entries are called **records**. They are based on the structure defined by models
and contain the actual data for each field specified in the model. In Python, records are
represented as instances of the model's class, allowing developers to interact with data using
object-oriented programming techniques. For example, in a real estate application using a tenant
model, each specific tenant (such as "Bafien Carpink") would be a separate record of that model.
.. seealso::
For the full list of fields and their attributes, see the :ref:`reference documentation
<reference/orm/fields>`.
.. example::
Before we dive into creating our own models, let's take a look at a basic example of a model that
represents storable products. It defines a `product` model with the `Product` class inheriting
from `models.Model`. Within this class, several fields are defined to capture product data:
.. code-block:: python
from odoo import fields, models
class Product(models.Model):
_name = 'product'
_description = "Storable Product"
name = fields.Char(string="Name", required=True)
description = fields.Text(string="Description")
price = fields.Float(string="Sale Price", required=True)
category = fields.Selection(
string="Category",
help="The category of the product; if none are suitable, select 'Other'.",
selection=[
('apparel', "Clothing")
('electronics', "Electronics"),
('home_decor', "Home Decor"),
('other', "Other"),
],
required=True,
default='apparel',
)
.. note::
- `name` is a `Char` field while `description` is a `Text` field; `Char` fields are typically
used for short texts, whereas `Text` fields can hold longer content and multiple lines.
- The label of the `price` field is arbitrary and doesn't have to be the upper-cased version
of the attribute name.
- `category` is a `Selection` field with predefined options, each defined by a technical code
and a corresponding label. Since it is required, a default value is provided.
Building on these new concepts, let's now create the first model for our real estate app. We'll
create a model with some fields to represent real estate properties and their characteristics.
.. exercise::
#. Create a new :file:`real_estate_property.py` file at the root of the `real_estate` module.
#. Update the :file:`real_estate/__init__.py` file to relatively import the
:file:`real_estate_property.py` file, like so:
.. code-block:: python
from . import real_estate_property
#. Define a new model with `real.estate.property` as `_name` and a short `_description`.
#. Add fields to represent the following characteristics:
- Name (required)
- Description
- Image (max 600x400 pixels)
- Active (default to true)
- State (new, offer received, under option, or sold; required; default to new)
- Type (house, apartment, office building, retail space, or warehouse; required; default to
house)
- Selling Price (without currency; with help text; required)
- Availability Date (default to creation date + two months)
- Floor Area (in square meters; with help text)
- Number of Bedrooms (default to two)
- Whether there is a garden
- Whether there is a garage
.. tip::
- The class name doesn't matter, but the convention is to use the model's upper-cased `_name`
(without dots).
- Rely on the reference documentation for :ref:`fields <reference/orm/fields>` to select the
right class and attributes for each field.
.. spoiler:: Solution
.. code-block:: python
:caption: `__init__.py`
from . import real_estate_property
.. code-block:: python
:caption: `real_estate_property.py`
from odoo import fields, models
from odoo.tools import date_utils
class RealEstateProperty(models.Model):
_name = 'real.estate.property'
_description = "Real Estate Property"
name = fields.Char(string="Name", required=True)
description = fields.Text(string="Description")
image = fields.Image(string="Image")
active = fields.Boolean(string="Active", default=True)
state = fields.Selection(
string="State",
selection=[
('new', "New"),
('offer_received', "Offer Received"),
('under_option', "Under Option"),
('sold', "Sold"),
],
required=True,
default='new',
)
type = fields.Selection(
string="Type",
selection=[
('house', "House"),
('apartment', "Apartment"),
('office', "Office Building"),
('retail', "Retail Space"),
('warehouse', "Warehouse"),
],
required=True,
default='house',
)
selling_price = fields.Float(
string="Selling Price", help="The selling price excluding taxes.", required=True
)
availability_date = fields.Date(
string="Availability Date", default=date_utils.add(fields.Date.today(), months=2)
)
floor_area = fields.Integer(
string="Floor Area", help="The floor area in square meters excluding the garden."
)
bedrooms = fields.Integer(string="Number of bedrooms", default=2)
has_garden = fields.Boolean(string="Garden")
has_garage = fields.Boolean(string="Garage")
Congrats, you have just defined the first model of our real estate app! However, the changes have
not yet been applied to the database. To do so, you must add the `-u real_estate` argument to the
server start-up command and restart the server. The :option:`-u <odoo-bin --update>` argument
instructs the server to update the specified modules at start-up.
.. _tutorials/server_framework_101/inspect_sql_table:
Inspect the SQL table
=====================
Earlier, we quickly introduced models as a convenient way to store and handle data in Odoo. In fact,
these models not only define the structure and behavior of data in Python, but they also correspond
to SQL tables in the database. The `_name` attribute of their model is taken (with dots replaced by
underscores) as the name of the corresponding table. For example, the `real.estate.property` model
is linked to the `real_estate_property` table.
The same goes for fields that become columns in the SQL table of their model. The name of the class
attribute representing the field is taken as the column name while the field's class determines the
column type.
Once the server is running again, let's take a look in the database and see how the model and fields
you created translate into a new SQL table. We will use `psql`, the CLI
:dfn:`command-line interface` allowing to browse and interact with PostgreSQL databases.
.. exercise::
#. In your terminal, execute the command :command:`psql -d tutorials`.
#. Enter the command :command:`\\d real_estate_property` to print the description of the
`real_estate_property` table.
#. For each field of the `real.estate.property` model, try to understand how the field's
attributes alter the table.
#. Enter the command :command:`exit` to exit `psql`.
.. spoiler:: Solution
.. code-block:: text
$ psql -d tutorials
tutorials=> \d real_estate_property
Table "public.real_estate_property"
Column | Type | Collation | Nullable | Default
-------------------+-----------------------------+-----------+----------+--------------------------------------------------
id | integer | | not null | nextval('real_estate_property_id_seq'::regclass)
floor_area | integer | | |
bedrooms | integer | | |
create_uid | integer | | |
write_uid | integer | | |
name | character varying | | not null |
state | character varying | | not null |
type | character varying | | not null |
availability_date | date | | |
description | text | | |
active | boolean | | |
has_garden | boolean | | |
has_garage | boolean | | |
create_date | timestamp without time zone | | |
write_date | timestamp without time zone | | |
selling_price | double precision | | not null |
Indexes:
"real_estate_property_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"real_estate_property_create_uid_fkey" FOREIGN KEY (create_uid) REFERENCES res_users(id) ON DELETE SET NULL
"real_estate_property_write_uid_fkey" FOREIGN KEY (write_uid) REFERENCES res_users(id) ON DELETE SET NULL
exit
- Each field, except the image that is saved as an attachment, is represented in the
`real_estate_property` SQL table by a column whose type is determined by the field's class:
+--------------------+----------------------+
| Field class | Column type |
+====================+======================+
| `fields.Integer` | `integer` |
+--------------------+----------------------+
| `fields.Float` | `double precision` |
+--------------------+----------------------+
| `fields.Char` | `character varying` |
+--------------------+----------------------+
| `fields.Text` | `text` |
+--------------------+----------------------+
| `fields.Selection` | `character varying` |
+--------------------+----------------------+
| `fields.Boolean` | `boolean` |
+--------------------+----------------------+
| `fields.Date` | `date` |
+--------------------+----------------------+
- The `required` attribute of a field prevents the corresponding column to be nullable.
- The `default` attribute of a field does *not* set a default value on the column; instead, it's
the ORM that takes care of setting default values for newly created records.
You might be surprised to find that the generated SQL table contains more columns than just the
fields you defined. That is because Odoo automatically adds several readonly :dfn:`you can read but
not write` fields to each model for internal purposes. Here are some additional fields you'll
typically find:
- `id`: A unique identifier that is automatically computed for each new record.
- `create_date`: The timestamp of when the record was created.
- `create_uid`: The ID of the user who created the record.
- `write_date`: The timestamp of the last modification to the record.
- `write_uid`: The ID of the user who last modified the record.
.. seealso::
:ref:`Reference documentation for automatic fields <reference/fields/automatic>`
.. _tutorials/server_framework_101/load_data_files:
Load data files
===============
Now that we have created our first model, let's consider an important question: What's missing from
our database? The answer is simple: data!
While we could create new records directly from the user interface, this approach has some
limitations. It would be quite tedious and time-consuming, especially for large amounts of data, and
the changes would only affect the current database.
.. _tutorials/server_framework_101/xml_data_files:
XML data files
--------------
Fortunately, the server framework allows for a different approach: describe data operations in XML
format in so-called **data files** that the server automatically loads at start-up in sequential
order. This automates the process of populating the database, saving time and effort, and allows
developers to include default data or configurations directly in their modules.
The most common data operation is creating new records through the `record` and `field` XML
elements, but other operations exist, such as `delete`, which deletes previously created records, or
even `function`, which allows executing arbitrary code.
.. seealso::
:doc:`Reference documentation for XML data files <../../reference/backend/data>`
Some data operations require their data elements to be uniquely identified by the system. This is
achieved by means of the `id` attribute, also known as the **XML ID** or **external identifier**. It
provides a way for other elements to reference it with the `ref` attribute and links data elements
to the records they create or update. XML IDs are automatically prefixed with their module name when
created from a data file so that records can be referenced by their full XML ID `<module>.<id>`.
.. example::
Let's again take the `product` model as an example and describe a few product records in a data
file.
.. code-block:: xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="coffee_table" model="product">
<field name="name">Coffee table</field>
<field name="description">A dark wood table easy to match with other furnishing.</field>
<field name="price">275</field>
<field name="category">home_decor</field>
</record>
<record id="product.tshirt" model="product">
<field name="name">T-shirt</field>
<field name="price">29.99</field>
<field name="shop_id" ref="product.tshirt_shop"/>
</record>
</odoo>
.. note::
As we can see, data files are rather straightforward:
- The root element must be `odoo`.
- Multiple data operations can be described inside a single `odoo` element.
- The `id` attribute can be written with the module prefix included for clarity.
- Required fields must be provided a value if they don't have a default one.
- Non-required fields can be omitted.
- The `ref` attribute is used to reference other records by their XML ID and use their record
ID as value.
Let's now load some default real estate properties in our database.
.. exercise::
#. Create a new :file:`real_estate_property_data.xml` file at the root of the `real_estate`
module.
#. Update the manifest to let the server know that it should load our data file. To do so, have
the `data` key list our data file name.
#. Use the `record` and `field` data operation to describe at least three default properties
records. Try to vary property types and set different values than the default ones. Add the
image files for the various properties at the root of the `real_estate` module and assign them
to the properties' Image field.
#. Restart the server, again with the `-u real_estate` argument, to load the module data at
server start-up.
#. In the terminal, execute the command `psql -d tutorials` and enter the command
`SELECT * FROM real_estate_property;` to verify that the records were loaded.
.. spoiler:: Solution
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 2
'data': [
'real_estate_property_data.xml',
],
.. code-block:: text
:caption: `country_house.png`
<binary data>
.. code-block:: text
:caption: `loft.png`
<binary data>
.. code-block:: text
:caption: `mixed_use_commercial.png`
<binary data>
.. code-block:: xml
:caption: `real_estate_property_data.xml`
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.country_house" model="real.estate.property">
<field name="name">Country house</field>
<field name="description">In the charming village of Grand-Rosière-Hottomont, 5 minutes from all facilities (shops, schools, public transport, ...), we offer this superb newly renovated country house!</field>
<field name="image" type="base64" file="real_estate/country_house.png"/>
<field name="type">house</field>
<field name="selling_price">745000</field>
<field name="floor_area">416</field>
<field name="bedrooms">5</field>
<field name="has_garden">True</field>
<field name="has_garage">True</field>
</record>
<record id="real_estate.loft" model="real.estate.property">
<field name="name">Loft</field>
<field name="description">Located on the 1st floor of a small, fully renovated building, magnificent 195 m² three-bedroom apartment with parking space.</field>
<field name="image" type="base64" file="real_estate/loft.png"/>
<field name="type">apartment</field>
<field name="selling_price">339000</field>
<field name="availability_date">2025-01-01</field>
<field name="floor_area">195</field>
<field name="bedrooms">3</field>
<field name="has_garden">False</field>
<field name="has_garage">True</field>
</record>
<record id="real_estate.mixed_use_commercial" model="real.estate.property">
<field name="name">Mixed use commercial building</field>
<field name="description">The property is a former bank agency which consists of a retail ground floor, a basement and 2 extra office floors.</field>
<field name="image" type="base64" file="real_estate/mixed_use_commercial.png"/>
<field name="type">retail</field>
<field name="selling_price">335000</field>
<field name="availability_date">2024-10-02</field>
<field name="floor_area">370</field>
<field name="bedrooms">0</field>
<field name="has_garden">False</field>
<field name="has_garage">False</field>
</record>
</odoo>
.. _tutorials/server_framework_101/csv_data_files:
CSV data files
--------------
In addition to XML data files, the server framework allows loading data files in CSV format. This
format is often more convenient for describing records with simple field values belonging to the
same model. It also loads faster, making it the go-to format when performance matters most.
.. seealso::
:ref:`Reference documentation for CSV data files <reference/data/csvdatafiles>`
.. example::
See below for an example of how a subset of `country states can be loaded into Odoo
<{GITHUB_PATH}/odoo/addons/base/data/res.country.state.csv>`_.
.. code-block:: csv
:caption: `res.country.state.csv`
"id","country_id:id","name","code"
state_ca_ab,ca,"Alberta","AB"
state_ca_bc,ca,"British Columbia","BC"
state_ca_mb,ca,"Manitoba","MB"
state_ca_nb,ca,"New Brunswick","NB"
state_ca_nl,ca,"Newfoundland and Labrador","NL"
state_ca_nt,ca,"Northwest Territories","NT"
state_ca_ns,ca,"Nova Scotia","NS"
state_ca_nu,ca,"Nunavut","NU"
state_ca_on,ca,"Ontario","ON"
state_ca_pe,ca,"Prince Edward Island","PE"
state_ca_qc,ca,"Quebec","QC"
state_ca_sk,ca,"Saskatchewan","SK"
state_ca_yt,ca,"Yukon","YT"
.. note::
- The file name must match the model name.
- The first line lists the model fields to populate.
- XML IDs are specified via the special `id` field.
- The `:id` suffix is used to reference other records by their XML ID and use their record ID
as value.
- Each subsequent line describes one new record.
In business applications like Odoo, one of the first questions to consider is who can access the
data. By default, access to newly created models is restricted until it is explicitly granted.
Granting access rights is done by creating records of the `ir.model.access` model, which specifies
who has access to which model.
The topic of security will be covered in detail in :doc:`../restrict_data_access`. For now, we'll
just give ourselves access rights to the `real.estate.property` model to get rid of the warning that
began being logged at server start-up after creating the model:
.. code-block:: text
WARNING tutorials odoo.modules.loading: The models ['real.estate.property'] have no access rules [...]
.. exercise::
#. Create a new :file:`ir.model.access.csv` file at the root of the `real_estate` module.
#. Declare it in the manifest as you did for the :file:`real_estate_property_data.xml` file.
#. Grant access to the `real.estate.property` model to all administrators of the database by
adding new access rights with the following specifications:
- XML ID: `real_estate_property_system`
- `name`: `real.estate.property.system`
- `model_id`: The record ID of `model_real_estate_property`
- `group_id`: The record ID of `base.group_system`
- `perm_read`, `perm_write`, `perm_create`, and `perm_unlink`: `1`
.. tip::
In Odoo, modules and models are automatically given an XML ID computed by prefixing their name
with `module_` and `model_` respectively.
.. spoiler:: Solution
.. code-block:: py
:caption: `__manifest__.py`
:emphasize-lines: 2
'data': [
'ir.model.access.csv',
'real_estate_property_data.xml',
],
.. code-block:: csv
:caption: `ir.model.access.csv`
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
real_estate_property_system,real.estate.property.system,model_real_estate_property,base.group_system,1,1,1,1
After restarting the server, the warning should no longer appear.
----
In the next chapter, we'll :doc:`create user interface elements <03_build_user_interface>` to
interact with the property model.

View File

@ -0,0 +1,709 @@
===================================
Chapter 3: Build the user interface
===================================
In this chapter, we will bring our application to life by building a :abbr:`UI (User Interface)`
that allows users to interact with the models. This will require defining menu items, actions, and
views. By the end of this chapter, we will have a fully functional interface to manage real estate
properties!
.. _tutorials/server_framework_101/menu_items:
Add menus items
===============
**Menus items** are the first thing users see and interact with. They give users access to the
different parts of Odoo and can be nested to form a hierarchical structure. This allows the
functionalities of complex applications to be organized into categories and sub-categories and makes
them easier to navigate. The top level of the menu structure typically contains the menu items for
the main applications (like "Contacts", "Sales", and "Accounting"). These top-level menu items can
also be visually enhanced with custom icons for better recognition.
Menu items can take on two distinct roles:
- **Category menu items**: They serve as organizational containers and simply expand to show their
submenu when clicked.
- **Action menu items**: They trigger a specific action in the UI when clicked. While menu items
often trigger actions, they are separate entities. The same action can be triggered by different
menu items, or even buttons.
In Odoo, menu items are actually records of the `ir.ui.menu` model whose key fields are:
.. rst-class:: o-definition-list
`name` (required)
The title of the menu item.
`sequence`
Determines the ordering of same-level menu items.
`parent_id`
The ID of the parent menu item's record.
`web_icon`
The path to the icon file to use as a menu item icon.
`action`
The action to trigger when the menu item is clicked.
Like for any other model, one can automatically create records of the `ir.ui.menu` model by means of
a data file. Lets do just that and add menu items to our real estate app!
.. exercise::
#. Create and declare a new :file:`menus.xml` file at the root of the `real_estate` module.
#. Describe a new "Real Estate" menu item to serve as root menu for our real estate app.
- Leave the `parent_id` field empty to place the menu item in the top-level menu.
- Use the `static/description/icon.png` file as `web_icon`, in the format
`<module>,<icon_file_path>`.
#. Nest new "Properties" and "Settings" menu items under the root menu item. As we have not yet
created an action to browse properties or open settings, reference the following existing
actions instead:
- `base.open_module_tree` that opens the list of modules.
- `base.action_client_base_menu` that opens the general settings.
.. spoiler:: Solution
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 3
'data': [
'ir.model.access.csv',
'menus.xml',
'real_estate_property_data.xml',
],
.. code-block:: xml
:caption: `menus.xml`
<?xml version="1.0"?>
<odoo>
<record id="real_estate.root_menu" model="ir.ui.menu">
<field name="name">Real Estate</field>
<field name="web_icon">real_estate,static/description/icon.png</field>
</record>
<record id="real_estate.properties_menu" model="ir.ui.menu">
<field name="name">Properties</field>
<field name="sequence">10</field>
<field name="parent_id" ref="real_estate.root_menu"/>
<field name="action" ref="base.open_module_tree"/>
</record>
<record id="real_estate.settings_menu" model="ir.ui.menu">
<field name="name">Settings</field>
<field name="sequence">20</field>
<field name="parent_id" ref="real_estate.root_menu"/>
<field name="action" ref="base.action_client_base_menu"/>
</record>
</odoo>
If you go to the app switcher :dfn:`the top-level menu of Odoo`, you should now see a menu item for
our real estate app! Click it to open the app and automatically trigger the first action in its
sub-menu. If you referenced the `base.open_module_tree` action, you should now see a list of Odoo
modules.
.. _tutorials/server_framework_101/menuitem_shortcut:
Use the `menuitem` shortcut
---------------------------
As an application grows in size, so do its menus, and it becomes increasingly complicated to define
and nest menu items. While defining menu items using the `record` data operation works perfectly
fine, the server framework provides a shortcut that makes the process easier and more intuitive,
especially for nesting menu items: the `menuitem` data operation.
The `menuitem` tag is a special XML element that is specifically designed for creating menu items;
it simplifies the syntax and automatically handles some technical details for you.
.. example::
Our fictional `product` module could define menu items as follows:
.. code-block:: xml
<menuitem id="product.root_menu" name="Product" web_icon="product,static/description/product.png">
<menuitem id="product.all_products_menu" name="All Products" sequence="1" action="product.view_all_products_action"/>
<menuitem id="product.new_products_menu" name="New Products" sequence="2" action="product.view_new_products_action"/>
</menuitem>
.. note::
- The outer `menuitem` data operation creates the top-level "Product" menu item.
- The specifications (`name`, `web_icon`, `sequence`, `action`, ...) of menu items are set
through attributes of the XML element.
- The menu items hierarchy is defined by nesting their XML elements.
Why keep complex code when you can simplify it? It's already time for our first **code
refactoring**!
.. exercise::
Rewrite the description of the menu items of our real estate app using the `menuitem` data
operation instead of `record`.
.. spoiler:: Solution
.. code-block:: xml
:caption: `menus.xml`
:emphasize-lines: 4-21
<?xml version="1.0"?>
<odoo>
<menuitem
id="real_estate.root_menu"
name="Real Estate"
web_icon="real_estate,static/description/icon.png"
>
<menuitem
id="real_estate.properties_menu"
name="Properties"
sequence="1"
action="base.open_module_tree"
/>
<menuitem
id="real_estate.settings_menu"
name="Settings"
sequence="2"
action="base.action_client_base_menu"
/>
</menuitem>
</odoo>
.. _tutorials/server_framework_101/define_window_actions:
Define window actions
=====================
**Actions** define what happens when a user interacts with the UI, such as clicking a menu item.
They connect the user interface with the underlying business logic. There exist different types of
actions in Odoo, the most common one being **window actions** (`ir.actions.act_window`), that
display the records of a specific model in a view. Other types of actions allow for different
behaviors, like **URL actions** that open URLs (`ir.actions.act_url`) or **server actions**
(`ir.actions.server`) that execute custom code.
In Odoo, actions can be stored in the database as records or returned as Python dictionaries
interpreted as action descriptors when business logic is executed. Window actions are described by
the `ir.actions.act_window` model whose key fields include:
.. rst-class:: o-definition-list
`name` (required)
The title of the action; is often used as the page title.
`res_model` (required)
The model on which the action operates.
`view_mode`
A comma-separated list of view types to enable for this action; for example, `list,form,kanban`.
`help`
An optional help text for the users when there are no records to display.
.. seealso::
:doc:`Reference documentation for actions <../../reference/backend/actions>`
.. example::
The example below defines an action to open existing products in either list or form view.
.. code-block:: xml
<record id="product.view_products_action" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="res_model">product</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new product.
</p>
</field>
</record>
.. note::
The content of the `help` field can be written in different formats thanks to the `type`
attribute of the :ref:`field <reference/data/field>` data operation.
As promised, we'll finally get to interact with our real estate properties in the UI. All we need
now is an action to assign to the menu item.
.. exercise::
#. Create and declare a new :file:`actions.xml` file at the root of the `real_estate` module.
#. Describe a new "Properties" window action that opens `real.estate.property` records in list
and form views, and assign it to the "Properties" menu item. Be creative with the help text!
For reference, the list of supported classes can be found in the `view.scss
<{GITHUB_PATH}/addons/web/static/src/views/view.scss>`_ file.
.. tip::
Pay attention to the declaration order of data files in the manifest; you might introduce a
data operation that depends on another one.
.. spoiler:: Solution
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 2,4
'data': [
'actions.xml',
'ir.model.access.csv',
'menus.xml', # Depends on `actions.xml`
'real_estate_property_data.xml',
],
.. code-block:: xml
:caption: `actions.xml`
<?xml version="1.0"?>
<odoo>
<record id="real_estate.view_properties_action" model="ir.actions.act_window">
<field name="name">Properties</field>
<field name="res_model">real.estate.property</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<!-- Turns out I didn't feel like being creative with the help text ¯\_(ツ)_/¯ -->
<p class="o_view_nocontent_smiling_face">
Create a new property.
</p>
</field>
</record>
</odoo>
.. code-block:: xml
:caption: `menus.xml`
:emphasize-lines: 5
<menuitem
id="real_estate.properties_menu"
name="Properties"
sequence="10"
action="real_estate.view_properties_action"
/>
Clicking the "Properties" menu item now displays a list view of the default properties we created
earlier. As we specified in the action that both list and form views were allowed, you can click any
property record to display its form view. Delete all three records to see the help text you created.
.. _tutorials/server_framework_101/create_custom_views:
Create custom views
===================
**Views** are the UI's building blocks, defining how model data is displayed on screen. They are
structures written in XML that describe the layout and behavior of various UI components.
Odoo supports different types of views, each serving a different purpose. The most common types
include **list views** for listing multiple records in a table-like format, **form views** for
displaying and editing individual records, **kanban views** for presenting records in a card layout,
and **search views** for defining search and filtering options.
In Odoo, views are records of the `ir.ui.view` model. Each view is associated with a specific model,
determining which data it displays and interacts with. Key fields include:
.. rst-class:: o-definition-list
`name` (required)
A unique name for the view.
`model` (required)
The model the view is associated with.
`arch` (required)
The view architecture as an XML string.
The `arch` field holds the view's XML architecture, which is composed of a root element determining
the type of the view, and various inner components that depend on the view type. The root element
(e.g., `list`, `form`, `search`) defines the view type, while the inner components describe the
structure and content of the view. These components can be structural (like `sheet` that makes the
layout responsive, or `group` that defines column layouts) or semantic (like `field` that displays
field labels and values).
.. seealso::
- :doc:`Reference documentation for view records <../../reference/user_interface/view_records>`
- :doc:`Reference documentation for view architectures
<../../reference/user_interface/view_architectures>`
.. example::
The following examples demonstrate how to define simple list, form and search views for the
`product` model.
.. code-block:: xml
:caption: A list view for `product`
<record id="product_list" model="ir.ui.view">
<field name="name">Product List</field>
<field name="model">product</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="price"/>
<field name="category"/>
</list>
</field>
</record>
.. code-block:: xml
:caption: A form view for `product`
<record id="product_form" model="ir.ui.view">
<field name="name">Product Form</field>
<field name="model">product</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="description"/>
<field name="price"/>
<field name="category"/>
</group>
</sheet>
</form>
</field>
</record>
.. code-block:: xml
:caption: A search view for `product`
<record id="product_search" model="ir.ui.view">
<field name="name">Product Search</field>
<field name="model">product</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="description"/>
</search>
</field>
</record>
.. note::
- The XML structure differs between view types.
- The `description` field is omitted from the list view because it wouldn't fit visually.
In :ref:`the previous section <tutorials/server_framework_101/define_window_actions>`, we defined
the `view_mode` of our action to display `real.estate.property` records in list and form view.
Although we haven't created the corresponding views yet, the server framework had our back and
automatically provided generic views. The generic list and form views were hard to miss, but a
generic search view was also provided; when searching for properties, you are in fact searching on
property names because it's the only field of the generic view.
However convenient, we should almost never rely on these generic views in business applications.
They are incomplete, badly structured, and often use the wrong field widgets. Let's create our own
custom views for a better :abbr:`UX (User experience)`.
.. _tutorials/server_framework_101/list_view:
List view
---------
For a start, the list view could use more fields than just the name.
.. exercise::
#. Create a new :file:`real_estate_property_views.xml` file at the root of the `real_estate`
module.
#. Create a custom list view to display the following fields of the `real.estate.property` model
in the given order: name, state, type, selling price, availability date, floor area, number of
bedrooms, presence of a garden, and presence of a garage.
#. Make the visibility of the floor area and all following fields optional so that only the floor
area is visible by default, while the remaining fields are hidden by default and must be
displayed by accessing the view's column selector (:icon:`oi-settings-adjust` button).
#. After restarting the server to load the new data, refresh the browser to see the result.
.. tip::
Rely on the reference documentation for :ref:`the field component in list views
<reference/view_architectures/list/field>`.
The final result should look like this:
.. image:: 03_build_user_interface/custom-list-view.png
:align: center
.. spoiler:: Solution
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 6
'data': [
'actions.xml',
'ir.model.access.csv',
'menus.xml', # Depends on `actions.xml`
'real_estate_property_data.xml',
'real_estate_property_views.xml',
],
.. code-block:: xml
:caption: `real_estate_property_views.xml`
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.property_list" model="ir.ui.view">
<field name="name">Property List</field>
<field name="model">real.estate.property</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="state"/>
<field name="type"/>
<field name="selling_price"/>
<field name="availability_date"/>
<field name="floor_area" optional="show"/>
<field name="bedrooms" optional="hide"/>
<field name="has_garden" optional="hide"/>
<field name="has_garage" optional="hide"/>
</list>
</field>
</record>
</odoo>
.. _tutorials/server_framework_101/form_view:
Form view
---------
.. exercise::
In the :file:`real_estate_property_views.xml` file, create a custom form view to display all
fields of the `real.estate.property` model in a well-structured manner:
- The state should be displayed as a status bar in the header and should be able to be updated
with a click.
- The form should have margins (hint: use the `sheet` component).
- The name should be displayed as the title of the form, should have its label on top, and should
have a placeholder.
- The image should be displayed as a thumbnail on the right side of the form.
- The fields should be grouped in two sections displayed next to each other:
- Listing Information: Type, Selling Price, Availability Date, Active
- Building Specifications: Floor Area, Number of Bedrooms, Garden, Garage
- The description should be displayed at the bottom of the form in its own section, should have
no label, should have a placeholder, and should take the full width.
.. tip::
- Rely on the reference documentation for :ref:`structural components
<reference/view_architectures/form/structural>` and :ref:`the field component
<reference/view_architectures/form/field>` in form views.
- Add the :option:`--dev xml <odoo-bin --dev>` argument to the server start-up command to
instruct the server to load records defined in XML from your filesystem rather than from the
database. This avoids restarting the server after modifying an XML file.
The final result should look like this:
.. image:: 03_build_user_interface/custom-form-view.png
:align: center
.. spoiler:: Solution
.. code-block:: xml
:caption: `real_estate_property_views.xml`
<record id="real_estate.property_form" model="ir.ui.view">
<field name="name">Property Form</field>
<field name="model">real.estate.property</field>
<field name="arch" type="xml">
<form>
<header>
<field name="state" widget="statusbar" options="{'clickable': True}"/>
</header>
<sheet>
<field name="image" widget="image" class="oe_avatar"/>
<div class="oe_title">
<label for="name" string="Property Name"/>
<h1>
<field name="name" placeholder="e.g. Tiny House"/>
</h1>
</div>
<group>
<group string="Listing Information">
<field name="type"/>
<field name="selling_price"/>
<field name="availability_date"/>
<field name="active"/>
</group>
<group string="Building Specifications">
<field name="floor_area"/>
<field name="bedrooms"/>
<field name="has_garden"/>
<field name="has_garage"/>
</group>
</group>
<separator string="Description"/>
<field
name="description"
nolabel="1"
colspan="2"
placeholder="Write a description about this property."
/>
</sheet>
</form>
</field>
</record>
.. _tutorials/server_framework_101/search_view:
Search view
-----------
The `name` and `active` fields we added earlier to the model are not ordinary fields; they're
examples of **reserved fields**. When set on a model, these special fields enable specific
pre-defined behaviors. For example, the `active` field enables **archiving** (`active = False`) and
**unarchiving** (`active = True`) records through the :icon:`oi-archive` :guilabel:`Archive` and
:icon:`oi-unarchive` :guilabel:`Unarchive` buttons in the action menu. Archived records are
automatically excluded from searches. You can observe this behavior by deselecting the
:guilabel:`Active` checkbox for one of your property records: you'll notice the record no longer
appears upon returning to the list view.
.. seealso::
:ref:`Reference documentation for the list of reserved field names
<reference/orm/fields/reserved>`
To facilitate the browsing of archived properties, we need to create a search view. Unlike list and
form views, search views are not used to display record data on screen. Instead, they define the
search behavior and enable users to search on specific fields. They also provide pre-defined
**filters** that allow for quickly searching with complex queries and grouping records by particular
fields.
.. seealso::
:ref:`Reference documentation for search views <reference/view_architectures/search>`
The most common way to set up filters is through **search domains**. Domains are used to select
specific records of a model by defining a list of criteria. Each criterion is a triplet in the
format :code:`(<field_name>, <operator>, <value>)`.
.. example::
The example search domain below selects only products of the category "Home Decor" whose price is
less than 1000.
.. code-block:: python
[('category', '=', 'home_decor'), ('price', '<', 1000)]
By default, domain criteria are combined with an implicit logical `&` (AND) operator, meaning
*every* criterion must be satisfied for a record to match a domain. Criteria can also be combined
with the logical `|` (OR) and `!` (NOT) operators in prefix form :dfn:`the operator is inserted
before its operands`.
.. example::
The example search domain below selects only products that belong to the category "Electronics"
*or* whose price is *not* between 1000 and 2000.
.. code-block:: python
['|', ('category', '=', 'electronics'), '!', '&', ('price', '>=', 1000), ('price', '<', 2000)]
.. seealso::
:ref:`Reference documentation for search domains <reference/orm/domains>`
All the generic search view only allows for is searching on property names; that's the bare minimum.
Let's enhance the search capabilities.
.. exercise::
#. Create a custom search view with the following features:
- Enable searching on the these fields:
- Name: Match records whose name contain the search value.
- Description: Match records whose description *or* name contains the search value.
- Selling price: Match records with a price *less than or equal to* the search value.
- Floor area: Match records with a floor area *at least* the search value.
- Number of bedrooms: Match records with *at least* the given number of bedrooms.
- Implement these filters:
- For Sale: The state is "New" or "Offer Received".
- Availability Date: Display a list of pre-defined availability date values.
- Garden: The property has a garden.
- Garage: The property has a garage.
- Archived: The property is archived.
- Combine selected filters with a logical AND, except for Garden and Garage, which should use
OR when both are selected.
- Enable grouping properties by state and type.
#. Modify the window action to display only properties available for sale by default.
#. Make sure that everything works!
.. tip::
- Rely on the reference documentation for :ref:`search view components
<reference/view_architectures/search/components>`, :ref:`search domains
<reference/orm/domains>`, and :ref:`search defaults
<reference/view_architectures/search/defaults>`.
- In XML, use entity references to avoid parsing errors: `&lt;` for `<`, `&gt;` for `>`, and
`&amp;` for `&`.
The final result should look like this:
.. image:: 03_build_user_interface/custom-search-view-fields.png
:align: center
.. image:: 03_build_user_interface/custom-search-view-filters.png
:align: center
.. spoiler:: Solution
.. code-block:: xml
:caption: `real_estate_property_views.xml`
<record id="real_estate.property_search" model="ir.ui.view">
<field name="name">Property Search</field>
<field name="model">real.estate.property</field>
<field name="arch" type="xml">
<search>
<!-- Fields -->
<field name="name"/>
<field
name="description"
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"
/>
<field name="selling_price" string="Maximum Price" operator="&lt;="/>
<field name="floor_area" string="Minimum Floor Area" operator="&gt;="/>
<field name="bedrooms" string="Minimum Bedrooms" operator="&gt;="/>
<!-- Filters -->
<filter
name="filter_for_sale"
string="For Sale"
domain="[('state', 'in', ['new', 'offer_received'])]"
/>
<separator/>
<filter name="filter_availability" date="availability_date"/>
<separator/>
<filter name="filter_garden" string="Garden" domain="[('has_garden', '=', True)]"/>
<filter name="filter_garage" string="Garage" domain="[('has_garage', '=', True)]"/>
<separator/>
<filter name="filter_inactive" string="Archived" domain="[('active', '=', False)]"/>
<!-- Group by -->
<filter name="group_by_state" context="{'group_by': 'state'}"/>
<filter name="group_by_type" context="{'group_by': 'type'}"/>
</search>
</field>
</record>
.. code-block:: xml
:caption: `actions.xml`
:emphasize-lines: 4
<record id="real_estate.view_properties_action" model="ir.actions.act_window">
<field name="name">Properties</field>
<field name="res_model">real.estate.property</field>
<field name="context">{'search_default_filter_for_sale': True}</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<!-- Turns out I didn't feel like being creative with the help text ¯\_(ツ)_/¯ -->
<p class="o_view_nocontent_smiling_face">
Create a new property.
</p>
</field>
</record>
----
We now have a shiny UI to manage real estate properties, but our information model is still quite
basic. We have a limited set of property types and a few building specifications, but that's not
enough for a good real estate application. In the next chapter, we'll :doc:`connect properties to
new models <04_relational_fields>` to transform our basic real estate app into a feature-rich tool.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,996 @@
==================================
Chapter 4: Extend the model family
==================================
In Odoo, data rarely exists in isolation. The true power of an application lies in its ability to
connect and relate different pieces of information. In this chapter, we'll explore the three
fundamental types of model relationships in Odoo: Many2One, One2Many, and Many2Many. Mastering these
connections will allow us to create rich, interconnected data structures that will form the backbone
of our real estate application.
.. _tutorials/server_framework_101/module_structure:
Module structure
================
As our `real_estate` module grows, you may notice that we've already created a dozen files for just
one model, along with its menu items, actions and views. With more models on the horizon, our module
directory could quickly become cluttered. To address this potential issue, Odoo provides **module
structure guidelines** that offer several benefits:
- **Improved maintainability**: A well-organized directory structure makes it easier to navigate the
module and locate specific files.
- **Scalability**: Proper organization prevents the module from becoming cluttered as it grows in
complexity and size.
- **Collaboration**: A standardized structure facilitates understanding among contributors and
ensures easier integration with the Odoo ecosystem.
.. seealso::
:ref:`Coding guidelines on module directories
<contributing/coding_guidelines/module_structure/directories>`
.. example::
Let's consider a possible structure for our example `product` module:
.. code-block:: text
product/
├── data/
│ └── product_data.xml
├── models/
│ ├── __init__.py
│ └── product.py
├── security/
│ └── ir.model.access.csv
├── views/
│ ├── menus.xml
│ └── product_views.xml
├── static/
│ ├── description/
│ │ └── icon.png
│ │
│ └── img/
│ ├── coffee_table.png
│ └── t_shirt.png
├── __init__.py
└── __manifest__.py
.. note::
- The :file:`models` directory contains its own :file:`__init__.py` file, simplifying Python
imports. The root :file:`__init__.py` file imports the :file:`models` Python package, which
in turns imports individual model files.
- Security-related files, such as :file:`ir.model.access.csv`, are placed in the dedicated
:file:`security` directory.
- UI files (:file:`menus.xml`, and view definitions) are organized within the :file:`views`
directory.
- There is no :file:`actions.xml` file. That is because managing actions is easier when they
are defined in the same file as the views they're linked to.
- The app icon resides in :file:`static/description`, while other image assets are stored in
:file:`static/img`.
- The :file:`__init__.py` and :file:`__manifest__.py` files remain in the module's root
directory.
.. exercise::
Restructure the `real_estate` module according to the guidelines.
.. tip::
Use `[CLN]` for your :ref:`commit message tag
<contributing/git_guidelines/commit_tag_module>`.
.. spoiler:: Solution
.. code-block:: text
:caption: Module structure
real_estate/
├── data/
│ └── real_estate_property_data.xml.xml
├── models/
│ ├── __init__.py
│ └── real_estate_property.py
├── security/
│ └── ir.model.access.csv
├── views/
│ ├── menus.xml
│ └── real_estate_property_views.xml
├── static/
│ ├── description/
│ │ └── icon.png
│ │
│ └── img/
│ ├── country_house.png.png
│ ├── loft.png
│ └── mixed_use_commercial.png.png
├── __init__.py
└── __manifest__.py
.. code-block:: python
:caption: `models/__init__.py`
from . import real_estate_property
.. code-block:: python
:caption: `__init__.py`
:emphasize-lines: 1
from . import models
.. code-block:: xml
:caption: `data/real_estate_property_data.xml`
:emphasize-lines: 3,9,15
<record id="real_estate.country_house" model="real.estate.property">
[...]
<field name="image" type="base64" file="real_estate/static/img/country_house.png"/>
[...]
</record>
<record id="real_estate.loft" model="real.estate.property">
[...]
<field name="image" type="base64" file="real_estate/static/img/loft.png"/>
[...]
</record>
<record id="real_estate.mixed_use_commercial" model="real.estate.property">
[...]
<field name="image" type="base64" file="real_estate/static/img/mixed_use_commercial.png"/>
[...]
</record>
.. code-block:: xml
:caption: `data/real_estate_property_views.xml`
:emphasize-lines: 4-15
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.view_properties_action" model="ir.actions.act_window">
<field name="name">Properties</field>
<field name="res_model">real.estate.property</field>
<field name="context">{'search_default_filter_for_sale': True}</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<!-- Turns out I didn't feel like being creative with the help text ¯\_(ツ)_/¯ -->
<p class="o_view_nocontent_smiling_face">
Create a new property.
</p>
</field>
</record>
[...]
</odoo>
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 2-10
'data': [
# Model data
'data/real_estate_property_data.xml',
# Security
'security/ir.model.access.csv',
# Views
'views/real_estate_property_views.xml',
'views/menus.xml', # Depends on `real_estate_property_views.xml`
],
.. _tutorials/server_framework_101/many2one:
Many-to-one
===========
As promised at the end of :doc:`the previous chapter <03_build_user_interface>`, we'll now expand
our app's capabilities by adding new models to manage additional information. This expansion
naturally leads us to an important question: How will our `real.estate.property` model connect to
these new models?
In relational databases, including Odoo's, **many-to-one relationships** play a crucial role. These
relationships allow you to link *multiple* records in one model to a *single* record in another
model.
In Odoo, many-to-one relationships are established by adding a `Many2one` field to the model
representing the *many* side of the relationship. The field is represented in the database by a
`foreign key <https://en.wikipedia.org/wiki/Foreign_key>`_ that references the ID of the connected
record. By convention, `Many2one` field names end with the `_id` suffix, indicating that they store
the referenced record's ID.
.. seealso::
:ref:`Reference documentation for Many2one fields <reference/fields/many2one>`
.. example::
In the example below, the `Selection` field of the `product` model is replaced by a `Many2one`
field to create a more flexible and scalable model structure.
.. code-block:: py
from odoo import fields, models
class Product(models.Model):
_name = 'product'
_description = "Storable Product"
[...]
category_id = fields.Many2one(
string="Category", comodel_name='product.category', ondelete='restrict', required=True
)
class ProductCategory(models.Model):
_name = 'product.category'
_description = "Product Category"
name = fields.Char(string="Name")
.. note::
- The relationship only needs to be declared on the *many* side to be established.
- The `ondelete` argument on the `Many2one` field defines what happens when the referenced
record is deleted.
In our real estate app, we currently have a fixed set of property types. To increase flexibility,
let's replace the current `type` field with a many-to-one relationship to a separate model for
managing property types.
.. exercise::
#. Create a new `real.estate.property.type` model.
- Update the :file:`ir.model.access.csv` file to grant all database administrators access to
the model.
- Replace the dummy :guilabel:`Settings` menu item with a new :menuselection:`Configuration
--> Property Types` menu item.
- Create a window action to browse property types only in list view.
- Create the list view for property types.
- In a data file, describe at least as many default property types as the `type` field of the
`real.estate.property` model supports.
#. Replace the `type` field on the `real.estate.property` model by a many-to-one relationship to
the `real.estate.property.type` model. Prevent deleting property types if a property
references them.
.. tip::
- As the window action doesn't allow opening property types in form view, clicking the
:guilabel:`New` button does nothing. To allow editing records in-place, rely on the
reference documentation for :ref:`root attributes of list views
<reference/view_architectures/list/root>`
- The server will throw an error at start-up because it can't require a value for the new,
currently empty field. To avoid fixing that manually in the database, run the command
:command:`dropdb tutorials` to delete the database and start from scratch.
.. spoiler:: Solution
.. code-block:: python
:caption: `real_estate_property_type.py`
from odoo import fields, models
from odoo.tools import date_utils
class RealEstatePropertyType(models.Model):
_name = 'real.estate.property.type'
_description = "Real Estate Property Type"
name = fields.Char(string="Name", required=True)
.. code-block:: py
:caption: `__init__.py`
:emphasize-lines: 2
from . import real_estate_property
from . import real_estate_property_type
.. code-block:: csv
:caption: `ir.model.access.csv`
:emphasize-lines: 3
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
real_estate_property_system,real.estate.property.system,model_real_estate_property,base.group_system,1,1,1,1
real_estate_property_type_system,real.estate.property.type.system,model_real_estate_property_type,base.group_system,1,1,1,1
.. code-block:: xml
:caption: `menus.xml`
:emphasize-lines: 3-9
<menuitem id="real_estate.root_menu"> <!-- truncated -->
<menuitem id="real_estate.properties_menu"/> <!-- truncated -->
<menuitem id="real_estate.configuration_menu" name="Configuration" sequence="20">
<menuitem
id="real_estate.property_types_menu"
name="Property Types"
action="real_estate.view_property_types_action"
/>
</menuitem>
</menuitem>
.. code-block:: xml
:caption: `real_estate_property_type_views.xml`
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.view_property_types_action" model="ir.actions.act_window">
<field name="name">Property Types</field>
<field name="res_model">real.estate.property.type</field>
<field name="view_mode">list</field>
</record>
<record id="real_estate.property_type_list" model="ir.ui.view">
<field name="name">Property Type List</field>
<field name="model">real.estate.property.type</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="name"/>
</list>
</field>
</record>
</odoo>
.. code-block:: xml
:caption: `real_estate_property_type_data.xml`
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.type_house" model="real.estate.property.type">
<field name="name">House</field>
</record>
<record id="real_estate.type_apartment" model="real.estate.property.type">
<field name="name">Apartment</field>
</record>
<record id="real_estate.type_office" model="real.estate.property.type">
<field name="name">Office Building</field>
</record>
<record id="real_estate.type_retail" model="real.estate.property.type">
<field name="name">Retail Space</field>
</record>
<record id="real_estate.type_warehouse" model="real.estate.property.type">
<field name="name">Warehouse</field>
</record>
</odoo>
.. code-block:: py
:caption: `__manifest__.py`
:emphasize-lines: 3,4,7,9
'data': [
# Model data
'data/real_estate_property_type_data.xml',
'data/real_estate_property_data.xml', # Depends on `real_estate_property_type_data.xml`
[...]
# Views
'views/real_estate_property_type_views.xml',
'views/real_estate_property_views.xml',
'views/menus.xml', # Depends on actions in views.
],
.. code-block:: py
:caption: `real_estate_property.py`
:emphasize-lines: 1-3
type_id = fields.Many2one(
string="Type", comodel_name='real.estate.property.type', ondelete='restrict', required=True
)
.. code-block:: xml
:caption: `real_estate_property_views.xml`
:emphasize-lines: 5,14,27
<record id="real_estate.property_list" model="ir.ui.view">
[...]
<list>
[...]
<field name="type_id"/>
[...]
</list>
[...]
</record>
<record id="real_estate.property_form" model="ir.ui.view">
[...]
<group string="Listing Information">
<field name="type_id"/>
<field name="selling_price"/>
<field name="availability_date"/>
<field name="active"/>
</group>
[...]
</record>
<record id="real_estate.property_search" model="ir.ui.view">
[...]
<search>
[...]
<filter name="group_by_state" context="{'group_by': 'state'}"/>
<filter name="group_by_type" context="{'group_by': 'type_id'}"/>
</search>
[...]
</record>
.. code-block:: xml
:caption: `real_estate_property_data.xml`
:emphasize-lines: 3,9,15
<record id="real_estate.country_house" model="real.estate.property">
[...]
<field name="type_id" ref="real_estate.type_house"/>
[...]
</record>
<record id="real_estate.loft" model="real.estate.property">
[...]
<field name="type_id" ref="real_estate.type_apartment"/>
[...]
</record>
<record id="real_estate.mixed_use_commercial" model="real.estate.property">
[...]
<field name="type_id" ref="real_estate.type_retail"/>
[...]
</record>
.. _tutorials/server_framework_101/generic_models:
Generic models
--------------
In the previous exercise, we created a many-to-one relationship with a custom model within the
`real_estate` module. However, Odoo provides several generic models that can extend your app's
capabilities without defining new models. These generic models are part of the default `base` module
and are typically prefixed with `res` or `ir`.
Two frequently used models in Odoo are:
- `res.users`: Represents user accounts in the database. They determine access rights to records and
can be `internal` (have access to the backend), `portal` (have access to the portal, e.g., to view
their invoices), or `public` (not logged in).
- `res.partner`: Represents physical or legal entities. They can be a company, an individual, or a
contact address.
.. seealso::
`The list of generic models in the base module <{GITHUB_PATH}/odoo/addons/base/models>`_
To make our real estate properties more informative, let's add two pieces of information: the seller
of the property and the salesperson managing the property.
.. exercise::
#. Add the following fields to the `real.estate.property` model:
- Seller (required): The person putting their property on sale; it can be any individual.
- Salesperson: The employee of the real estate agency overseeing the sale of the property.
#. Modify the form view of properties to include a notebook component. The property description
should be in the first page, and the two new fields should be in the second page.
.. tip::
You don't need to define any new UI component to browse the seller you assigned to your
default properties! Just go to :menuselection:`Apps` and install the :guilabel:`Contacts` app.
.. spoiler:: Solution
.. code-block:: python
:caption: `real_estate_property.py`
:emphasize-lines: 1-2
seller_id = fields.Many2one(string="Seller", comodel_name='res.partner', required=True)
salesperson_id = fields.Many2one(string="Salesperson", comodel_name='res.users')
.. code-block:: xml
:caption: `real_estate_property_views.xml`
:emphasize-lines: 3-18
<record id="real_estate.property_form" model="ir.ui.view">
[...]
<notebook>
<page string="Description">
<field
name="description"
placeholder="Write a description about this property."
/>
</page>
<page string="Other Info">
<group>
<group>
<field name="seller_id"/>
<field name="salesperson_id"/>
</group>
</group>
</page>
</notebook>
[...]
</record>
.. code-block:: xml
:caption: `res_partner_data.xml`
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.bafien_carpink" model="res.partner">
<field name="name">Bafien Carpink</field>
</record>
<record id="real_estate.antony_petisuix" model="res.partner">
<field name="name">Antony Petisuix</field>
</record>
<record id="real_estate.amyfromthevideos" model="res.partner">
<field name="name">AmyFromTheVideos</field>
</record>
</odoo>
.. code-block:: xml
:caption: `real_estate_property_data.xml`
:emphasize-lines: 3,8,13
<record id="real_estate.country_house" model="real.estate.property">
[...]
<field name="seller_id" ref="real_estate.amyfromthevideos"/>
</record>
<record id="real_estate.loft" model="real.estate.property">
[...]
<field name="seller_id" ref="real_estate.antony_petisuix"/>
</record>
<record id="real_estate.mixed_use_commercial" model="real.estate.property">
[...]
<field name="seller_id" ref="real_estate.bafien_carpink"/>
</record>
.. code-block:: py
:caption: `__manifest__.py`
:emphasize-lines: 3,5,6
'data': [
# Model data
'data/res_partner_data.xml',
'data/real_estate_property_type_data.xml',
# Depends on `res_partner_data.xml`, `real_estate_property_type_data.xml`
'data/real_estate_property_data.xml',
[...]
],
.. _tutorials/server_framework_101/one2many:
One-to-many
===========
After exploring how to connect multiple records to a single one with many-to-one relationships,
let's consider their counterparts: **one-to-many relationships**. These relationships represent the
inverse of the many-to-one relationships we just discussed, enabling a *single* record in one model
to be associated with *multiple* records in another model.
In Odoo, one-to-many relationships are established by adding a `One2many` field to the model
representing the *one* side of **an already existing** many-to-one relationship. It's important to
note that `One2many` fields don't store data in the database; instead, they provide a virtual field
that Odoo computes based on the referenced `Many2one` field. By convention, `One2many` field names
end with the `_ids` suffix, indicating that they allow accessing the IDs of the connected records.
.. seealso::
:ref:`Reference documentation for One2many fields <reference/fields/one2many>`
.. example::
In the example below, a `One2many` field is added to the `product.category` model to allow quick
access to the connected products from the product category.
.. code-block:: py
from odoo import fields, models
class Product(models.Model):
_name = 'product'
_description = "Storable Product"
[...]
category_id = fields.Many2one(
string="Category", comodel_name='product.category', ondelete='restrict', required=True
)
class ProductCategory(models.Model):
_name = 'product.category'
_description = "Product Category"
name = fields.Char(string="Name")
product_ids = fields.One2many(
string="Products", comodel_name='product', inverse_name='category_id'
)
.. note::
The `One2many` field must reference its `Many2one` counterpart through the `inverse_name`
argument.
A good use case for a one-to-many relationship in our real estate app would be to connect properties
to a list of offers received from potential buyers.
.. exercise::
#. Create a new `real.estate.offer` model. It should have the following fields:
- Amount (required): The amount offered to buy the property.
- Buyer (required): The person making the offer.
- Date (required; default to creation date): When the offer was made.
- Validity (default to 7): The number of days before the offer expires.
- State (required): Either "Waiting", "Accepted", or "Refused".
#. Create a list and form views for the `real.estate.offer` model. It's not necessary to create
menu items or actions, as offers will be accessible from properties, but feel free to do it
anyway!
#. Allow connecting properties to multiple offers.
#. Modify the form view of properties to display offers in a new notebook page titled "Offers".
.. spoiler:: Solution
.. code-block:: python
:caption: `real_estate_offer.py`
from odoo import fields, models
class RealEstateOffer(models.Model):
_name = 'real.estate.offer'
_description = "Real Estate Offer"
amount = fields.Float(string="Amount", required=True)
buyer_id = fields.Many2one(string="Buyer", comodel_name='res.partner', required=True)
date = fields.Date(string="Date", required=True, default=fields.Date.today())
validity = fields.Integer(
string="Validity", help="The number of days before the offer expires.", default=7
)
state = fields.Selection(
string="State",
selection=[
('waiting', "Waiting"),
('accepted', "Accepted"),
('refused', "Refused"),
],
required=True,
default='waiting',
)
property_id = fields.Many2one(
string="Property", comodel_name='real.estate.property', required=True
)
.. code-block:: python
:caption: `__init__.py`
:emphasize-lines: 1
from . import real_estate_offer
from . import real_estate_property
from . import real_estate_property_type
.. code-block:: csv
:caption: `ir.model.access.csv`
:emphasize-lines: 2
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
real_estate_offer_system,real.estate.offer.system,model_real_estate_offer,base.group_system,1,1,1,1
real_estate_property_system,real.estate.property.system,model_real_estate_property,base.group_system,1,1,1,1
real_estate_property_type_system,real.estate.property.type.system,model_real_estate_property_type,base.group_system,1,1,1,1
.. code-block:: xml
:caption: `real_estate_offer_views.xml`
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.offer_list" model="ir.ui.view">
<field name="name">Offer List</field>
<field name="model">real.estate.offer</field>
<field name="arch" type="xml">
<list>
<field name="amount"/>
<field name="buyer_id"/>
<field name="date"/>
<field name="validity"/>
<field name="state"/>
</list>
</field>
</record>
<record id="real_estate.offer_form" model="ir.ui.view">
<field name="name">Offer Form</field>
<field name="model">real.estate.offer</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="amount"/>
<field name="buyer_id"/>
<field name="state"/>
</group>
<group>
<field name="date"/>
<field name="validity"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 4
'data': [
[...]
# Views
'views/real_estate_offer_views.xml',
'views/real_estate_property_type_views.xml',
'views/real_estate_property_views.xml',
'views/menus.xml', # Depends on actions in views.
],
.. code-block:: py
:caption: `real_estate_property.py`
:emphasize-lines: 1-3
offer_ids = fields.One2many(
string="Offers", comodel_name='real.estate.offer', inverse_name='property_id'
)
.. code-block:: xml
:caption: `real_estate_property_views.xml`
:emphasize-lines: 3-5
<record id="real_estate.property_form" model="ir.ui.view">
[...]
<page string="Offers">
<field name="offer_ids"/>
</page>
[...]
</record>
.. _tutorials/server_framework_101/many2many:
Many-to-many
============
After the many-to-one and one-to-many relationships, let's consider a more complex use case:
**many-to-many relationships**. These relationships enable *multiple* records in one model to be
associated with *multiple* records in another model, creating a bidirectional connection between
sets of records.
In Odoo, many-to-many relationships are established by adding a `Many2many` field to one or both of
the models. The server framework implements many-to-many relationships by automatically creating an
intermediate (junction) table in the database. This table stores pairs of IDs, each pair
representing a connection between a record of the first model and a record of the second model. By
convention, `Many2many` field names end with the `_ids` suffix, like for `One2many` fields.
.. seealso::
:ref:`Reference documentation for Many2many fields <reference/fields/many2many>`
.. example::
In the example below, a many-to-many relationship is established between the `product` model and
the `res.partner` model, which is used to represent sellers offering products for sale.
.. code-block:: py
from odoo import fields, models
class Product(models.Model):
_name = 'product'
_description = "Storable Product"
[...]
seller_ids = fields.Many2many(
string="Sellers",
help="The sellers offering the product for sale.",
comodel_name='res.partner',
relation='product_seller_rel',
column1='product_id',
column2='partner_id',
)
.. note::
- It is not necessary to add a `Many2many` field to both models of the relationship.
- The optional `relation`, `column1`, and `column2` field arguments allow specifying the name
of the junction table and of its columns.
Let's conclude this extension of the model family by allowing to associate multiple description tags
with each property.
.. exercise::
#. Create a new `real.estate.tag` model. It should have the following fields:
- Name (required): The label of the tag.
- Color: The color code to use for the tag, as an integer.
#. In a data file, describe various default property tags. For example, "Renovated".
#. Create all necessary UI components to manage tags from the :guilabel:`Configuration` category
menu item.
#. Allow connecting properties to multiple tags, and tags to multiple properties.
#. Modify the form view of properties to display their associated tags. It should not be possible
to create new tags from the form view of properties.
.. tip::
Rely on the reference documentation for :ref:`the field component
<reference/view_architectures/form/field>` in form views to find a nice display for property
tags.
.. spoiler:: Solution
.. code-block:: python
:caption: `real_estate_tag.py`
from odoo import fields, models
class RealEstateTag(models.Model):
_name = 'real.estate.tag'
_description = "Real Estate Tag"
name = fields.Char(string="Label", required=True)
color = fields.Integer(string="Color")
.. code-block:: python
:caption: `__init__.py`
:emphasize-lines: 4
from . import real_estate_offer
from . import real_estate_property
from . import real_estate_property_type
from . import real_estate_tag
.. code-block:: csv
:caption: `ir.model.access.csv`
:emphasize-lines: 3
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
[...]
real_estate_tag_system,real.estate.tag.system,model_real_estate_tag,base.group_system,1,1,1,1
.. code-block:: xml
:caption: `real_estate_tag_data.xml`
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.tag_eco_passive" model="real.estate.tag">
<field name="name">Eco Passive</field>
<field name="color">1</field>
</record>
<record id="real_estate.tag_modern" model="real.estate.tag">
<field name="name">Modern</field>
<field name="color">2</field>
</record>
<record id="real_estate.tag_renovated" model="real.estate.tag">
<field name="name">Renovated</field>
<field name="color">3</field>
</record>
<record id="real_estate.tag_rural" model="real.estate.tag">
<field name="name">Rural</field>
<field name="color">4</field>
</record>
<record id="real_estate.tag_suburban" model="real.estate.tag">
<field name="name">Suburban</field>
<field name="color">5</field>
</record>
<record id="real_estate.tag_urban" model="real.estate.tag">
<field name="name">Urban</field>
<field name="color">6</field>
</record>
<record id="real_estate.tag_waterfront" model="real.estate.tag">
<field name="name">Waterfront</field>
<field name="color">7</field>
</record>
</odoo>
.. code-block:: xml
:caption: `menus.xml`
:emphasize-lines: 3-7
<menuitem id="real_estate.configuration_menu" name="Configuration" sequence="20">
[...]
<menuitem
id="real_estate.tags_menu"
name="Tags"
action="real_estate.view_tags_action"
/>
</menuitem>
.. code-block:: xml
:caption: `real_estate_tag_views.xml`
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.views_tag_action" model="ir.actions.act_window">
<field name="name">Tags</field>
<field name="res_model">real.estate.tag</field>
<field name="view_mode">list</field>
</record>
<record id="real_estate.tag_list" model="ir.ui.view">
<field name="name">Tag List</field>
<field name="model">real.estate.tag</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="name"/>
<field name="color" widget="color_picker"/>
</list>
</field>
</record>
</odoo>
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 3,5
'data': [
[...]
'data/real_estate_tag_data.xml',
[...]
'views/real_estate_tag_views.xml',
[...]
],
.. code-block:: python
:caption: `real_estate_property.py`
:emphasize-lines: 1
tag_ids = fields.Many2many(string="Tags", comodel_name='real.estate.tag')
.. code-block:: xml
:caption: `real_estate_property_views.xml`
:emphasize-lines: 3-7
<record id="real_estate.property_form" model="ir.ui.view">
[...]
<field
name="tag_ids"
widget="many2many_tags"
options="{'color_field': 'color', 'no_quick_create': True, 'no_create_edit': True}"
/>
[...]
</record>
----
Congratulations! You've learned the art of forging connections between your Odoo models. You're now
well-equipped to build complex, interconnected data structures. In the next chapter, we'll
:doc:`add custom business logic to the models <05_connect_the_dots>`, turning your application from
a simple data management tool into a smart, automated system that can handle complex business
processes.

View File

@ -0,0 +1,35 @@
=========================
Chapter 5: Business logic
=========================
tmp
.. todo: constraints, defaults, onchanges, computes
.. todo: model actions ("assign myself as salesperson" action, "view offers" statbutton)
.. todo: explain the env (self.env.cr, self.env.uid, self.env.user, self.env.context, self.env.ref(xml_id), self.env[model_name])
.. todo: explain the thing about `self`
.. todo: explain magic commands
.. todo: copy=False on some fields
.. todo: introduce lambda functions for defaults :point_down:
There is a problem with the way we defined our Date fields: their default value relies on
:code:`fields.Date.today()` or some other static method. When the code is loaded into memory, the
date is computed once and reused for all newly created records until the server is shut down. You
probably didn't notice it, unless you kept your server running for several days, but it would be
much more visible with Datetime fields, as all newly created records would share the same timestamp.
That's where lambda functions come in handy. As they generate an anonymous function each time
they're evaluated at runtime, they can be used in the computation of default field values to return
an updated value for each new record.
.. todo: salesperson_id = fields.Many2one(default=lambda self: self.env.user)
.. todo: real.estate.offer.amount::default -> property.selling_price
.. todo: real.estate.tag.color -> default=_default_color ; def _default_color(self): return random.randint(1, 11)
.. todo: 6,0,0 to associate tags to properties in data
.. todo: unique tag
.. todo: odoo-bin shell section
----
.. todo: add incentive for chapter 6

View File

@ -0,0 +1,9 @@
===================
Chapter 6: Security
===================
tmp
----
.. todo: add incentive for next chapter

View File

@ -0,0 +1,15 @@
=========================
Chapter 7: Advanced views
=========================
tmp
.. todo:: invisible, required, readonly modifiers
.. todo:: introduce bootstrap
.. todo:: widgets; eg, <widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
.. todo:: add Gantt view of properties availability
.. todo:: add Kanban view of properties
----
.. todo: add incentive for next chapter

View File

@ -0,0 +1,11 @@
======================
Chapter 8: Inheritance
======================
tmp
.. todo:: inherit from mail.tread mixin and add a chatter
----
.. todo: add incentive for next chapter

View File

@ -0,0 +1,9 @@
=================
Chapter 9: Portal
=================
Controllers + QWeb
----
.. todo: add incentive for next chapter

View File

@ -0,0 +1,9 @@
========================
Chapter 10: Unit testing
========================
tmp
----
.. todo: add incentive for next chapter

View File

@ -0,0 +1,48 @@
:show-content:
:orphan:
=============================
Server framework 101 (legacy)
=============================
.. danger::
This tutorial is outdated. We recommend reading :doc:`server_framework_101` instead.
.. toctree::
:titlesonly:
:glob:
server_framework_101_legacy/*
Welcome to the Server framework 101 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.
The goal of this tutorial is for you to get an insight of the most important parts of the Odoo
development framework while developing your own Odoo module to manage real estate assets. 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.
.. important::
Before going further, make sure you have prepared your development environment with the
:doc:`setup guide <setup_guide>`.
Ready? Let's get started!
* :doc:`server_framework_101_legacy/01_architecture`
* :doc:`server_framework_101_legacy/02_newapp`
* :doc:`server_framework_101_legacy/03_basicmodel`
* :doc:`server_framework_101_legacy/04_securityintro`
* :doc:`server_framework_101_legacy/05_firstui`
* :doc:`server_framework_101_legacy/06_basicviews`
* :doc:`server_framework_101_legacy/07_relations`
* :doc:`server_framework_101_legacy/08_compute_onchange`
* :doc:`server_framework_101_legacy/09_actions`
* :doc:`server_framework_101_legacy/10_constraints`
* :doc:`server_framework_101_legacy/11_sprinkles`
* :doc:`server_framework_101_legacy/12_inheritance`
* :doc:`server_framework_101_legacy/13_other_module`
* :doc:`server_framework_101_legacy/14_qwebintro`
* :doc:`server_framework_101_legacy/15_final_word`

View File

@ -1,9 +1,15 @@
.. _tutorials/server_framework_101/01_architecture:
.. _tutorials/server_framework_101_legacy/01_architecture:
================================
Chapter 1: Architecture Overview
================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
Multitier application
=====================

View File

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

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -2,6 +2,12 @@
Chapter 2: A New Application
============================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
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.

View File

@ -2,6 +2,12 @@
Chapter 3: Models And Basic Fields
==================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
At the end of the :doc:`previous chapter <02_newapp>`, we were able to
create an Odoo module. However, at this point it is still an empty shell which doesn't allow us to
store any data. In our real estate module, we want to store the information related to the

View File

@ -2,6 +2,12 @@
Chapter 4: Security - A Brief Introduction
==========================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
In the :doc:`previous chapter <03_basicmodel>`, we created our first table
intended to store business data. In a business application such as Odoo, one of the first questions
to consider is who\ [#who]_ can access the data. Odoo provides a security mechanism to allow access

View File

@ -2,6 +2,12 @@
Chapter 5: Finally, Some UI To Play With
========================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
Now that we've created our new :doc:`model <03_basicmodel>` and its
corresponding :doc:`access rights <04_securityintro>`, it is time to
interact with the user interface.

View File

@ -2,6 +2,12 @@
Chapter 6: Basic Views
======================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
We have seen in the :doc:`previous chapter <05_firstui>` that Odoo is able
to generate default views for a given model. In practice, the default view is **never** acceptable
for a business application. Instead, we should at least organize the various fields in a logical

View File

@ -2,6 +2,12 @@
Chapter 7: Relations Between Models
===================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
The :doc:`previous chapter <06_basicviews>` covered the creation of custom
views for a model containing basic fields. However, in any real business scenario we need more than
one model. Moreover, links between models are necessary. One can easily imagine one model containing

View File

@ -2,6 +2,12 @@
Chapter 8: Computed Fields And Onchanges
========================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
The :doc:`relations between models <07_relations>` are a key component of
any Odoo module. They are necessary for the modelization of any business case. However, we may want
links between the fields within a given model. Sometimes the value of one field is determined from
@ -50,7 +56,7 @@ method should set the value of the computed field for every record in
By convention, :attr:`~odoo.fields.Field.compute` methods are private, meaning that they cannot
be called from the presentation tier, only from the business tier (see
:ref:`tutorials/server_framework_101/01_architecture`). Private methods have a name starting with an
:ref:`tutorials/server_framework_101_legacy/01_architecture`). Private methods have a name starting with an
underscore ``_``.
Dependencies

View File

@ -2,6 +2,12 @@
Chapter 9: Ready For Some Action?
=================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
So far we have mostly built our module by declaring fields and views. We just introduced business
logic in the :doc:`previous chapter <08_compute_onchange>` thanks to
computed fields and onchanges. In any real business scenario, we would want to link some business

View File

@ -2,6 +2,12 @@
Chapter 10: Constraints
=======================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
The :doc:`previous chapter <09_actions>` introduced the ability to add
some business logic to our model. We can now link buttons to business code, but how can we prevent
users from entering incorrect data? For example, in our real estate module nothing prevents

View File

@ -2,6 +2,12 @@
Chapter 11: Add The Sprinkles
=============================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
Our real estate module now makes sense from a business perspective. We created
:doc:`specific views <06_basicviews>`, added several
:doc:`action buttons <09_actions>` and

View File

@ -2,6 +2,12 @@
Chapter 12: Inheritance
=======================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
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

View File

@ -2,6 +2,12 @@
Chapter 13: Interact With Other Modules
=======================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
In the :doc:`previous chapter <12_inheritance>`, we used inheritance to
modify the behavior of a module. In our real estate scenario, we would like to go a step further
and be able to generate invoices for our customers. Odoo provides an Invoicing module, so it
@ -47,7 +53,7 @@ When the ``estate_account`` module appears in the list, go ahead and install it!
the Invoicing application is installed as well. This is expected since your module depends on it.
If you uninstall the Invoicing application, your module will be uninstalled as well.
.. _tutorials/server_framework_101/13_other_module/create:
.. _tutorials/server_framework_101_legacy/13_other_module/create:
Invoice Creation
----------------

View File

@ -2,6 +2,12 @@
Chapter 14: A Brief History Of QWeb
===================================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
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

View File

@ -2,6 +2,12 @@
Chapter 15: The final word
==========================
.. danger::
This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead.
.. seealso::
:doc:`Homepage of the tutorial <../server_framework_101_legacy>`
Coding guidelines
=================

View File

@ -10,8 +10,10 @@ Odoo community and Odoo employees alike, the preferred way is to perform a sourc
Follow the :ref:`contributing/development/setup` section of the contributing guide to prepare
your environment for pushing local changes to the Odoo repositories.
Adapt the environment for the tutorials
=======================================
.. _tutorials/setup_guide/adapt_env:
Adapt the environment to the tutorials
======================================
By now, you should have downloaded the source code into two local repositories, one for `odoo/odoo`
and one for `odoo/enterprise`. These repositories are set up to push changes to pre-defined
@ -29,10 +31,11 @@ will be part of the `addons-path` that references all directories containing Odo
.. code-block:: console
$ git clone git@github.com:odoo/tutorials.git
$ git clone --branch {BRANCH} --single-branch git@github.com:odoo/tutorials.git
#. Configure your fork and Git to push changes to your fork rather than to the main codebase. If you
work at Odoo, configure Git to push changes to the shared fork created on the account **odoo-dev**.
work at Odoo, configure Git to push changes to the shared fork created on the account
**odoo-dev**.
.. tabs::
@ -41,8 +44,8 @@ will be part of the `addons-path` that references all directories containing Odo
#. Visit `github.com/odoo/tutorials <https://github.com/odoo/tutorials>`_ and click the
:guilabel:`Fork` button to create a fork of the repository on your account.
#. In the command below, replace `<your_github_account>` with the name of the GitHub account
on which you created the fork.
#. In the command below, replace `<your_github_account>` with the name of the GitHub
account on which you created the fork.
.. code-block:: console
@ -58,57 +61,67 @@ will be part of the `addons-path` that references all directories containing Odo
$ git remote set-url --push origin you_should_not_push_on_this_repository
That's it! Your environment is now prepared to run Odoo from the sources, and you have successfully
created a repository to serve as an addons directory. This will allow you to push your work to GitHub.
created a local repository to serve as an addons directory. This will allow you to push your work to
GitHub.
.. important::
**For Odoo employees only:**
#. Make sure to read very carefully :ref:`contributing/development/first-contribution`. In particular,
your branch name must follow our conventions.
#. Make sure to read very carefully :ref:`contributing/development/first-contribution`. In
particular:
#. Once you have pushed your first change to the shared fork on **odoo-dev**, create a
:abbr:`PR (Pull Request)`. Please put your quadrigram in the PR title (e.g., "abcd - Technical
Training").
This will enable you to share your upcoming work and receive feedback from your coaches. To ensure
a continuous feedback loop, we recommend pushing a new commit as soon as you complete a chapter
of the tutorial. Note that the PR is automatically updated with commits you push to **odoo-dev**,
you don't need to open multiple PRs.
- Your code must follow the :doc:`guidelines </contributing/development/coding_guidelines>`.
- Your commit messages must be :doc:`correctly written
</contributing/development/git_guidelines>`.
- Your branch name must follow our conventions.
#. Once you have pushed your first change to the shared fork on **odoo-dev**, create a **draft**
:abbr:`PR (Pull Request)` with your quadrigram in the title. This will enable you to share
your upcoming work and receive feedback from your coaches. To ensure a continuous feedback
loop, push a new commit as soon as you complete a chapter of a tutorial.
#. At Odoo we use `Runbot <https://runbot.odoo.com>`_ extensively for our :abbr:`CI (Continuous
Integration)` tests. When you push your changes to **odoo-dev**, Runbot creates a new build
and test your code. Once logged in, you will be able to see your branches `Tutorials project
<https://runbot.odoo.com/runbot/tutorials-12>`_.
and tests your code. Once logged in, you will be able to see your branch on the `Tutorials
project <https://runbot.odoo.com/runbot/tutorials-12>`_.
.. note::
The specific location of the repositories on your file system is not crucial. However, for the
sake of simplicity, we will assume that you have cloned all the repositories under the same
directory. If this is not the case, make sure to adjust the following commands accordingly,
providing the appropriate relative path from the `odoo/odoo` repository to the
`odoo/tutorials` repository.
Run the server
==============
.. _tutorials/setup_guide/start_server:
Launch with `odoo-bin`
----------------------
Start the server
================
Once all dependencies are set up, Odoo can be launched by running `odoo-bin`, the command-line
interface of the server.
interface of the server, and passing the comma-separated list of repositories with the `addons-path`
argument. If you have access to the `odoo/enterprise` repository, add it to the `addons-path`.
.. code-block:: console
.. tabs::
$ cd $HOME/src/odoo/
$ ./odoo-bin --addons-path="addons/,../enterprise/,../tutorials" -d rd-demo
.. tab:: Run the community edition
.. code-block:: console
$ cd $HOME/src/odoo/
$ ./odoo-bin --addons-path="addons/,../tutorials" -d tutorials
.. tab:: Run the enterprise edition
.. code-block:: console
$ cd $HOME/src/odoo/
$ ./odoo-bin --addons-path="addons/,../enterprise/,../tutorials" -d tutorials
There are multiple :ref:`command-line arguments <reference/cmdline/server>` that you can use to run
the server. In this training you will only need some of them.
.. option:: -d <database>
The database that is going to be used.
The database to use.
.. option:: --addons-path <directories>
@ -117,66 +130,59 @@ the server. In this training you will only need some of them.
.. option:: --limit-time-cpu <limit>
Prevent the worker from using more than <limit> CPU seconds for each request.
Prevent the worker from using more than `<limit>` CPU seconds for each request.
.. option:: --limit-time-real <limit>
Prevent the worker from taking longer than <limit> seconds to process a request.
Prevent the worker from taking longer than `<limit>` seconds to process a request.
.. tip::
- The :option:`--limit-time-cpu` and :option:`--limit-time-real` arguments can be used to prevent
the worker from being killed when debugging the source code.
- | 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 :command:`$ pip
install --upgrade --force-reinstall <MODULE_NAME>`.
| If this error occurs with more than one module, you may need to re-install all the
requirements with :command:`$ pip 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
- Other commonly used arguments are:
- :option:`-i <odoo-bin --init>`: Install some modules before running the server
(comma-separated list). This is equivalent to going to :guilabel:`Apps` in the user interface,
(comma-separated list). This is equivalent to going to :guilabel:`Apps` in the user interface
and installing the module from there.
- :option:`-u <odoo-bin --update>`: Update some modules before running the server
(comma-separated list). This is equivalent to going to :guilabel:`Apps` in the user interface,
selecting a module, and upgrading it from there.
(comma-separated list). This is equivalent to going to :guilabel:`Apps` in the user interface
and updating the module from there.
.. _tutorials/setup_guide/log_in:
Log in to Odoo
--------------
==============
Open http://localhost:8069/ on your browser. We recommend using `Chrome
Open http://localhost:8069/ in your browser. We recommend using `Chrome
<https://www.google.com/intl/en/chrome/>`_, `Firefox <https://www.mozilla.org/firefox/new/>`_, or
any other browser with development tools.
To log in as the administrator user, use the following credentials:
- email: `admin`
- password: `admin`
- Email: `admin`
- Password: `admin`
Enable the developer mode
=========================
The developer or debug mode is useful for training as it gives access to additional (advanced)
tools. :ref:`Enable the developer mode <developer-mode>` now. Choose the method that you prefer;
they are all equivalent.
.. _tutorials/setup_guide/extra_tools:
Extra tools
===========
.. _tutorials/setup_guide/extra_tools/dev_mode:
Developer mode
--------------
:ref:`Enable the developer mode <developer-mode>` to get access to developer-oriented tools in the
interface.
.. _tutorials/setup_guide/extra_tools/git_commands:
Useful Git commands
-------------------
Here are some useful Git commands for your day-to-day work.
- | Switch branches:
| When you switch branches, both repositories (odoo and enterprise) must be synchronized, i.e.
both need to be in the same branch.
- Switch branches:
.. code-block:: console
@ -186,6 +192,10 @@ Here are some useful Git commands for your day-to-day work.
$ cd $HOME/src/enterprise
$ git switch {BRANCH}
.. important::
When you switch branches, both repositories (odoo and enterprise) must be synchronized, i.e.
both need to be in the same branch.
- Fetch and rebase:
.. code-block:: console
@ -198,57 +208,53 @@ Here are some useful Git commands for your day-to-day work.
$ git fetch --all --prune
$ git rebase --autostash enterprise/{BRANCH}
Code Editor
.. _tutorials/setup_guide/extra_tools/code_editor:
Code editor
-----------
If you are working at Odoo, many of your colleagues are using `VSCode
You are free to choose your code preferred editor. Most Odoo developers use `VSCode
<https://code.visualstudio.com>`_, `VSCodium <https://vscodium.com>`_ (the open source equivalent),
`PyCharm <https://www.jetbrains.com/pycharm/download/#section=linux>`_, or `Sublime Text
<https://www.sublimetext.com>`_. However, you are free to choose your preferred editor.
<https://www.sublimetext.com>`_.
It is important to configure your linters correctly. Using a linter helps you by showing syntax and
semantic warnings or errors. Odoo source code tries to respect Python's and JavaScript's standards,
but some of them can be ignored.
semantic warnings or errors. For JavaScript, we use ESLint and you can find a `configuration file
example here <https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines#use-a-linter>`_.
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
<https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines#use-a-linter>`_.
.. _tutorials/setup_guide/extra_tools/psql_tools:
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/>`_.
You can manage your PostgreSQL databases using the command line or 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.
We recommend you connect the GUI application to your database using the Unix socket.
- Host name/address: `/var/run/postgresql`
- Port: `5432`
- Username: `$USER`
Python Debugging
.. _tutorials/setup_guide/extra_tools/python_debugging:
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.
When facing a bug or trying to understand how the code works, simply printing things out can help a
lot, 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.
You can use your editor's debugger, or 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/>`_).
In the following example we use ipdb, but the process is similar with other libraries.
In the following example, we use ipdb, but the process is similar to other libraries.
#. Install the library:
.. code-block:: console
pip install ipdb
$ pip install ipdb
#. Place a trigger (breakpoint):

View File

@ -7,3 +7,21 @@ applications/finance/accounting/payments/internal_transfers.rst applications/fin
# applications/point of sale
content/applications/sales/point_of_sale/payment_methods/terminals/vantiv.rst content/applications/sales/point_of_sale/payment_methods/terminals.rst
# developer/tutorials
developer/tutorials/server_framework_101/01_architecture.rst developer/tutorials/server_framework_101_legacy/01_architecture.rst
developer/tutorials/server_framework_101/02_newapp.rst developer/tutorials/server_framework_101_legacy/02_newapp.rst
developer/tutorials/server_framework_101/03_basicmodel.rst developer/tutorials/server_framework_101_legacy/03_basicmodel.rst
developer/tutorials/server_framework_101/04_securityintro.rst developer/tutorials/server_framework_101_legacy/04_securityintro.rst
developer/tutorials/server_framework_101/05_firstui.rst developer/tutorials/server_framework_101_legacy/05_firstui.rst
developer/tutorials/server_framework_101/06_basicviews.rst developer/tutorials/server_framework_101_legacy/06_basicviews.rst
developer/tutorials/server_framework_101/07_relations.rst developer/tutorials/server_framework_101_legacy/07_relations.rst
developer/tutorials/server_framework_101/08_compute_onchange.rst developer/tutorials/server_framework_101_legacy/08_compute_onchange.rst
developer/tutorials/server_framework_101/09_actions.rst developer/tutorials/server_framework_101_legacy/09_actions.rst
developer/tutorials/server_framework_101/10_constraints.rst developer/tutorials/server_framework_101_legacy/10_constraints.rst
developer/tutorials/server_framework_101/11_sprinkles.rst developer/tutorials/server_framework_101_legacy/11_sprinkles.rst
developer/tutorials/server_framework_101/12_inheritance.rst developer/tutorials/server_framework_101_legacy/12_inheritance.rst
developer/tutorials/server_framework_101/13_other_module.rst developer/tutorials/server_framework_101_legacy/13_other_module.rst
developer/tutorials/server_framework_101/14_qwebintro.rst developer/tutorials/server_framework_101_legacy/14_qwebintro.rst
developer/tutorials/server_framework_101/15_final_word.rst developer/tutorials/server_framework_101_legacy/15_final_word.rst