chapter 5 - connect the dots

This commit is contained in:
Antoine Vandevenne (anv) 2024-08-19 17:01:46 +02:00
parent 32cba451c1
commit 81929ceb1c
15 changed files with 1560 additions and 209 deletions

View File

@ -813,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

@ -518,6 +518,8 @@ behavior is desired:
:class:`~odoo.fields.Many2one`
:type: :class:`~odoo.addons.base.models.res_company`
.. _reference/orm/recordsets:
Recordsets
==========

View File

@ -39,7 +39,7 @@ Ready? Let's get started!
- :doc:`server_framework_101/02_lay_the_foundations`
- :doc:`server_framework_101/03_build_user_interface`
- :doc:`server_framework_101/04_relational_fields`
- :doc:`server_framework_101/05_business_logic`
- :doc:`server_framework_101/05_connect_the_dots`
- :doc:`server_framework_101/06_security`
- :doc:`server_framework_101/07_advanced_views`
- :doc:`server_framework_101/08_inheritance`

View File

@ -65,8 +65,8 @@ classes, fields are defined as class attributes. Each field is an instance of a
`odoo.fields` package. For example, `Char`, `Float`, `Boolean`, each designed to handle different
types of data. When defining a field, developers can pass various arguments to finely control how
data is handled and presented in Odoo. For example, `string` defines the label for the field in the
user interface, `help` provides a tooltip when hovering the field in the user interface, and
`required` makes filling in the field mandatory.
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
@ -74,10 +74,6 @@ represented as instances of the model's class, allowing developers to interact w
object-oriented programming techniques. For example, in a real estate application using a tenant
model, each specific tenant (such as "Bafien Carpink") would be a separate record of that model.
.. seealso::
For the full list of fields and their attributes, see the :ref:`reference documentation
<reference/orm/fields>`.
.. example::
Before we dive into creating our own models, let's take a look at a basic example of a model that
represents storable products. It defines a `product` model with the `Product` class inheriting
@ -94,12 +90,12 @@ model, each specific tenant (such as "Bafien Carpink") would be a separate recor
name = fields.Char(string="Name", required=True)
description = fields.Text(string="Description")
price = fields.Float(string="Sale Price", required=True)
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")
('apparel', "Clothing"),
('electronics', "Electronics"),
('home_decor', "Home Decor"),
('other', "Other"),
@ -116,11 +112,14 @@ model, each specific tenant (such as "Bafien Carpink") would be a separate recor
- `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:
@ -132,19 +131,21 @@ create a model with some fields to represent real estate properties and their ch
#. Define a new model with `real.estate.property` as `_name` and a short `_description`.
#. Add fields to represent the following characteristics:
- Name (required)
- Description
- Image (max 600x400 pixels)
- Active (default to true)
- State (new, offer received, under option, or sold; required; default to new)
- Type (house, apartment, office building, retail space, or warehouse; required; default to
house)
- Selling Price (without currency; with help text; required)
- Availability Date (default to creation date + two months)
- Floor Area (in square meters; with help text)
- Number of Bedrooms (default to two)
- Whether there is a garden
- Whether there is a garage
- :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`
@ -163,7 +164,6 @@ create a model with some fields to represent real estate properties and their ch
:caption: `real_estate_property.py`
from odoo import fields, models
from odoo.tools import date_utils
class RealEstateProperty(models.Model):
@ -200,15 +200,16 @@ create a model with some fields to represent real estate properties and their ch
selling_price = fields.Float(
string="Selling Price", help="The selling price excluding taxes.", required=True
)
availability_date = fields.Date(
string="Availability Date", default=date_utils.add(fields.Date.today(), months=2)
)
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_garden = fields.Boolean(string="Garden")
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
@ -235,7 +236,6 @@ 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.
@ -246,6 +246,7 @@ you created translate into a new SQL table. We will use `psql`, the CLI
.. spoiler:: Solution
.. code-block:: text
:caption: terminal
$ psql -d tutorials
@ -256,6 +257,7 @@ you created translate into a new SQL table. We will use `psql`, the CLI
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 |
@ -264,8 +266,8 @@ you created translate into a new SQL table. We will use `psql`, the CLI
availability_date | date | | |
description | text | | |
active | boolean | | |
has_garden | 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 |
@ -343,9 +345,6 @@ The most common data operation is creating new records through the `record` and
elements, but other operations exist, such as `delete`, which deletes previously created records, or
even `function`, which allows executing arbitrary code.
.. seealso::
:doc:`Reference documentation for XML data files <../../reference/backend/data>`
Some data operations require their data elements to be uniquely identified by the system. This is
achieved by means of the `id` attribute, also known as the **XML ID** or **external identifier**. It
provides a way for other elements to reference it with the `ref` attribute and links data elements
@ -387,10 +386,12 @@ created from a data file so that records can be referenced by their full XML ID
- The `ref` attribute is used to reference other records by their XML ID and use their record
ID as value.
.. seealso::
:doc:`Reference documentation for 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
@ -441,10 +442,12 @@ Let's now load some default real estate properties in our database.
<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_garden">True</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">
@ -456,8 +459,8 @@ Let's now load some default real estate properties in our database.
<field name="availability_date">2025-01-01</field>
<field name="floor_area">195</field>
<field name="bedrooms">3</field>
<field name="has_garden">False</field>
<field name="has_garage">True</field>
<field name="has_garden">False</field>
</record>
<record id="real_estate.mixed_use_commercial" model="real.estate.property">
@ -469,8 +472,8 @@ Let's now load some default real estate properties in our database.
<field name="availability_date">2024-10-02</field>
<field name="floor_area">370</field>
<field name="bedrooms">0</field>
<field name="has_garden">False</field>
<field name="has_garage">False</field>
<field name="has_garden">False</field>
</record>
</odoo>
@ -484,9 +487,6 @@ In addition to XML data files, the server framework allows loading data files in
format is often more convenient for describing records with simple field values belonging to the
same model. It also loads faster, making it the go-to format when performance matters most.
.. seealso::
:ref:`Reference documentation for CSV data files <reference/data/csvdatafiles>`
.. example::
See below for an example of how a subset of `country states can be loaded into Odoo
<{GITHUB_PATH}/odoo/addons/base/data/res.country.state.csv>`_.
@ -510,7 +510,6 @@ same model. It also loads faster, making it the go-to format when performance ma
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.
@ -518,6 +517,9 @@ same model. It also loads faster, making it the go-to format when performance ma
as value.
- Each subsequent line describes one new record.
.. seealso::
:ref:`Reference documentation for 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
@ -532,7 +534,6 @@ began being logged at server start-up after creating the model:
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
@ -550,7 +551,7 @@ began being logged at server start-up after creating the model:
.. spoiler:: Solution
.. code-block:: py
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 2

