diff --git a/content/contributing/development/coding_guidelines.rst b/content/contributing/development/coding_guidelines.rst index 32b7aa9f2..b7f4c8966 100644 --- a/content/contributing/development/coding_guidelines.rst +++ b/content/contributing/development/coding_guidelines.rst @@ -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 ----------------------- diff --git a/content/developer/reference/backend/orm.rst b/content/developer/reference/backend/orm.rst index fbbd739b7..cc8838365 100644 --- a/content/developer/reference/backend/orm.rst +++ b/content/developer/reference/backend/orm.rst @@ -518,6 +518,8 @@ behavior is desired: :class:`~odoo.fields.Many2one` :type: :class:`~odoo.addons.base.models.res_company` +.. _reference/orm/recordsets: + Recordsets ========== diff --git a/content/developer/tutorials/server_framework_101.rst b/content/developer/tutorials/server_framework_101.rst index 31693b405..88b268632 100644 --- a/content/developer/tutorials/server_framework_101.rst +++ b/content/developer/tutorials/server_framework_101.rst @@ -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` diff --git a/content/developer/tutorials/server_framework_101/02_lay_the_foundations.rst b/content/developer/tutorials/server_framework_101/02_lay_the_foundations.rst index 2964cd5a0..8cedcfd32 100644 --- a/content/developer/tutorials/server_framework_101/02_lay_the_foundations.rst +++ b/content/developer/tutorials/server_framework_101/02_lay_the_foundations.rst @@ -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 - `. - .. 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 + `. + 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. house 745000 + 2024-08-01 416 5 - True True + True + 2100 @@ -456,8 +459,8 @@ Let's now load some default real estate properties in our database. 2025-01-01 195 3 - False True + False @@ -469,8 +472,8 @@ Let's now load some default real estate properties in our database. 2024-10-02 370 0 - False False + False @@ -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 ` - .. 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 ` + 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 diff --git a/content/developer/tutorials/server_framework_101/03_build_user_interface.rst b/content/developer/tutorials/server_framework_101/03_build_user_interface.rst index 2000b89da..a5e08baae 100644 --- a/content/developer/tutorials/server_framework_101/03_build_user_interface.rst +++ b/content/developer/tutorials/server_framework_101/03_build_user_interface.rst @@ -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. Let’s 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 `,`. - #. 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 .. 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 ` data operation. +.. seealso:: + :ref:`Reference documentation for window actions ` + 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). .. 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 `, 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. - + + @@ -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 - + + @@ -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 - ` - 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 ` - 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:`(, , )`. @@ -594,32 +592,37 @@ before its operands`. ['|', ('category', '=', 'electronics'), '!', '&', ('price', '>=', 1000), ('price', '<', 2000)] .. seealso:: - :ref:`Reference documentation for search domains ` + - :ref:`Reference documentation for search views ` + - :ref:`Reference documentation for search domains ` + - :ref:`Reference documentation for the list of reserved field names + ` 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. - + diff --git a/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-form-view.png b/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-form-view.png index 3bb3bd923..7d798cdeb 100644 Binary files a/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-form-view.png and b/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-form-view.png differ diff --git a/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-list-view.png b/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-list-view.png index 1fd824406..6b812913e 100644 Binary files a/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-list-view.png and b/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-list-view.png differ diff --git a/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-filters.png b/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-filters.png index 4011d84c2..f3ab0cc1a 100644 Binary files a/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-filters.png and b/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-filters.png differ diff --git a/content/developer/tutorials/server_framework_101/04_relational_fields.rst b/content/developer/tutorials/server_framework_101/04_relational_fields.rst index 83e50beec..db385d94a 100644 --- a/content/developer/tutorials/server_framework_101/04_relational_fields.rst +++ b/content/developer/tutorials/server_framework_101/04_relational_fields.rst @@ -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 - ` - .. 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 + ` +.. 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 ` - .. 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 ` + 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. - .. 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 [...] @@ -515,6 +515,7 @@ of the property and the salesperson managing the property. + @@ -530,6 +531,30 @@ of the property and the salesperson managing the property. + + Country House + Chaussée de Namur 40 + Grand-Rosière-Hottomont + 1367 + + + + + Loft + Rue des Bourlottes 9 + Grand-Rosière-Hottomont + 1367 + + + + + Mixed use commercial building + Rue de Ramillies 1 + Grand-Rosière-Hottomont + 1367 + + + Bafien Carpink @@ -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 [...] + [...] + [...] + - .. 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 ` - .. 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 ` + 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 ` - .. 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 ` + 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. [...] diff --git a/content/developer/tutorials/server_framework_101/05_business_logic.rst b/content/developer/tutorials/server_framework_101/05_business_logic.rst deleted file mode 100644 index 79255bfb2..000000000 --- a/content/developer/tutorials/server_framework_101/05_business_logic.rst +++ /dev/null @@ -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 diff --git a/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst b/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst new file mode 100644 index 000000000..22e8b05fa --- /dev/null +++ b/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst @@ -0,0 +1,1335 @@ +=========================== +Chapter 5: Connect the dots +=========================== + +In this chapter, we'll add business logic to the models to automate the processes of our application +and turn it into a dynamic and useful tool. This will involve defining actions, constraints, +automatic computations, and other model methods. + +.. _tutorials/server_framework_101/computed_fields: + +Automate field computations +=========================== + +So far, we have built an object model capable of handling the essential data for a real estate +business. However, requiring users to manually set every field value can be inconvenient, especially +when some values are derived from other fields. Fortunately, the server framework provides the +ability to define **computed fields**. + +Computed fields are a special type of field whose value are derived through programmatic computation +rather than stored directly in the database. The server computes these values on the fly whenever +the field is accessed. This makes computed fields highly convenient for tasks such as displaying +calculation results in views, automating repetitive processes, or assisting users during data entry. + +In Odoo, computed fields are implemented by defining a Python method and linking it to a field +declaration using the `compute` argument. This method operates on a **recordset** :dfn:`a collection +of records from the same model` accessible through the `self` argument. The method must iterate over +the records to compute and set the field's value for each one. Additionally, compute methods must be +decorated with the :code:`@api.depends()` decorator when they depend on other fields. This decorator +specifies the fields that trigger an automatic recomputation whenever their values change, ensuring +that computed fields remain consistent. + +.. example:: + In the following example, the computed `margin`, `is_profitable` and `breadcrumb` fields are + added to the `product` model. + + .. code-block:: python + + from odoo import api, fields, models + + + class Product(models.Model): + _name = 'product' + + name = fields.Char(string="Name", required=True) + price = fields.Float(string="Sales Price", required=True) + cost = fields.Float(string="Manufacturing Cost") + margin = fields.Float(string="Profit Margin", compute='_compute_margin') + is_profitable = fields.Boolean(string="Profitable", compute='_compute_is_profitable') + category_id = fields.Many2one( + string="Category", comodel_name='product.category', ondelete='restrict', required=True + ) + breadcrumb = fields.Char( + string="Breadcrumb", + help="The path to the product. For example: 'Home Decor / Coffee table'", + compute='_compute_breadcrumb', + ) + + @api.depends('price', 'cost') + def _compute_margin(self): + for product in self: + product.margin = product.price - product.cost + + @api.depends('margin') + def _compute_is_profitable(self): + for product in self: + product.is_profitable = product.margin > 0 + + @api.depends('name', 'category_id.name') + def _compute_breadcrumb(self): + for product in self: + category = product.category_id + product.breadcrumb = f"{category.name} / {product.name}" + + .. note:: + - Compute methods are referenced using their names as strings in the `compute` field argument, + rather than directly linking the method object. This allows placing them after the field + declaration. + - Model methods should be private :dfn:`prefixed with an underscore` to keep them hidden from + the :doc:`external API <../../reference/external_api>`. + - Numeric field values default to `0` when not explicitly set. + - A compute method can depend on another computed field. + - Field values for related models can be accessed via their `Many2one`, `One2many`, or + `Many2many` field. + - Variables used for relational field values are typically not suffixed with `_id` or `_ids`. + While the field itself represents the stored ID(s), the variable holds the corresponding + recordset in memory. + +.. seealso:: + - :ref:`Reference documentation for computed fields ` + - :ref:`Reference documentation for recordsets ` + - Reference documentation for the :meth:`@api.depends() ` decorator + - :ref:`Coding guidelines on naming and ordering the members of model classes + ` + +Our real estate models could benefit from several computed fields to automate common calculations. +Let's implement them. + +.. exercise:: + Add the following fields to the corresponding models and relevant views: + + - :guilabel:`Total Area` (`real.estate.property`): The sum of the floor and garden areas. + - :guilabel:`Best Offer` (`real.estate.property`): The maximum amount of all offers. + - :guilabel:`Expiry Date` (`real.estate.offer`): The start date offset by the validity period. + + .. tip:: + - Use the :meth:`mapped ` model method to extract a recordset's + field values into a list. + - Import the `odoo.tools.date_utils` package to simplify operations on `Date` fields. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 1,9,14,17-28 + + from odoo import api, fields, models + + + class RealEstateProperty(models.Model): + [...] + garden_area = fields.Integer( + string="Garden Area", help="The garden area excluding the building." + ) + total_area = fields.Integer(string="Total Area", compute='_compute_total_area') + [...] + offer_ids = fields.One2many( + string="Offers", comodel_name='real.estate.offer', inverse_name='property_id' + ) + best_offer_amount = fields.Float(string="Best Offer", compute='_compute_best_offer_amount') + tag_ids = fields.Many2many(string="Tags", comodel_name='real.estate.tag') + + @api.depends('floor_area', 'garden_area') + def _compute_total_area(self): + for property in self: + property.total_area = property.floor_area + property.garden_area + + @api.depends('offer_ids.amount') + def _compute_best_offer_amount(self): + for property in self: + if property.offer_ids: + property.best_offer_amount = max(property.offer_ids.mapped('amount')) + else: + property.best_offer_amount = 0 + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + :emphasize-lines: 5,15,22 + + + [...] + + [...] + + + [...] + + + + [...] + + + + + + + + + [...] + + + [...] + + [...] + + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 1-2,10,13-16 + + from odoo import api, fields, models + from odoo.tools import date_utils + + + class RealEstateOffer(models.Model): + [...] + validity = fields.Integer( + string="Validity", help="The number of days before the offer expires.", default=7 + ) + expiry_date = fields.Date(string="Expiry Date", compute='_compute_expiry_date') + [...] + + @api.depends('date', 'validity') + def _compute_expiry_date(self): + for offer in self: + offer.expiry_date = date_utils.add(offer.date, days=offer.validity) + + .. code-block:: xml + :caption: `real_estate_offer_views.xml` + :emphasize-lines: 5,16 + + + [...] + + [...] + + + + [...] + + + + [...] + + [...] + + + + [...] + + +.. _tutorials/server_framework_101/inverse_methods: + +Make computed fields editable +----------------------------- + +You might have noticed that computed fields are read-only by default. This is expected since their +values are typically determined programmatically rather than set manually by users. However, this +behavior can be limiting when users need to adjust the computed value themselves. **Inverse +methods** address this limitation by allowing edits to computed fields and propagating the changes +back to their dependent fields. + +To make a computed field editable, a Python method must be defined and linked to the field +declaration using the `inverse` argument. This method specifies how updates to the computed field +should be applied to its dependencies. + +.. example:: + In the following example, an inverse method is added to the `margin` field. + + .. code-block:: python + + margin = fields.Float( + string="Profit Margin", compute='_compute_margin', inverse='_inverse_margin' + ) + + def _inverse_margin(self): + for product in self: + # As the cost is fixed, the sales price is increased to match the desired margin. + product.price = product.cost + product.margin + +Now that we have seen how inverse methods make computed fields editable, let's put this concept in +practice. + +.. exercise:: + Make the :guilabel:`Expiry Date` field editable on real estate offers. + + .. tip:: + You'll need to save the property form view to trigger the computation. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 1-3,6-8 + + expiry_date = fields.Date( + string="Expiry Date", compute='_compute_expiry_date', inverse='_inverse_expiry_date' + ) + [...] + + def _inverse_expiry_date(self): + for offer in self: + offer.validity = date_utils.relativedelta(dt1=offer.expiry_date, dt2=offer.date).days + +.. _tutorials/server_framework_101/store_computed_fields: + +Store computed fields +--------------------- + +As computed fields are calculated on the fly, recalculating their values repeatedly can become +inefficient, especially when they are frequently accessed or used in models with large datasets. +Another consequence is that they cannot be used in search queries by default. **Stored computed +fields** address both these issues by saving their values in the database and automatically updating +them only when their dependent data changes. Storing a computed field also enables the database to +index the field's column, significantly improving query performance for large datasets. + +Computed fields can be stored in the database by including the `store=True` argument in their field +declaration. The :code:`@api.depends()` decorator ensures that computed fields remain consistent not +only in the cache, but also when they are stored in the database. + +However, storing computed fields should be carefully considered. Every update to a dependency +triggers a recomputation, which can significantly impact performance on production servers with a +large number of records. + +.. example:: + In the following example, the `margin` field is stored in the database. + + .. code-block:: python + + margin = fields.Float( + string="Profit Margin", compute='_compute_margin', inverse='_inverse_margin', store=True + ) + +To make our real estate app more efficient and scalable, we can store certain computed fields in the +database. Let’s store one for now and see how it translates into the database schema. + +.. exercise:: + #. Store the :guilabel:`Total Area` field in the database. + #. Use `psql` to check that the field is stored in the database. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 1 + + total_area = fields.Integer(string="Total Area", compute='_compute_total_area', store=True) + + .. code-block:: text + :caption: terminal + + $ psql -d tutorials + + tutorials=> \d real_estate_property + Table "public.real_estate_property" + Column | Type | Collation | Nullable | Default + -------------------+-----------------------------+-----------+----------+-------------------------------------------------- + [...] + total_area | integer | | | + +.. _tutorials/server_framework_101/search_methods: + +Search computed fields +---------------------- + +As mentioned before, computed fields cannot be used in search queries unless they are stored in the +database. This limitation arises because searches are performed at the database level, which is not +aware of the existence of non-stored computed fields. However, storing every field that we wish to +search on would be inefficient. **Search methods** provide a way to overcome this limitation. + +To enable searching on a computed field, a Python method must be defined and linked to the field +declaration using the `search` argument. This method receives the search query's `operator` and +`value` and should return a search domain that specifies how the query should filter records. The +domain must be constructed using stored fields only. + +.. example:: + In the following example, a search method is added to allow searching on the `is_profitable` + field. + + .. code-block:: python + + is_profitable = fields.Boolean( + string="Profitable", compute='_compute_is_profitable', search='_search_is_profitable' + ) + + def _search_is_profitable(self, operator, value): + if (operator == '=' and value is True) or (operator == '!=' and value is False): + return [('margin', '>', 0)] + elif (operator == '=' and value is False) or (operator == '!=' and value is True): + return [('margin', '<=', 0)] + else: + raise NotImplementedError() + + .. note:: + - Search methods return a search domain that matches the computation of the searched field. + - It is not required to implement all search operators. + +Our real estate app would be more powerful if we could add a set of search filters based on computed +fields to the property views. Let’s leverage search methods to achieve this. + +.. exercise:: + Add the following search filters to the real estate property views: + + - :guilabel:`Stalled`: The property is past its availability date. + - :guilabel:`Priority`: The property has an offer that expires in less than two days. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 2,8,13-15,18-55 + + from odoo import api, fields, models + from odoo.tools import date_utils + + + class RealEstateProperty(models.Model): + [...] + availability_date = fields.Date(string="Availability Date") + stalled = fields.Boolean(string="Stalled", compute='_compute_stalled', search='_search_stalled') + [...] + offer_ids = fields.One2many( + string="Offers", comodel_name='real.estate.offer', inverse_name='property_id' + ) + is_priority = fields.Boolean( + string="Priority", compute='_compute_is_priority', search='_search_is_priority' + ) + [...] + + @api.depends('availability_date') + def _compute_stalled(self): + for property in self: + property.stalled = property.availability_date < fields.Date.today() + + def _search_stalled(self, operator, value): + if (operator == '=' and value is True) or (operator == '!=' and value is False): + return [('availability_date', '<', fields.Date.today())] + elif (operator == '=' and value is False) or (operator == '!=' and value is True): + return [('availability_date', '>=', fields.Date.today())] + else: + raise NotImplementedError() + + @api.depends('offer_ids.expiry_date') + def _compute_is_priority(self): + for property in self: + is_priority = False + for offer in property.offer_ids: + if offer.expiry_date <= fields.Date.today() + date_utils.relativedelta(days=2): + is_priority = True + break + property.is_priority = is_priority + + def _search_is_priority(self, operator, value): + if (operator == '=' and value is True) or (operator == '!=' and value is False): + return [( + 'offer_ids.expiry_date', + '<=', + fields.Date.today() + date_utils.relativedelta(days=2), + )] + elif (operator == '=' and value is False) or (operator == '!=' and value is True): + return [( + 'offer_ids.expiry_date', + '>', + fields.Date.today() + date_utils.relativedelta(days=2), + )] + else: + raise NotImplementedError() + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 5 + + expiry_date = fields.Date( + string="Expiry Date", + compute='_compute_expiry_date', + inverse='_inverse_expiry_date', + store=True, + ) + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + :emphasize-lines: 10-11,14 + + + [...] + + [...] + + + + + + + [...] + + [...] + + +.. _tutorials/server_framework_101/related_fields: + +Simplify related record access +------------------------------ + +While computed fields make it easier to derive values programmatically, there are cases where the +desired data already exists in related records. Manually computing such values would be redundant +and error-prone. **Related fields** solve this by dynamically fetching data from related records. As +a special case of computed fields, they simplify access to information without requiring explicit +computation. + +In practice, related fields are defined like regular fields, but with the `related` argument set to +the path of the related record's field. Related fields can also be stored with the `store=True` +argument, just like regular computed fields. + +.. example:: + In the following example, the related `category_name` field is derived from the `category_id` + field. + + .. code-block:: python + + category_name = fields.Char(string="Category Name", related='category_id.name') + +.. seealso:: + :ref:`Reference documentation for related fields ` + +In :doc:`04_relational_fields`, we introduced several relational fields. Retrieving information from +their related models often requires additional steps from the user, but we can use related fields to +simplify this process. + +.. exercise:: + #. Use a related field to display the phone number of buyers in the offer list view. + #. Use a related field to display the street of properties in form view. Allow editing the field + and searching by street without implementing a search method. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 2 + + buyer_id = fields.Many2one(string="Buyer", comodel_name='res.partner', required=True) + phone = fields.Char(string="Phone", related='buyer_id.phone') + + .. code-block:: xml + :caption: `real_estate_offer_views.xml` + :emphasize-lines: 6 + + + [...] + + [...] + + + [...] + + [...] + + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 2 + + address_id = fields.Many2one(string="Address", comodel_name='res.partner', required=True) + street = fields.Char(string="Street", related='address_id.street', readonly=False, store=True) + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + :emphasize-lines: 5,15 + + + [...] + + + + [...] + + [...] + + + + [...] + + [...] + + + [...] + + [...] + + +.. _tutorials/server_framework_101/onchanges: + +Provide real-time feedback +========================== + +**Onchange methods** are a feature of the server framework designed to respond to changes in field +values directly within the user interface. They are executed when a user modifies a field in a form +view, even before saving the record to the database. This allows for real-time updates of other +fields and provides immediate user feedback, such as blocking user errors, non-blocking warnings, or +suggestions. However, because onchange methods are only triggered by changes made in the UI, +specifically from a form view, they are best suited for assisting with data entry and providing +feedback, rather than implementing core business logic in a module. + +In Odoo, onchange methods are implemented as Python methods and linked to one or more fields using +the :code:`@api.onchange()` decorator. These methods are triggered when the specified fields' values +are altered. They operate on the in-memory representation of a single-record recordset received +through `self`. If field values are modified, the changes are automatically reflected in the UI. + +.. example:: + In the following example, onchange methods are implemented to: + + - unpublish products when all sellers are removed; + - warn the user if changing the sales price would result in a negative margin; + - raise a blocking user error if the category is changed after sales have been made. + + .. code-block:: python + + from odoo import _, api, fields, models + from odoo.exceptions import UserError + + + class Product(models.Model): + is_published = fields.Boolean(string="Published") + + @api.onchange('seller_ids') + def _onchange_seller_ids_unpublish_if_no_sellers(self): + if not self.seller_ids: + self.is_published = False + + @api.onchange('price') + def _onchange_price_warn_if_margin_is_negative(self): + if self.margin < 0: + return { + 'warning': { + 'title': _("Warning"), + 'message': _( + "The sales price was changed from %(before_price)s to %(new_price)s, which" + " would result in a negative margin. A sales price of minimum %(min_price)s" + " is recommended.", + before_price=self._origin.price, new_price=self.price, min_price=self.cost, + ), + } + } + + @api.onchange('category_id') + def _onchange_category_id_block_if_existing_sales(self): + existing_sales = self.env['sales.order'].search([('product_id', '=', self._origin.id)]) + if existing_sales: + raise UserError(_( + "You cannot change the category of a product that has already been sold; unpublish" + " it instead." + )) + + .. note:: + - It is recommended to give self-explanatory names to onchange methods as multiple onchange + methods can be defined for a single field. + - Onchange methods don't need to iterate over the records as `self` is always a recordset of + length 1. + - The :code:`_()` function from the `odoo` package marks display strings :dfn:`strings shown + to the user and denoted with double quotes` for translation. + - Regular string interpolation isn't possible withing the translation function. Instead, + values to interpolate are passed as either positional arguments when using the :code:`%s` + format, or as keyword arguments when using the :code:`%(name)s` format. + - The `_origin` model attribute refers to the original record before user modifications. + - The `env` model attribute is an object that allows access to other models and their classes. + - The `search` environment method can be used to query a model for records matching a given + search domain. + - In onchanges methods, the `id` attribute cannot be used to directly access the record's ID. + - Blocking user errors are raised as exceptions. + +.. seealso:: + - Reference documentation for the :meth:`@api.onchange() ` decorator + - :doc:`How-to guide on translations ` + - Reference documentation for the :class:`UserError ` exception + - :ref:`Reference documentation for the environment object ` + - Reference documentation for the :meth:`search ` method + +In our real estate app, data entry could be more intuitive and efficient. Let's use onchange methods +to automate updates and guide users as they edit data. + +.. exercise:: + #. Set the garden area to zero if :guilabel:`Garden` is unchecked. + #. Set :guilabel:`Garden` to checked if the garden area is set. + #. Display a non-blocking warning if the garden area is set to zero and :guilabel:`Garden` is + checked. + #. Prevent archiving a property that has **pending** offers. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 1-2, 9-40 + + from odoo import _, api, fields, models + from odoo.exceptions import UserError + from odoo.tools import date_utils + + + class RealEstateProperty(models.Model): + [...] + + @api.onchange('active') + def _onchange_active_block_if_existing_offers(self): + if not self.active: + existing_offers = self.env['real.estate.offer'].search( + [('property_id', '=', self._origin.id), ('state', '=', 'waiting')] + ) + if existing_offers: + raise UserError( + _("You cannot change the active state of a property that has pending offers.") + ) + + @api.onchange('has_garden') + def _onchange_has_garden_set_garden_area_to_zero_if_unchecked(self): + if not self.has_garden: + self.garden_area = 0 + + @api.onchange('garden_area') + def _onchange_garden_area_uncheck_garden_if_zero(self): + if self.garden_area and not self.has_garden: + self.has_garden = True + + @api.onchange('garden_area') + def _onchange_garden_area_display_warning_if_zero_and_checked(self): + if not self.garden_area and self.has_garden: + return { + 'warning': { + 'title': _("Warning"), + 'message': _( + "The garden area was set to zero, but the garden checkbox is checked." + ), + } + } + +.. _tutorials/server_framework_101/constraints: + +Enforce data integrity +====================== + +**Constraints** are rules that enforce data integrity by validating field values and relationships +between records. They ensure that the data stored in your application remains consistent and meets +business requirements, preventing invalid values, duplicate entries, or inconsistent relationships +from being saved to the database. + +In Odoo, constraints can be implemented at two different levels: directly in the database schema +using **SQL constraints**, or in the model's logic using **constraint methods**. Each type has its +own advantages and use cases, allowing developers to choose the most appropriate validation method +based on their specific needs. Unlike onchange methods, constraints are enforced when saving records +to the database, not when they are altered in the UI. + +.. _tutorials/server_framework_101/sql_constraints: + +SQL constraints +--------------- + +SQL constraints are database-level rules that are enforced directly by PostgreSQL when records are +created or modified. They are highly efficient in terms of performance, but they cannot handle +complex logic or access individual records. As a result, they are best suited for straightforward +use cases, such as ensuring that a field value is unique or falls within a specific range. + +.. todo: Update for https://github.com/odoo/odoo/pull/175783 in 18.1 + +SQL constraints are defined in the model using the `_sql_constraints` class attribute. This +attribute contains a list of tuples, with each tuple specifying the constraint's name, the SQL +expression to validate, and the error message to display if the constraint is violated. + +.. example:: + The following example defines SQL constraints to enforce a positive product sales price and + ensure that product category names remain unique. + + .. code-block:: python + + class Product(models.Model): + _name = 'product' + _description = "Storable Product" + _sql_constraints = [ + ('positive_price', 'CHECK (price >= 0)', "The sales price must be positive.") + ] + + class ProductCategory(models.Model): + _name = 'product.category' + _description = "Product Category" + _sql_constraints = [ + ('unique_name', 'unique(name)', "A category name must be unique.") + ] + +.. seealso:: + - Reference documentation for the :attr:`_sql_constraints + ` class attribute + - `Reference documentation for PostgreSQL's constraints + `_ + +.. exercise:: + #. Enforce that the selling price of a property and the amount of an offer are strictly positive. + #. Ensure that property types and tag names are unique. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 4-8 + + class RealEstateProperty(models.Model): + _name = 'real.estate.property' + _description = "Real Estate Property" + _sql_constraints = [( + 'positive_price', + 'CHECK (selling_price > 0)', + "The selling price must be strictly positive.", + )] + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 4-6 + + class RealEstateOffer(models.Model): + _name = 'real.estate.offer' + _description = "Real Estate Offer" + _sql_constraints = [ + ('positive_amount', 'CHECK (amount > 0)', "The amount must be strictly positive.") + ] + + .. code-block:: python + :caption: `real_estate_property_type.py` + :emphasize-lines: 4-6 + + class RealEstatePropertyType(models.Model): + _name = 'real.estate.property.type' + _description = "Real Estate Property Type" + _sql_constraints = [ + ('unique_name', 'unique(name)', "A property type name must be unique.") + ] + + .. code-block:: python + :caption: `real_estate_tag.py` + :emphasize-lines: 4-6 + + class RealEstateTag(models.Model): + _name = 'real.estate.tag' + _description = "Real Estate Tag" + _sql_constraints = [ + ('unique_name', 'unique(name)', "A property tag name must be unique.") + ] + +.. _tutorials/server_framework_101/constraint_methods: + +Constraint methods +------------------ + +Constraint methods are record-level rules implemented through Python methods defined on the model. +Unlike SQL constraints, they allow for flexible and context-aware validations based on business +logic. However, they come with a higher performance cost compared to SQL constraints, as they are +evaluated server-side on recordsets. Use cases include ensuring that certain fields align with +specific conditions or that multiple fields work together in a valid combination. + +Constraint methods are defined in the model as methods decorated with :code:`@api.constrains()`, +which specifies the fields that trigger the validation when they are altered. Upon activation, they +perform custom validation and raise blocking validation errors if the constraint is violated. + +.. example:: + The following example shows how a constraint method can be defined to ensure that the price of a + product is higher than the minimum price of its category. + + .. code-block:: python + + from odoo import _, api, fields, models + from odoo.exceptions import UserError, ValidationError + + + class ProductCategory(models.Model): + _name = 'product.category' + + min_price = fields.Float(string="Minimum Sales Price", required=True) + + + class Product(models.Model): + _name = 'product.product' + + @api.constrains('price', 'category_id') + def _check_price_is_higher_than_category_min_price(self): + for product in self: + if product.price < product.category_id.min_price: + raise ValidationError( + _("The price must be higher than %s.", product.category_id.min_price) + ) + +.. seealso:: + - Reference documentation for the :meth:`@api.constrains ` decorator + - Reference documentation for the :class:`ValidationError ` + exception + +.. exercise:: + #. Ensure that a property's availability date is no more than one year from today. + #. Ensure that a buyer's new offer for a property has a higher amount than their previous offers + for the same property. + #. Ensure that only one offer can be accepted for a property at a time. + + .. tip:: + - Use the :meth:`filtered ` method to filter records of a + recordset based on a predicate. + - Recordsets can be counted using the `len` function. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 2,9-13 + + from odoo import _, api, fields, models + from odoo.exceptions import UserError, ValidationError + from odoo.tools import date_utils + + + class RealEstateProperty(models.Model): + [...] + + @api.constrains('availability_date') + def _check_availability_date_under_1_year(self): + for property in self.filtered('availability_date'): + if property.availability_date > fields.Date.today() + date_utils.relativedelta(years=1): + raise ValidationError(_("The availability date must be in less than one year.")) + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 1-2,9-22 + + from odoo import _, api, fields, models + from odoo.exceptions import ValidationError + from odoo.tools import date_utils + + + class RealEstateOffer(models.Model): + [...] + + @api.constrains('amount') + def _check_amount_higher_than_previous_offers(self): + for offer in self: + same_buyer_offers = offer.property_id.offer_ids.filtered( + lambda o: o.buyer_id == offer.buyer_id + ) + if offer.amount < max(same_buyer_offers.mapped('amount')): + raise ValidationError(_( + "The amount of the new offer must be higher than the amount of the previous " + "offers." + )) + + @api.constrains('state') + def _check_state_is_accepted_for_only_one_offer(self): + for offer in self.filtered(lambda o: o.state == 'accepted'): + if len(offer.property_id.offer_ids.filtered(lambda o: o.state == 'accepted')) > 1: + raise ValidationError(_("Only one offer can be accepted for a property.")) + +.. _tutorials/server_framework_101/defaults: + +Set default field values +======================== + +When creating new records, pre-filling certain fields with default values can simplify data entry +and reduce the likelihood of errors. Defaults are particularly useful when values are derived from +the system or context, such as the current date, time, or logged-in user. + +Fields can be assigned a default value by including the `default` argument in their declaration. +This argument can be set to a static value or dynamically generated using a callable function, such +as a model method reference or a lambda function. In both cases, the `self` argument provides access +to the environment but does not represent the current record, as no record exists yet during the +creation process. + +.. example:: + In the following example, a default value is assigned to the `price` and `category_id` fields. + + .. code-block:: python + + price = fields.Float(string="Sales Price", required=True, default=100) + category_id = fields.Many2one( + string="Category", + comodel_name='product.category', + ondelete='restrict', + required=True, + default=lambda self: self.env.ref('product.category_apparel'), + ) + + .. note:: + The `ref` environment method can be used to retrieve a record by its XML ID, similar to how + it's done in data files. + +.. seealso:: + Reference documentation for the :meth:`ref ` method. + +To make our real estate app more user-friendly, we can help with data entry by pre-filling key +fields with default values. + +.. exercise:: + #. Set the default date of offers to today. + #. Set the current user as the default salesperson for new properties. + #. Set the default availability date of properties to two months from today. + #. Assign a random default color to property tags. + + .. tip:: + - Ensure you pass callable function references as default values, and not the result of + function calls, to avoid setting fixed defaults. + - The current user can be accessed through the `user` environment property. + - Color codes range from 1 to 11. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 1 + + date = fields.Date(string="Date", required=True, default=fields.Date.today) + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 1-4,6-8 + + availability_date = fields.Date( + string="Availability Date", + default=lambda self: date_utils.add(fields.Date.today(), months=2) + ) + [...] + salesperson_id = fields.Many2one( + string="Salesperson", comodel_name='res.users', default=lambda self: self.env.user + ) + + .. code-block:: python + :caption: `real_estate_tag.py` + :emphasize-lines: 1,9-10,13 + + import random + + from odoo import fields, models + + + class RealEstateTag(models.Model): + [...] + + def _default_color(self): + return random.randint(1, 11) + + name = fields.Char(string="Label", required=True) + color = fields.Integer(string="Color", default=_default_color) + +.. _tutorials/server_framework_101/business_workflows: + +Trigger business workflows +========================== + +.. _tutorials/server_framework_101/crud_methods: + +CRUD methods +------------ + +CRUD :dfn:`Create, Read, Update, Delete` operations are the foundation of a model's business logic. +They define how data is stored, retrieved, and modified. + +In Odoo, these operations are handled through predefined model methods, which can be overridden to +implement additional business logic: + +- `create`: Called on a model class (`env['model_name']`) with a dictionary of field values (or a + list of dictionaries) as an argument. It returns the newly created record(s). +- `write`: Called on an existing recordset with a dictionary of field values to update all records + in the set. +- `unlink`: Called on a recordset to delete the records permanently. + +Unlike the other CRUD operations, reading data does not require a method call; record fields can be +accessed directly using :code:`record.field`. + +.. example:: + In the following example, a product is automatically archived if its category is inactive. + + .. code-block:: python + + class Product(models.Model): + _name = 'product' + + active = fields.Boolean(default=True) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if category_id := vals.get('category_id'): # A category is specified in the values. + category = self.env['product.category'].browse(category_id).exists() + if category and not category.active: # The category exists and is archived. + vals['active'] = False # Create the product in the inactive state. + return super().create(vals_list) + + def write(self, vals): + if new_category_id := vals.get('category_id'): # The category of the product is updated. + new_category = self.env['product.category'].browse(new_category_id).exists() + if new_category and not new_category.active: # The category exists and is archived. + vals['active'] = False # Archive the product. + return super().write(vals) + + class ProductCategory(models.Model): + _name = 'product.category' + + active = fields.Boolean(default=True) + + def write(self, vals): + if self.active and vals.get('active') is False: # The category is being archived. + self.product_ids.active = False # Archive all products of the category. + return super().write(vals) + + .. note:: + - Both the `create` and the `write` methods of `product` are overridden to ensure that the + behavior is enforced consistently. The `write` method override is necessary because it is + not called during record creation. + - The `create` method must support batch processing, which is why it is decorated with + :code:`api.model_create_multi` and processes a list of dictionaries (`vals_list`). + - The `browse` model method can be used to retrieve a record by its ID. Not to be confused + with the `ref` method. + - The `browse` method always returns a recordset, even if no record exists. Therefore, + chaining `exists` ensures that only existing records are considered before reading field + values. + - A field can be updated directly on a recordset with :code:`recordset.field = value`, which + is equivalent to calling :code:`recordset.write({'field': value})`. + +.. seealso:: + - Reference documentation for the :meth:`@api.model_create_multi ` + decorator. + - Reference documentation for the :meth:`create ` method. + - Reference documentation for the :meth:`write ` method. + - Reference documentation for the :meth:`unlink ` method. + - Reference documentation for the :meth:`browse ` method. + - Reference documentation for the :meth:`exists ` method. + +.. exercise:: + #. Move a property to the :guilabel:`Offer Received` state when its first offer is received. + #. Move a property back to the :guilabel:`New` state when all its offers are deleted. + #. Make the :guilabel:`Address` field optional and automatically assign an address when creating + a new property. + #. If no address is set, automatically assign one when the street is updated. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 3-18 + + [...] + + @api.model_create_multi + def create(self, vals_list): + offers = super().create(vals_list) + for offer in offers: + if offer.property_id.state == 'new': + offer.property_id.state = 'offer_received' + return offers + + def unlink(self): + for offer in self: + property_offers = offer.property_id.offer_ids + if ( + offer.property_id.state in ('offer_received', 'under_option') + and not (property_offers - self) # All the property's offers are being deleted. + ): + offer.property_id.state = 'new' + return super().unlink() + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 1,5-27 + + address_id = fields.Many2one(string="Address", comodel_name='res.partner') + + [...] + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if not vals.get('address_id'): # No address is provided at creation time. + # Create and assign a new one based on the property name. + address = self.env['res.partner'].create({ + 'name': vals.get('name'), + }) + vals['address_id'] = address.id + return super().create(vals_list) + + def write(self, vals): + res = super().write(vals) + if vals.get('street'): # The street has been updated. + for property in self: + if not property.address_id: # The property has no address record. + # Create and assign a new one based on the property name and the street. + address = self.env['res.partner'].create({ + 'name': property.name, + 'street': vals['street'], + }) + property.address_id = address.id + return res + +.. _tutorials/server_framework_101/xml_actions: + +XML actions +----------- + +**Action buttons** allow users to trigger workflows directly from the user interface. The simplest +type of action button is **action**. These buttons are linked to actions defined in XML and are +typically used to open specific views or trigger server actions. These buttons allow developers to +link workflows to the UI without needing to write Python code, making them ideal for simple, +preconfigured tasks. + +We have already seen how to :ref:`link menu items to XML-defined window actions +`. To link a **button** to an XML-defined +action, a `button` element must be added to the view, with its `type` attribute set to `action`. The +`name` attribute should reference the XML ID of the action to be executed, following the format +`%(XML_ID)d`. + +.. example:: + In the following example, a button is added to the product form view to display all products in + the same category. + + .. code-block:: xml + +
+ +
+
+
+
+ + .. note:: + - The button is placed at the top of the form view by using a button container (`button_box`). + - The `context` attribute is used to: + + - Filter the products to display only those in the same category as the current product. + - Prevent users from creating or editing products when browsing them through the button. + +.. seealso:: + Reference documentation for :ref:`button containers + `. + +.. exercise:: + Replace the property form view's :guilabel:`Offers` notebook page with a **stat button**. This + button should: + + - Be placed at the top of the property form view. + - Display the total number of offers for the property. + - Use a relevant icon. + - Allow users to browse offers in list and form views. + + .. tip:: + - Rely on the reference documentation for :ref:`action buttons + ` in form views. + - Find icon codes (`fa-`) in the `Font Awesome v4 catalog + `_. + - Ensure that your count computations :ref:`scale with the number of records to process + `. + - Assign the `default_` context key to a button to define default values when creating + new records opened through that button. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 4,8-15 + + offer_ids = fields.One2many( + string="Offers", comodel_name='real.estate.offer', inverse_name='property_id' + ) + offer_count = fields.Integer(string="Offer Count", compute='_compute_offer_count') + + [...] + + @api.depends('offer_ids') + def _compute_offer_count(self): + offer_data = self.env['real.estate.offer']._read_group( + [('property_id', 'in', self.ids)], groupby=['property_id'], aggregates=['__count'], + ) + property_data = {property.id: count for property, count in offer_data} + for property in self: + property.offer_count = property_data.get(property.id, 0) + + .. code-block:: xml + :caption: `real_estate_offer_views.xml` + :emphasize-lines: 1-10 + + + Offers + real.estate.offer + list,form + +

