Compare commits

...

5 Commits

Author SHA1 Message Date
Antoine Vandevenne (anv)
61b62525ca scheduled actions 2025-02-24 17:07:07 +01:00
Antoine Vandevenne (anv)
5fcfde48a4 model actions 2025-02-20 18:15:12 +01:00
Antoine Vandevenne (anv)
df1e582b93 documentation for -> documentation on 2025-02-19 11:47:49 +01:00
Antoine Vandevenne (anv)
81929ceb1c chapter 5 - connect the dots 2025-02-19 11:47:49 +01:00
Antoine Vandevenne (anv)
32cba451c1 [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
2025-02-19 11:47:49 +01:00
88 changed files with 4683 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
-----------
@ -808,6 +813,8 @@ In general in Odoo, when manipulating strings, prefer ``%`` over ``.format()``
of position (when multiple variables have to be replaced). This makes the
translation easier for the community translators.
.. _contributing/coding_guidelines/model_members:
Symbols and Conventions
-----------------------

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:
@ -510,6 +518,8 @@ behavior is desired:
:class:`~odoo.fields.Many2one`
:type: :class:`~odoo.addons.base.models.res_company`
.. _reference/orm/recordsets:
Recordsets
==========

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_connect_the_dots`
- :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,574 @@
==============================
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, `required`
makes filling in the field mandatory, and `default` provides a default field value.
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.
.. 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="Sales 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.
.. seealso::
For the full list of fields and their attributes, see the :ref:`reference documentation
<reference/orm/fields>`.
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:
- :guilabel:`Name` (required)
- :guilabel:`Description`
- :guilabel:`Image` (max 600x400 pixels)
- :guilabel:`Active` (whether the property listing is active; defaults to true)
- :guilabel:`State` (:guilabel:`New`, :guilabel:`Offer Received`, :guilabel:`Under Option`, or
:guilabel:`Sold`; required; defaults to :guilabel:`New`)
- :guilabel:`Type` (:guilabel:`House`, :guilabel:`Apartment`, :guilabel:`Office Building`,
:guilabel:`Retail Space`, or :guilabel:`Warehouse`; required; defaults to :guilabel:`House`)
- :guilabel:`Selling Price` (without currency; with help text; required)
- :guilabel:`Availability Date`
- :guilabel:`Floor Area` (in square meters; with help text)
- :guilabel:`Number of Bedrooms` (defaults to two)
- :guilabel:`Garage` (whether there is a garage)
- :guilabel:`Garden` (whether there is a garden)
- :guilabel:`Garden Area` (in square meters; with help text)
.. tip::
- The class name doesn't matter, but the convention is to use the model's upper-cased `_name`
(without dots).
- Refer to the documentation on :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
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")
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_garage = fields.Boolean(string="Garage")
has_garden = fields.Boolean(string="Garden")
garden_area = fields.Integer(
string="Garden Area", help="The garden area excluding the building."
)
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
:caption: terminal
$ 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 | | |
garden_area | 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_garage | boolean | | |
has_garden | 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 on 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.
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.
.. seealso::
:doc:`Reference documentation on XML data files <../../reference/backend/data>`
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="availability_date">2024-08-01</field>
<field name="floor_area">416</field>
<field name="bedrooms">5</field>
<field name="has_garage">True</field>
<field name="has_garden">True</field>
<field name="garden_area">2100</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_garage">True</field>
<field name="has_garden">False</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_garage">False</field>
<field name="has_garden">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.
.. 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.
.. seealso::
:ref:`Reference documentation on CSV data files <reference/data/csvdatafiles>`
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:: python
: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,712 @@
===================================
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 :guilabel:`Contacts`, :guilabel:`Sales`, and :guilabel:`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 :guilabel:`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 :guilabel:`Properties` and :guilabel:`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 :guilabel:`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.
.. 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.
.. seealso::
:ref:`Reference documentation on window actions <reference/actions/window>`
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 :guilabel:`Properties` window action that opens `real.estate.property` records
in list and form views, and assign it to the :guilabel:`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 :guilabel:`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).
.. 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.
.. seealso::
- :doc:`Reference documentation on view records <../../reference/user_interface/view_records>`
- :doc:`Reference documentation on view architectures
<../../reference/user_interface/view_architectures>`
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: :guilabel:`Name`, :guilabel:`State`, :guilabel:`Type`,
:guilabel:`Selling Price`, :guilabel:`Availability Date`, :guilabel:`Floor Area`,
:guilabel:`Number of Bedrooms`, :guilabel:`Garage`, :guilabel:`Garden`, and
:guilabel:`Garden Area`.
#. Make the visibility of :guilabel:`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 manually 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::
Refer to the documentation on :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_garage" optional="hide"/>
<field name="has_garden" optional="hide"/>
<field name="garden_area" 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: :guilabel:`Type`, :guilabel:`Selling Price`,
:guilabel:`Availability Date`, :guilabel:`Active`
- Building Specifications: :guilabel:`Floor Area`, :guilabel:`Number of Bedrooms`,
:guilabel:`Garage`, :guilabel:`Garden`, :guilabel:`Garden Area`
- 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::
- Refer to the documentation on :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_garage"/>
<field name="has_garden"/>
<field name="garden_area"/>
</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.
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.
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 on search views <reference/view_architectures/search>`
- :ref:`Reference documentation on search domains <reference/orm/domains>`
- :ref:`Reference documentation of the list of reserved field names
<reference/orm/fields/reserved>`
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:
- :guilabel:`Name`: Match records whose name contain the search value.
- :guilabel:`Description`: Match records whose description *or* name contains the search
value.
- :guilabel:`Selling Price`: Match records with a price *less than or equal to* the search
value.
- :guilabel:`Floor Area`: Match records with a floor area *at least* the search value.
- :guilabel:`Number of Bedrooms`: Match records with *at least* the given number of
bedrooms.
- Implement these filters:
- :guilabel:`For Sale`: The state is :guilabel:`New` or :guilabel:`Offer Received`.
- :guilabel:`Availability Date`: Display a list of pre-defined availability date values.
- :guilabel:`Garage`: The property has a garage.
- :guilabel:`Garden`: The property has a garden.
- :guilabel:`Archived`: The property is archived.
- Combine selected filters with a logical AND, except for Garage and Garden, 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::
- Refer to the documentation on :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_garage" string="Garage" domain="[('has_garage', '=', True)]"/>
<filter name="filter_garden" string="Garden" domain="[('has_garden', '=', 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: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
===========================
Chapter 6: Tighten security
===========================
tmp
.. todo: restrict access through acl
.. todo: rule to only see your properties or unassigned ones
----
.. todo: add incentive for next chapter

View File

@ -0,0 +1,25 @@
=========================
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"/>
<field name="is_priority" widget="boolean_favorite" nolabel="1" readonly="False"/> (/!\ requires to have product installed to have the correct font-size in form view)
.. todo: add Gantt view of properties availability
.. todo: add Kanban view of properties
.. todo: wizards -> create a "receive offer wizard" to default the amount to the property's selling price
.. todo: context active_test False on the category_id field of products to see archived categories
.. todo: sequence widget on tags
.. todo: compute display_name for offers in form view
.. todo: multi edit offers state
.. todo: hide the 'Cancelled' state from the statusbar widget unless selected
.. todo: pills for the offer state in list view
.. todo: adapt existing or add new stat button relying on a model method that returns a dictionary acting as an action descriptor
----
.. todo: add incentive for next chapter

View File

@ -0,0 +1,15 @@
======================
Chapter 8: Inheritance
======================
tmp
.. todo: inherit from mail.tread mixin and add a chatter
.. todo: self.env._context
.. todo: inherit from account.move
.. todo: ex: override of real.estate.property::create to set vals['address_id'] = (0, 0, {})
.. todo: ex: create invoice with lines (6, 0, 0)
----
.. 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: run with coverage
----

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 an exercise 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