View File

@ -16,8 +16,8 @@ Add menus items
different parts of Odoo and can be nested to form a hierarchical structure. This allows the
functionalities of complex applications to be organized into categories and sub-categories and makes
them easier to navigate. The top level of the menu structure typically contains the menu items for
the main applications (like "Contacts", "Sales", and "Accounting"). These top-level menu items can
also be visually enhanced with custom icons for better recognition.
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:
@ -47,15 +47,16 @@ a data file. Lets do just that and add menu items to our real estate app!
.. exercise::
#. Create and declare a new :file:`menus.xml` file at the root of the `real_estate` module.
#. Describe a new "Real Estate" menu item to serve as root menu for our real estate app.
#. 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 "Properties" and "Settings" menu items under the root menu item. As we have not yet
created an action to browse properties or open settings, reference the following existing
actions instead:
#. 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.
@ -128,7 +129,7 @@ it simplifies the syntax and automatically handles some technical details for yo
</menuitem>
.. note::
- The outer `menuitem` data operation creates the top-level "Product" menu item.
- 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.
@ -197,9 +198,6 @@ the `ir.actions.act_window` model whose key fields include:
`help`
An optional help text for the users when there are no records to display.
.. seealso::
:doc:`Reference documentation for actions <../../reference/backend/actions>`
.. example::
The example below defines an action to open existing products in either list or form view.
@ -220,16 +218,18 @@ the `ir.actions.act_window` model whose key fields include:
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 for 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 "Properties" window action that opens `real.estate.property` records in list
and form views, and assign it to the "Properties" menu item. Be creative with the help text!
For reference, the list of supported classes can be found in the `view.scss
<{GITHUB_PATH}/addons/web/static/src/views/view.scss>`_ file.
#. 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
@ -279,9 +279,10 @@ now is an action to assign to the menu item.
action="real_estate.view_properties_action"
/>
Clicking the "Properties" menu item now displays a list view of the default properties we created
earlier. As we specified in the action that both list and form views were allowed, you can click any
property record to display its form view. Delete all three records to see the help text you created.
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:
@ -315,11 +316,6 @@ structure and content of the view. These components can be structural (like `she
layout responsive, or `group` that defines column layouts) or semantic (like `field` that displays
field labels and values).
.. seealso::
- :doc:`Reference documentation for view records <../../reference/user_interface/view_records>`
- :doc:`Reference documentation for view architectures
<../../reference/user_interface/view_architectures>`
.. example::
The following examples demonstrate how to define simple list, form and search views for the
`product` model.
@ -374,10 +370,14 @@ field labels and values).
</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 for view records <../../reference/user_interface/view_records>`
- :doc:`Reference documentation for 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
@ -397,15 +397,17 @@ List view
For a start, the list view could use more fields than just the name.
.. exercise::
#. Create a new :file:`real_estate_property_views.xml` file at the root of the `real_estate`
module.
#. Create a custom list view to display the following fields of the `real.estate.property` model
in the given order: name, state, type, selling price, availability date, floor area, number of
bedrooms, presence of a garden, and presence of a garage.
#. Make the visibility of the floor area and all following fields optional so that only the floor
area is visible by default, while the remaining fields are hidden by default and must be
displayed by accessing the view's column selector (:icon:`oi-settings-adjust` button).
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::
@ -449,8 +451,9 @@ For a start, the list view could use more fields than just the name.
<field name="availability_date"/>
<field name="floor_area" optional="show"/>
<field name="bedrooms" optional="hide"/>
<field name="has_garden" optional="hide"/>
<field name="has_garage" optional="hide"/>
<field name="has_garden" optional="hide"/>
<field name="garden_area" optional="hide"/>
</list>
</field>
</record>
@ -463,7 +466,6 @@ 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:
@ -475,8 +477,10 @@ Form view
- The image should be displayed as a thumbnail on the right side of the form.
- The fields should be grouped in two sections displayed next to each other:
- Listing Information: Type, Selling Price, Availability Date, Active
- Building Specifications: Floor Area, Number of Bedrooms, Garden, Garage
- 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.
@ -525,8 +529,9 @@ Form view
<group string="Building Specifications">
<field name="floor_area"/>
<field name="bedrooms"/>
<field name="has_garden"/>
<field name="has_garage"/>
<field name="has_garden"/>
<field name="garden_area"/>
</group>
</group>
<separator string="Description"/>
@ -555,19 +560,12 @@ automatically excluded from searches. You can observe this behavior by deselecti
:guilabel:`Active` checkbox for one of your property records: you'll notice the record no longer
appears upon returning to the list view.
.. seealso::
:ref:`Reference documentation for the list of reserved field names
<reference/orm/fields/reserved>`
To facilitate the browsing of archived properties, we need to create a search view. Unlike list and
form views, search views are not used to display record data on screen. Instead, they define the
search behavior and enable users to search on specific fields. They also provide pre-defined
**filters** that allow for quickly searching with complex queries and grouping records by particular
fields.
.. seealso::
:ref:`Reference documentation for search views <reference/view_architectures/search>`
The most common way to set up filters is through **search domains**. Domains are used to select
specific records of a model by defining a list of criteria. Each criterion is a triplet in the
format :code:`(<field_name>, <operator>, <value>)`.
@ -594,32 +592,37 @@ before its operands`.
['|', ('category', '=', 'electronics'), '!', '&', ('price', '>=', 1000), ('price', '<', 2000)]
.. seealso::
:ref:`Reference documentation for search domains <reference/orm/domains>`
- :ref:`Reference documentation for search views <reference/view_architectures/search>`
- :ref:`Reference documentation for search domains <reference/orm/domains>`
- :ref:`Reference documentation for 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:
- Name: Match records whose name contain the search value.
- Description: Match records whose description *or* name contains the search value.
- Selling price: Match records with a price *less than or equal to* the search value.
- Floor area: Match records with a floor area *at least* the search value.
- Number of bedrooms: Match records with *at least* the given number of bedrooms.
- :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:
- For Sale: The state is "New" or "Offer Received".
- Availability Date: Display a list of pre-defined availability date values.
- Garden: The property has a garden.
- Garage: The property has a garage.
- Archived: The property is archived.
- :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 Garden and Garage, which should use
- 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.
@ -672,8 +675,8 @@ Let's enhance the search capabilities.
<separator/>
<filter name="filter_availability" date="availability_date"/>
<separator/>
<filter name="filter_garden" string="Garden" domain="[('has_garden', '=', True)]"/>
<filter name="filter_garage" string="Garage" domain="[('has_garage', '=', True)]"/>
<filter name="filter_garden" string="Garden" domain="[('has_garden', '=', True)]"/>
<separator/>
<filter name="filter_inactive" string="Archived" domain="[('active', '=', False)]"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -10,8 +10,8 @@ of our real estate application.
.. _tutorials/server_framework_101/module_structure:
Module structure
================
Organize your module structure
==============================
As our `real_estate` module grows, you may notice that we've already created a dozen files for just
one model, along with its menu items, actions and views. With more models on the horizon, our module
@ -25,10 +25,6 @@ structure guidelines** that offer several benefits:
- **Collaboration**: A standardized structure facilitates understanding among contributors and
ensures easier integration with the Odoo ecosystem.
.. seealso::
:ref:`Coding guidelines on module directories
<contributing/coding_guidelines/module_structure/directories>`
.. example::
Let's consider a possible structure for our example `product` module:
@ -62,7 +58,6 @@ structure guidelines** that offer several benefits:
└── __manifest__.py
.. note::
- The :file:`models` directory contains its own :file:`__init__.py` file, simplifying Python
imports. The root :file:`__init__.py` file imports the :file:`models` Python package, which
in turns imports individual model files.
@ -77,8 +72,11 @@ structure guidelines** that offer several benefits:
- The :file:`__init__.py` and :file:`__manifest__.py` files remain in the module's root
directory.
.. exercise::
.. seealso::
:ref:`Coding guidelines on module directories
<contributing/coding_guidelines/module_structure/directories>`
.. exercise::
Restructure the `real_estate` module according to the guidelines.
.. tip::
@ -193,8 +191,8 @@ structure guidelines** that offer several benefits:
.. _tutorials/server_framework_101/many2one:
Many-to-one
===========
Many-to-one relationships
=========================
As promised at the end of :doc:`the previous chapter <03_build_user_interface>`, we'll now expand
our app's capabilities by adding new models to manage additional information. This expansion
@ -211,14 +209,11 @@ representing the *many* side of the relationship. The field is represented in th
record. By convention, `Many2one` field names end with the `_id` suffix, indicating that they store
the referenced record's ID.
.. seealso::
:ref:`Reference documentation for Many2one fields <reference/fields/many2one>`
.. example::
In the example below, the `Selection` field of the `product` model is replaced by a `Many2one`
field to create a more flexible and scalable model structure.
.. code-block:: py
.. code-block:: python
from odoo import fields, models
@ -239,17 +234,18 @@ the referenced record's ID.
name = fields.Char(string="Name")
.. note::
- The relationship only needs to be declared on the *many* side to be established.
- The `ondelete` argument on the `Many2one` field defines what happens when the referenced
record is deleted.
.. seealso::
:ref:`Reference documentation for Many2one fields <reference/fields/many2one>`
In our real estate app, we currently have a fixed set of property types. To increase flexibility,
let's replace the current `type` field with a many-to-one relationship to a separate model for
managing property types.
.. exercise::
#. Create a new `real.estate.property.type` model.
- Update the :file:`ir.model.access.csv` file to grant all database administrators access to
@ -258,12 +254,12 @@ managing property types.
--> Property Types` menu item.
- Create a window action to browse property types only in list view.
- Create the list view for property types.
- In a data file, describe at least as many default property types as the `type` field of the
`real.estate.property` model supports.
- In a data file, describe at least as many default property types as the :guilabel:`Type`
field of the `real.estate.property` model supports.
#. Replace the `type` field on the `real.estate.property` model by a many-to-one relationship to
the `real.estate.property.type` model. Prevent deleting property types if a property
references them.
#. Replace the :guilabel:`Type` field on the `real.estate.property` model by a many-to-one
relationship to the `real.estate.property.type` model. Prevent deleting property types if a
property references them.
.. tip::
@ -281,7 +277,6 @@ managing property types.
:caption: `real_estate_property_type.py`
from odoo import fields, models
from odoo.tools import date_utils
class RealEstatePropertyType(models.Model):
@ -290,7 +285,7 @@ managing property types.
name = fields.Char(string="Name", required=True)
.. code-block:: py
.. code-block:: python
:caption: `__init__.py`
:emphasize-lines: 2
@ -372,7 +367,7 @@ managing property types.
</odoo>
.. code-block:: py
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 3,4,7,9
@ -387,7 +382,7 @@ managing property types.
'views/menus.xml', # Depends on actions in views.
],
.. code-block:: py
.. code-block:: python
:caption: `real_estate_property.py`
:emphasize-lines: 1-3
@ -473,35 +468,40 @@ Two frequently used models in Odoo are:
.. seealso::
`The list of generic models in the base module <{GITHUB_PATH}/odoo/addons/base/models>`_
To make our real estate properties more informative, let's add two pieces of information: the seller
of the property and the salesperson managing the property.
To make our real estate properties more informative, let's add three pieces of information: the
seller of the property, the salesperson managing the property, and the address of the property.
.. exercise::
#. Add the following fields to the `real.estate.property` model:
- Seller (required): The person putting their property on sale; it can be any individual.
- Salesperson: The employee of the real estate agency overseeing the sale of the property.
- :guilabel:`Seller` (required): The person putting their property on sale; it can be any
individual.
- :guilabel:`Salesperson`: The employee of the real estate agency overseeing the sale of the
property.
- :guilabel:`Address` (required): The address of the property.
#. Modify the form view of properties to include a notebook component. The property description
should be in the first page, and the two new fields should be in the second page.
should be in the first page, and the three new fields should be in the second page.
.. tip::
You don't need to define any new UI component to browse the seller you assigned to your
default properties! Just go to :menuselection:`Apps` and install the :guilabel:`Contacts` app.
- You don't need to define any new UI component to browse the seller you assigned to your
default properties! Just go to :menuselection:`Apps` and install the :guilabel:`Contacts`
app.
- In Odoo, addresses are usually represented by a partner.
.. spoiler:: Solution
.. code-block:: python
:caption: `real_estate_property.py`
:emphasize-lines: 1-2
:emphasize-lines: 1-3
address_id = fields.Many2one(string="Address", comodel_name='res.partner', required=True)
seller_id = fields.Many2one(string="Seller", comodel_name='res.partner', required=True)
salesperson_id = fields.Many2one(string="Salesperson", comodel_name='res.users')
.. code-block:: xml
:caption: `real_estate_property_views.xml`
:emphasize-lines: 3-18
:emphasize-lines: 3-19
<record id="real_estate.property_form" model="ir.ui.view">
[...]
@ -515,6 +515,7 @@ of the property and the salesperson managing the property.
<page string="Other Info">
<group>
<group>
<field name="address_id"/>
<field name="seller_id"/>
<field name="salesperson_id"/>
</group>
@ -530,6 +531,30 @@ of the property and the salesperson managing the property.
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="real_estate.country_house_address" model="res.partner">
<field name="name">Country House</field>
<field name="street">Chaussée de Namur 40</field>
<field name="city">Grand-Rosière-Hottomont</field>
<field name="zip">1367</field>
<field name="country_id" ref="base.be"/>
</record>
<record id="real_estate.loft_address" model="res.partner">
<field name="name">Loft</field>
<field name="street">Rue des Bourlottes 9</field>
<field name="city">Grand-Rosière-Hottomont</field>
<field name="zip">1367</field>
<field name="country_id" ref="base.be"/>
</record>
<record id="real_estate.mixed_use_commercial_address" model="res.partner">
<field name="name">Mixed use commercial building</field>
<field name="street">Rue de Ramillies 1</field>
<field name="city">Grand-Rosière-Hottomont</field>
<field name="zip">1367</field>
<field name="country_id" ref="base.be"/>
</record>
<record id="real_estate.bafien_carpink" model="res.partner">
<field name="name">Bafien Carpink</field>
</record>
@ -546,24 +571,27 @@ of the property and the salesperson managing the property.
.. code-block:: xml
:caption: `real_estate_property_data.xml`
:emphasize-lines: 3,8,13
:emphasize-lines: 3-4,9-10,15-16
<record id="real_estate.country_house" model="real.estate.property">
[...]
<field name="address_id" ref="real_estate.country_house_address"/>
<field name="seller_id" ref="real_estate.amyfromthevideos"/>
</record>
<record id="real_estate.loft" model="real.estate.property">
[...]
<field name="address_id" ref="real_estate.loft_address"/>
<field name="seller_id" ref="real_estate.antony_petisuix"/>
</record>
<record id="real_estate.mixed_use_commercial" model="real.estate.property">
[...]
<field name="address_id" ref="real_estate.mixed_use_commercial_address"/>
<field name="seller_id" ref="real_estate.bafien_carpink"/>
</record>
.. code-block:: py
.. code-block:: python
:caption: `__manifest__.py`
:emphasize-lines: 3,5,6
@ -578,8 +606,8 @@ of the property and the salesperson managing the property.
.. _tutorials/server_framework_101/one2many:
One-to-many
===========
One-to-many relationships
=========================
After exploring how to connect multiple records to a single one with many-to-one relationships,
let's consider their counterparts: **one-to-many relationships**. These relationships represent the
@ -592,14 +620,11 @@ note that `One2many` fields don't store data in the database; instead, they prov
that Odoo computes based on the referenced `Many2one` field. By convention, `One2many` field names
end with the `_ids` suffix, indicating that they allow accessing the IDs of the connected records.
.. seealso::
:ref:`Reference documentation for One2many fields <reference/fields/one2many>`
.. example::
In the example below, a `One2many` field is added to the `product.category` model to allow quick
access to the connected products from the product category.
.. code-block:: py
.. code-block:: python
from odoo import fields, models
@ -623,28 +648,33 @@ end with the `_ids` suffix, indicating that they allow accessing the IDs of the
)
.. note::
The `One2many` field must reference its `Many2one` counterpart through the `inverse_name`
argument.
.. seealso::
:ref:`Reference documentation for One2many fields <reference/fields/one2many>`
A good use case for a one-to-many relationship in our real estate app would be to connect properties
to a list of offers received from potential buyers.
.. exercise::
#. Create a new `real.estate.offer` model. It should have the following fields:
- Amount (required): The amount offered to buy the property.
- Buyer (required): The person making the offer.
- Date (required; default to creation date): When the offer was made.
- Validity (default to 7): The number of days before the offer expires.
- State (required): Either "Waiting", "Accepted", or "Refused".
- :guilabel:`Amount` (required): The amount offered to buy the property.
- :guilabel:`Buyer` (required): The person making the offer.
- :guilabel:`Date` (required): When the offer was made.
- :guilabel:`Validity` (defaults to 7): The number of days before the offer expires.
- :guilabel:`State` (required): Either :guilabel:`Waiting`, :guilabel:`Accepted`, or
:guilabel:`Refused`.
#. Create a list and form views for the `real.estate.offer` model. It's not necessary to create
menu items or actions, as offers will be accessible from properties, but feel free to do it
anyway!
#. Allow connecting properties to multiple offers.
#. Modify the form view of properties to display offers in a new notebook page titled "Offers".
#. Modify the form view of properties to display offers in a new notebook page titled
:guilabel:`Offers`.
.. spoiler:: Solution
@ -660,7 +690,7 @@ to a list of offers received from potential buyers.
amount = fields.Float(string="Amount", required=True)
buyer_id = fields.Many2one(string="Buyer", comodel_name='res.partner', required=True)
date = fields.Date(string="Date", required=True, default=fields.Date.today())
date = fields.Date(string="Date", required=True)
validity = fields.Integer(
string="Validity", help="The number of days before the offer expires.", default=7
)
@ -752,7 +782,7 @@ to a list of offers received from potential buyers.
'views/menus.xml', # Depends on actions in views.
],
.. code-block:: py
.. code-block:: python
:caption: `real_estate_property.py`
:emphasize-lines: 1-3
@ -774,8 +804,8 @@ to a list of offers received from potential buyers.
.. _tutorials/server_framework_101/many2many:
Many-to-many
============
Many-to-many relationships
==========================
After the many-to-one and one-to-many relationships, let's consider a more complex use case:
**many-to-many relationships**. These relationships enable *multiple* records in one model to be
@ -788,14 +818,11 @@ intermediate (junction) table in the database. This table stores pairs of IDs, e
representing a connection between a record of the first model and a record of the second model. By
convention, `Many2many` field names end with the `_ids` suffix, like for `One2many` fields.
.. seealso::
:ref:`Reference documentation for Many2many fields <reference/fields/many2many>`
.. example::
In the example below, a many-to-many relationship is established between the `product` model and
the `res.partner` model, which is used to represent sellers offering products for sale.
.. code-block:: py
.. code-block:: python
from odoo import fields, models
@ -815,22 +842,23 @@ convention, `Many2many` field names end with the `_ids` suffix, like for `One2ma
)
.. note::
- It is not necessary to add a `Many2many` field to both models of the relationship.
- The optional `relation`, `column1`, and `column2` field arguments allow specifying the name
of the junction table and of its columns.
.. seealso::
:ref:`Reference documentation for Many2many fields <reference/fields/many2many>`
Let's conclude this extension of the model family by allowing to associate multiple description tags
with each property.
.. exercise::
#. Create a new `real.estate.tag` model. It should have the following fields:
- Name (required): The label of the tag.
- Color: The color code to use for the tag, as an integer.
- :guilabel:`Name` (required): The label of the tag.
- :guilabel:`Color`: The color code to use for the tag, as an integer.
#. In a data file, describe various default property tags. For example, "Renovated".
#. In a data file, describe various default property tags. For example, :guilabel:`Renovated`.
#. Create all necessary UI components to manage tags from the :guilabel:`Configuration` category
menu item.
#. Allow connecting properties to multiple tags, and tags to multiple properties.
@ -982,7 +1010,7 @@ with each property.
<field
name="tag_ids"
widget="many2many_tags"
options="{'color_field': 'color', 'no_quick_create': True, 'no_create_edit': True}"
options="{'color_field': 'color', 'no_quick_create': True, 'no_create': True}"
/>
[...]
</record>

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,12 @@
===================
Chapter 6: Security
===================
===========================
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

@ -4,11 +4,17 @@ Chapter 7: Advanced views
tmp
.. todo:: invisible, required, readonly modifiers
.. todo:: introduce bootstrap
.. todo:: widgets; eg, <widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
.. todo:: add Gantt view of properties availability
.. todo:: add Kanban view of properties
.. todo: 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
----

View File

@ -4,7 +4,13 @@ Chapter 8: Inheritance
tmp
.. todo:: inherit from mail.tread mixin and add a chatter
.. todo: inherit from mail.tread mixin and add a chatter
.. todo: self.env._context
.. todo: inherit from account.move
.. todo: explain magic commands
.. 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: ex: 6,0,0 to associate tags to properties in data
----

View File

@ -78,7 +78,7 @@ GitHub.
#. Once you have pushed your first change to the shared fork on **odoo-dev**, create a **draft**
:abbr:`PR (Pull Request)` with your quadrigram in the title. This will enable you to share
your upcoming work and receive feedback from your coaches. To ensure a continuous feedback
loop, push a new commit as soon as you complete a chapter of a tutorial.
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 tests your code. Once logged in, you will be able to see your branch on the `Tutorials