+ Create a new offer. +

+
+
+ + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 1 + + 'views/real_estate_property_views.xml', # Depends on `real_estate_offer_views.xml`. + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + :emphasize-lines: 2-12 + + +
+ +
+ [...] + +.. _tutorials/server_framework_101/model_actions: + +Model actions +------------- + +Another, more versatile type of action button is **object**. These buttons are linked to model +methods that execute custom business logic. These methods enable more complex workflows, such as +processing the current records, configuring client actions depending on these records, or +integrating with external systems. + +To link a button to a model-defined action, its `type` attribute must be set to `object`, and its +`name` attribute must be set to the name of the model method to call when the button is clicked. The +method receives the current recordset through `self` and should return a dictionary acting as an +action descriptor. + +.. example:: + In the following example, + + .. note:: + - Action methods should be public :dfn:`not prefixed with an underscore` to make them callable + by the client. Such methods should always return something as they are automatically part of + the :doc:`external API <../../reference/external_api>`. + +.. exercise:: + #. tmp ... in the header. + + .. tip:: + - Rely on the reference documentation for :ref:`headers + ` in form views. + +.. todo: accept/refuse offer buttons -> auto refuse others when accepting (write) +.. todo: multi-checkbox refuse offers in bulk +.. todo: "assign myself as salesperson" action + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 1 + + [...] + +---- + +.. todo: add incentive for chapter 6 diff --git a/content/developer/tutorials/server_framework_101/06_security.rst b/content/developer/tutorials/server_framework_101/06_security.rst index ced1d4f2d..321bf6ad1 100644 --- a/content/developer/tutorials/server_framework_101/06_security.rst +++ b/content/developer/tutorials/server_framework_101/06_security.rst @@ -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 diff --git a/content/developer/tutorials/server_framework_101/07_advanced_views.rst b/content/developer/tutorials/server_framework_101/07_advanced_views.rst index 1e2021cac..93d6860dd 100644 --- a/content/developer/tutorials/server_framework_101/07_advanced_views.rst +++ b/content/developer/tutorials/server_framework_101/07_advanced_views.rst @@ -4,11 +4,17 @@ Chapter 7: Advanced views tmp -.. todo:: invisible, required, readonly modifiers -.. todo:: introduce bootstrap -.. todo:: widgets; eg, -.. todo:: add Gantt view of properties availability -.. todo:: add Kanban view of properties +.. todo: invisible, required, readonly modifiers +.. todo: introduce bootstrap +.. todo: widgets; eg, + (/!\ 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 + ---- diff --git a/content/developer/tutorials/server_framework_101/08_inheritance.rst b/content/developer/tutorials/server_framework_101/08_inheritance.rst index bc78cf63c..e15f1def5 100644 --- a/content/developer/tutorials/server_framework_101/08_inheritance.rst +++ b/content/developer/tutorials/server_framework_101/08_inheritance.rst @@ -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 ---- diff --git a/content/developer/tutorials/setup_guide.rst b/content/developer/tutorials/setup_guide.rst index 0d2b59082..c8943661d 100644 --- a/content/developer/tutorials/setup_guide.rst +++ b/content/developer/tutorials/setup_guide.rst @@ -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 `_ 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