From 32cba451c1a73569cb011f059aa7b7b12bbeb004 Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Mon, 6 May 2024 17:52:16 +0200 Subject: [PATCH 1/5] [IMP] tutorials/server_framework_101: rewrite the tutorial The Server Framework 101 (formerly Getting Started) is generally seen as an interesting and rewarding tutorial, but also somewhat outdated and too limited for beginners in Odoo development, as it fails to introduce key concepts of the server framework (e.g., controllers, tests, etc.). The instructions are also too directive for the reader to try and search by themselves and learn from it. With this commit, all of the content of the tutorial is rewritten while keeping the objective of building a real estate module. The setup guide for tutorials is also improved to ensure smoother onboarding for Odoo employees and community members alike. task-3802536 --- content/administration/on_premise/source.rst | 4 +- content/contributing/development.rst | 5 +- .../development/coding_guidelines.rst | 5 + .../development/git_guidelines.rst | 10 + content/developer/reference/backend/data.rst | 4 + content/developer/reference/backend/orm.rst | 8 + .../user_interface/view_architectures.rst | 40 +- .../tutorials/define_module_data.rst | 2 +- content/developer/tutorials/pdf_reports.rst | 4 +- .../tutorials/restrict_data_access.rst | 4 +- .../tutorials/server_framework_101.rst | 51 +- .../01_architecture_overview.rst | 72 ++ .../three-tier-architecture.svg} | 0 .../02_lay_the_foundations.rst | 573 ++++++++++ .../03_build_user_interface.rst | 709 +++++++++++++ .../custom-form-view.png | Bin 0 -> 26402 bytes .../custom-list-view.png | Bin 0 -> 16697 bytes .../custom-search-view-fields.png | Bin 0 -> 7594 bytes .../custom-search-view-filters.png | Bin 0 -> 9442 bytes .../04_relational_fields.rst | 996 ++++++++++++++++++ .../05_business_logic.rst | 35 + .../server_framework_101/06_security.rst | 9 + .../07_advanced_views.rst | 15 + .../server_framework_101/08_inheritance.rst | 11 + .../server_framework_101/09_portal.rst | 9 + .../server_framework_101/10_unit_testing.rst | 9 + .../tutorials/server_framework_101_legacy.rst | 48 + .../01_architecture.rst | 8 +- .../01_architecture/three_tier.svg | 253 +++++ .../02_newapp.rst | 6 + .../02_newapp/app_in_list.png | Bin .../02_newapp/overview_form_view_01.png | Bin .../02_newapp/overview_form_view_02.png | Bin .../02_newapp/overview_list_view_01.png | Bin .../03_basicmodel.rst | 6 + .../04_securityintro.rst | 6 + .../05_firstui.rst | 6 + .../05_firstui/attribute_and_default.gif | Bin .../05_firstui/estate_form_default.png | Bin .../05_firstui/estate_menu_action.png | Bin .../05_firstui/estate_menu_root.png | Bin .../05_firstui/inactive.gif | Bin .../05_firstui/menu_01.png | Bin .../05_firstui/menu_02.png | Bin .../06_basicviews.rst | 6 + .../06_basicviews/form.png | Bin .../06_basicviews/list.png | Bin .../06_basicviews/search_01.png | Bin .../06_basicviews/search_02.png | Bin .../06_basicviews/search_03.png | Bin .../07_relations.rst | 6 + .../07_relations/property_many2many.png | Bin .../07_relations/property_many2one.png | Bin .../07_relations/property_offer.png | Bin .../07_relations/property_tag.png | Bin .../07_relations/property_type.png | Bin .../08_compute_onchange.rst | 8 +- .../08_compute_onchange/compute.gif | Bin .../08_compute_onchange/compute_inverse.gif | Bin .../08_compute_onchange/onchange.gif | Bin .../09_actions.rst | 6 + .../09_actions/offer_01.gif | Bin .../09_actions/offer_02.gif | Bin .../09_actions/property.gif | Bin .../10_constraints.rst | 6 + .../10_constraints/python.gif | Bin .../10_constraints/sql_01.gif | Bin .../10_constraints/sql_02.gif | Bin .../11_sprinkles.rst | 6 + .../11_sprinkles/decoration.png | Bin .../11_sprinkles/editable_list.gif | Bin .../11_sprinkles/form.gif | Bin .../11_sprinkles/inline_view.png | Bin .../11_sprinkles/search.gif | Bin .../11_sprinkles/stat_button.gif | Bin .../11_sprinkles/widget.png | Bin .../12_inheritance.rst | 6 + .../12_inheritance/create.gif | Bin .../12_inheritance/inheritance_methods.png | Bin .../12_inheritance/unlink.gif | Bin .../12_inheritance/users.png | Bin .../13_other_module.rst | 8 +- .../13_other_module/create_inv.gif | Bin .../14_qwebintro.rst | 6 + .../14_qwebintro/kanban.png | Bin .../15_final_word.rst | 6 + content/developer/tutorials/setup_guide.rst | 176 ++-- redirects/18.0.txt | 18 + 88 files changed, 3025 insertions(+), 141 deletions(-) create mode 100644 content/developer/tutorials/server_framework_101/01_architecture_overview.rst rename content/developer/tutorials/server_framework_101/{01_architecture/three_tier.svg => 01_architecture_overview/three-tier-architecture.svg} (100%) create mode 100644 content/developer/tutorials/server_framework_101/02_lay_the_foundations.rst create mode 100644 content/developer/tutorials/server_framework_101/03_build_user_interface.rst create mode 100644 content/developer/tutorials/server_framework_101/03_build_user_interface/custom-form-view.png create mode 100644 content/developer/tutorials/server_framework_101/03_build_user_interface/custom-list-view.png create mode 100644 content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-fields.png create mode 100644 content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-filters.png create mode 100644 content/developer/tutorials/server_framework_101/04_relational_fields.rst create mode 100644 content/developer/tutorials/server_framework_101/05_business_logic.rst create mode 100644 content/developer/tutorials/server_framework_101/06_security.rst create mode 100644 content/developer/tutorials/server_framework_101/07_advanced_views.rst create mode 100644 content/developer/tutorials/server_framework_101/08_inheritance.rst create mode 100644 content/developer/tutorials/server_framework_101/09_portal.rst create mode 100644 content/developer/tutorials/server_framework_101/10_unit_testing.rst create mode 100644 content/developer/tutorials/server_framework_101_legacy.rst rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/01_architecture.rst (95%) create mode 100644 content/developer/tutorials/server_framework_101_legacy/01_architecture/three_tier.svg rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/02_newapp.rst (95%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/02_newapp/app_in_list.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/02_newapp/overview_form_view_01.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/02_newapp/overview_form_view_02.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/02_newapp/overview_list_view_01.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/03_basicmodel.rst (98%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/04_securityintro.rst (96%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/05_firstui.rst (98%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/05_firstui/attribute_and_default.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/05_firstui/estate_form_default.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/05_firstui/estate_menu_action.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/05_firstui/estate_menu_root.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/05_firstui/inactive.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/05_firstui/menu_01.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/05_firstui/menu_02.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/06_basicviews.rst (97%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/06_basicviews/form.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/06_basicviews/list.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/06_basicviews/search_01.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/06_basicviews/search_02.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/06_basicviews/search_03.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/07_relations.rst (98%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/07_relations/property_many2many.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/07_relations/property_many2one.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/07_relations/property_offer.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/07_relations/property_tag.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/07_relations/property_type.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/08_compute_onchange.rst (97%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/08_compute_onchange/compute.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/08_compute_onchange/compute_inverse.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/08_compute_onchange/onchange.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/09_actions.rst (96%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/09_actions/offer_01.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/09_actions/offer_02.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/09_actions/property.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/10_constraints.rst (96%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/10_constraints/python.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/10_constraints/sql_01.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/10_constraints/sql_02.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/11_sprinkles.rst (99%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/11_sprinkles/decoration.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/11_sprinkles/editable_list.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/11_sprinkles/form.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/11_sprinkles/inline_view.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/11_sprinkles/search.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/11_sprinkles/stat_button.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/11_sprinkles/widget.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/12_inheritance.rst (98%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/12_inheritance/create.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/12_inheritance/inheritance_methods.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/12_inheritance/unlink.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/12_inheritance/users.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/13_other_module.rst (96%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/13_other_module/create_inv.gif (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/14_qwebintro.rst (96%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/14_qwebintro/kanban.png (100%) rename content/developer/tutorials/{server_framework_101 => server_framework_101_legacy}/15_final_word.rst (86%) diff --git a/content/administration/on_premise/source.rst b/content/administration/on_premise/source.rst index 3b43fc64a..25285e318 100644 --- a/content/administration/on_premise/source.rst +++ b/content/administration/on_premise/source.rst @@ -44,8 +44,8 @@ Git basic knowledge of Git commands to proceed. To clone a Git repository, choose between cloning with HTTPS or SSH. In most cases, the best option -is HTTPS. However, choose SSH to contribute to Odoo source code or when following the :doc:`Getting -Started developer tutorial `. +is HTTPS. However, choose SSH to contribute to Odoo source code or when following the +:doc:`/developer/tutorials/server_framework_101` tutorial. .. tabs:: diff --git a/content/contributing/development.rst b/content/contributing/development.rst index 3bfc16993..b93b426d5 100644 --- a/content/contributing/development.rst +++ b/content/contributing/development.rst @@ -161,7 +161,10 @@ navigate to the directory where you installed Odoo from sources and follow the g #. Select **/odoo** or **/enterprise** for the head repository. Replace `` with the name of the GitHub account on which you created the fork or by **odoo-dev** if you work at Odoo. - #. Review your changes and click on the :guilabel:`Create pull request` button. + #. Click on :guilabel:`Create pull request` to create the :abbr:`PR (Pull Request)` and + automatically request a review from the code maintainers. If you wish to double-check your + changes first, or if you work at Odoo and follow an internal process for reviews, click on + :guilabel:`Create draft pull request`. #. Tick the :guilabel:`Allow edits from maintainer` checkbox. Skip this step if you work at Odoo. #. Complete the description and click on the :guilabel:`Create pull request` button again. diff --git a/content/contributing/development/coding_guidelines.rst b/content/contributing/development/coding_guidelines.rst index 08567eb57..32b7aa9f2 100644 --- a/content/contributing/development/coding_guidelines.rst +++ b/content/contributing/development/coding_guidelines.rst @@ -25,9 +25,13 @@ These guidelines should be applied to every new module and to all new developmen going under major changes. In that case first do a **move** commit then apply the changes related to the feature. +.. _contributing/coding_guidelines/module_structure: + Module structure ================ +.. _contributing/coding_guidelines/module_structure/directories: + Directories ----------- @@ -46,6 +50,7 @@ Other optional directories compose the module. - *report/* : contains the printable reports and models based on SQL views. Python objects and XML views are included in this directory - *tests/* : contains the Python tests +.. _contributing/coding_guidelines/module_structure/file_naming: File naming ----------- diff --git a/content/contributing/development/git_guidelines.rst b/content/contributing/development/git_guidelines.rst index 9323555f1..2bae7126c 100644 --- a/content/contributing/development/git_guidelines.rst +++ b/content/contributing/development/git_guidelines.rst @@ -2,6 +2,8 @@ Git guidelines ============== +.. _contributing/git_guidelines/configure_git: + Configure your git ------------------ @@ -17,6 +19,8 @@ way towards making your commits more helpful: - Be sure to add your full name to your Github profile here. Please feel fancy and add your team, avatar, your favorite quote, and whatnot ;-) +.. _contributing/git_guidelines/message_structure: + Commit message structure ------------------------ @@ -42,6 +46,8 @@ description. Try to follow the preferred structure for your commit messages Closes #123 (close related PR on Github) opw-123 (related to ticket) +.. _contributing/git_guidelines/commit_tag_module: + Tag and module name ------------------- @@ -73,6 +79,8 @@ various to tell it is cross-modules. Unless really required or easier avoid modifying code across several modules in the same commit. Understanding module history may become difficult. +.. _contributing/git_guidelines/commit_header: + Commit message header --------------------- @@ -86,6 +94,8 @@ Commit message header should make a valid sentence once concatenated with archive users linked to active partners`` is correct as it makes a valid sentence ``if applied, this commit will prevent users to archive...``. +.. _contributing/git_guidelines/commit_body: + Commit message full description ------------------------------- diff --git a/content/developer/reference/backend/data.rst b/content/developer/reference/backend/data.rst index dcfc40d85..ed80ca1eb 100644 --- a/content/developer/reference/backend/data.rst +++ b/content/developer/reference/backend/data.rst @@ -76,6 +76,8 @@ following attributes: Requires an :term:`external id`, defaults to ``True``. +.. _reference/data/field: + ``field`` --------- @@ -203,6 +205,8 @@ Because some important structural models of Odoo are complex and involved, data files provide shorter alternatives to defining them using :ref:`record tags `: +.. _reference/data/shortcuts/menuitem: + ``menuitem`` ------------ diff --git a/content/developer/reference/backend/orm.rst b/content/developer/reference/backend/orm.rst index 978685a86..fbbd739b7 100644 --- a/content/developer/reference/backend/orm.rst +++ b/content/developer/reference/backend/orm.rst @@ -213,12 +213,20 @@ These helpers are also available by importing `odoo.tools.date_utils`. Relational Fields ~~~~~~~~~~~~~~~~~ +.. _reference/fields/many2one: + .. autoclass:: Many2one() +.. _reference/fields/one2many: + .. autoclass:: One2many() +.. _reference/fields/many2many: + .. autoclass:: Many2many() +.. _reference/fields/command: + .. autoclass:: Command() :members: :undoc-members: diff --git a/content/developer/reference/user_interface/view_architectures.rst b/content/developer/reference/user_interface/view_architectures.rst index 3a32de5e9..9ca57b681 100644 --- a/content/developer/reference/user_interface/view_architectures.rst +++ b/content/developer/reference/user_interface/view_architectures.rst @@ -516,8 +516,8 @@ Structural components Structural components provide structure or "visual" features with little logic. They are used as elements or sets of elements in form views. -Form views accept the following children structural components: :ref:`group -`, :ref:`sheet `, +Form views accept the following children structural components: :ref:`sheet +`, :ref:`group `, :ref:`notebook `, :ref:`notebook `, :ref:`newline `, @@ -529,6 +529,24 @@ Form views accept the following children structural components: :ref:`group Placeholders are denoted in all caps. +.. _reference/view_architectures/form/sheet: + +`sheet`: make the layout responsive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `sheet` element can be used as a direct child of the :ref:`form +` root element for a narrower and more responsive form layout +(centered page, margin...). It usually contains :ref:`group +` elements. + +.. code-block:: xml + +
+ + ... + +
+ .. _reference/view_architectures/form/group: `group`: define columns layouts @@ -615,24 +633,6 @@ The `group` element can have the following attributes: -.. _reference/view_architectures/form/sheet: - -`sheet`: make the layout responsive -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The `sheet` element can be used as a direct child of the :ref:`form -` root element for a narrower and more responsive form layout -(centered page, margin...). It usually contains :ref:`group -` elements. - -.. code-block:: xml - -
- - ... - -
- .. _reference/view_architectures/form/notebook: `notebook` & `page`: add tabbed sections diff --git a/content/developer/tutorials/define_module_data.rst b/content/developer/tutorials/define_module_data.rst index 332dc2329..869fa783c 100644 --- a/content/developer/tutorials/define_module_data.rst +++ b/content/developer/tutorials/define_module_data.rst @@ -191,7 +191,7 @@ When the data to create is more complex it can be useful, or even necessary, to Data Extension ~~~~~~~~~~~~~~ -During the Core Training, we saw in the :doc:`server_framework_101/12_inheritance` chapter we +During the Core Training, we saw in the :doc:`server_framework_101_legacy/12_inheritance` chapter we could inherit (extend) an existing view. This was a special case of data extension: any data can be extended in a module. diff --git a/content/developer/tutorials/pdf_reports.rst b/content/developer/tutorials/pdf_reports.rst index 77717e98f..ee82151ab 100644 --- a/content/developer/tutorials/pdf_reports.rst +++ b/content/developer/tutorials/pdf_reports.rst @@ -7,7 +7,7 @@ Build PDF Reports completed it and use the `estate` module you have built as a base for the exercises in this tutorial. -We were previously :doc:`introduced to QWeb ` +We were previously :doc:`introduced to QWeb ` where it was used to build a kanban view. Now we will expand on one of QWeb's other main uses: creating PDF reports. A common business requirement is the ability to create documents to send to customers and to use internally. These reports can be used to summarize and display @@ -178,7 +178,7 @@ Its contents are all explained in :ref:`the documentation `. + invoice `. However: @@ -140,8 +140,6 @@ Access Rights real-estate application. - Real-estate agents will not be able to update the property types or tags. -Access rights were first introduced in :doc:`server_framework_101/04_securityintro`. - Access rights are a way to give users access to models *via* groups: associate an access right to a group, then all users with that group will have the access. diff --git a/content/developer/tutorials/server_framework_101.rst b/content/developer/tutorials/server_framework_101.rst index e6ef2e04e..31693b405 100644 --- a/content/developer/tutorials/server_framework_101.rst +++ b/content/developer/tutorials/server_framework_101.rst @@ -1,4 +1,5 @@ :show-content: +:hide-page-toc: ==================== Server framework 101 @@ -10,16 +11,23 @@ Server framework 101 server_framework_101/* -Welcome to the Server framework 101 tutorial! If you reached this page that means you are -interested in the development of your own Odoo module. It might also mean that you recently -joined the Odoo company for a rather technical position. In any case, your journey to the -technical side of Odoo starts here. +**Welcome to the Server framework 101 tutorial!** -The goal of this tutorial is for you to get an insight of the most important parts of the Odoo -development framework while developing your own Odoo module to manage real estate assets. The -chapters should be followed in their given order since they cover the development of a new Odoo -application from scratch in an incremental way. In other words, each chapter depends on the previous -one. +Are you eager to learn about the server framework of Odoo? Perhaps you have recently joined Odoo in +a technical role. Either way, this tutorial will level you up as an Odoo developer and show you the +ropes of working with the server framework. + +You will embark on a step-by-step journey, building a real estate asset management application as +you explore the core concepts of the server framework. Each chapter of the tutorial builds upon the +previous one, so make sure to follow them in order. + +.. todo: insert picture of the final view + +.. note:: + Throughout this tutorial, you will encounter exercises designed to reinforce your learning. There + will often be multiple ways to approach these exercises, so feel free to explore your own ideas + and solutions. This is the secret to secure your understanding and develop your problem-solving + skills. .. important:: Before going further, make sure you have prepared your development environment with the @@ -27,18 +35,13 @@ one. Ready? Let's get started! -* :doc:`server_framework_101/01_architecture` -* :doc:`server_framework_101/02_newapp` -* :doc:`server_framework_101/03_basicmodel` -* :doc:`server_framework_101/04_securityintro` -* :doc:`server_framework_101/05_firstui` -* :doc:`server_framework_101/06_basicviews` -* :doc:`server_framework_101/07_relations` -* :doc:`server_framework_101/08_compute_onchange` -* :doc:`server_framework_101/09_actions` -* :doc:`server_framework_101/10_constraints` -* :doc:`server_framework_101/11_sprinkles` -* :doc:`server_framework_101/12_inheritance` -* :doc:`server_framework_101/13_other_module` -* :doc:`server_framework_101/14_qwebintro` -* :doc:`server_framework_101/15_final_word` +- :doc:`server_framework_101/01_architecture_overview` +- :doc:`server_framework_101/02_lay_the_foundations` +- :doc:`server_framework_101/03_build_user_interface` +- :doc:`server_framework_101/04_relational_fields` +- :doc:`server_framework_101/05_business_logic` +- :doc:`server_framework_101/06_security` +- :doc:`server_framework_101/07_advanced_views` +- :doc:`server_framework_101/08_inheritance` +- :doc:`server_framework_101/09_portal` +- :doc:`server_framework_101/10_unit_testing` diff --git a/content/developer/tutorials/server_framework_101/01_architecture_overview.rst b/content/developer/tutorials/server_framework_101/01_architecture_overview.rst new file mode 100644 index 000000000..5c48eab6c --- /dev/null +++ b/content/developer/tutorials/server_framework_101/01_architecture_overview.rst @@ -0,0 +1,72 @@ +================================ +Chapter 1: Architecture overview +================================ + +Before we start building our app, let's take a high level glance at the architecture of Odoo. + +.. _tutorials/server_framework_101/multitier_app: + +Multitier application +===================== + +Odoo leverages a `multitier architecture `_, +meaning that the presentation, the business logic, and the data storage are separated. More +specifically, it uses a three-tier architecture (image from Wikipedia): + +.. image:: 01_architecture_overview/three-tier-architecture.svg + :align: center + :alt: Overview of a three-tier application + +The presentation tier of Odoo is a combination of HTML5, JavaScript, and CSS. The logic tier is +exclusively written in Python, while the data tier only supports PostgreSQL as an :abbr:`RDBMS +(Relational Database Management System Software)`. + +Depending on the scope of your Odoo development, it can be done in any of these tiers. Therefore, +before going any further, it may be a good idea to refresh your memory if you don't have an +intermediate level in these topics. In order to go through this tutorial, you will need a very basic +knowledge of HTML and an intermediate level of Python. There are plenty of tutorials that are freely +accessible, so we cannot recommend one over another since it depends on your background. For +reference, this is the official `Python tutorial `_. + +.. _tutorials/server_framework_101/modules: + +Odoo modules +============ + +Odoo relies on modular components called **modules** to extend its functionality. These modules are +essentially self-contained packages of code and data that serve a specific purpose within the +system. You can think of them as building blocks. + +Modules offer two main ways to customize Odoo: + +- Adding new functionality: You can create entirely new features with modules, such as a real-time + bus fleet visualization module. +- Extending existing functionality: Modules can also be used to modify or enhance existing Odoo + features, like adding your country's accounting rules to the generic accounting support. + +Terminology: + +- Developers group their business features in Odoo *modules*. +- The main user-facing modules are flagged and exposed as *Apps*, but a majority of the modules are + not Apps. +- *Modules* may also be referred to as *addons*. + +In practice, modules are represented by directories. They are placed in a designated location called +the **addons path**, which the server scans to discover available modules. + +In the :doc:`setup guide <../setup_guide>`, we cloned the `odoo/tutorials` repository and included +it in the `addons-path` argument when starting the server. The directories present in the repository +are all modules that can be installed on your Odoo server. + +.. exercise:: + In your file explorer, navigate to the `odoo/tutorials` repository and inspect the available + modules. You should find the `real_estate` module in which we will build our real estate + application throughout this tutorial. + +You must have noticed that the module directories are not empty; they all contain at least two +essential files: :file:`__init__.py` and :file:`__manifest__.py`. These files are what makes a +simple directory an Odoo module. We'll get back to it in the next chapter. + +---- + +Ready to start? Let's now :doc:`lay the foundations <02_lay_the_foundations>` of our first Odoo app! diff --git a/content/developer/tutorials/server_framework_101/01_architecture/three_tier.svg b/content/developer/tutorials/server_framework_101/01_architecture_overview/three-tier-architecture.svg similarity index 100% rename from content/developer/tutorials/server_framework_101/01_architecture/three_tier.svg rename to content/developer/tutorials/server_framework_101/01_architecture_overview/three-tier-architecture.svg 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 new file mode 100644 index 000000000..2964cd5a0 --- /dev/null +++ b/content/developer/tutorials/server_framework_101/02_lay_the_foundations.rst @@ -0,0 +1,573 @@ +============================== +Chapter 2: Lay the foundations +============================== + +In this chapter, we'll focus on the fundamental building blocks of Odoo's data structure: models, +fields, and records. We'll use them together to lay the foundations of our real estate application. + +.. _tutorials/server_framework_101/install_app: + +Install the app +=============== + +If you followed the :doc:`setup guide <../setup_guide>` carefully, you should now have a running +Odoo server with the `odoo/tutorials` repository in the `addons-path`, and be logged in as an +administrator. + +**Let's install our real estate app!** In your browser, open Odoo and navigate to +:menuselection:`Apps`. Search for :guilabel:`Real Estate` and click :guilabel:`Activate`. + +Nothing has changed? That's normal; the `real_estate` module you just installed is currently an +empty shell. It doesn't even have a menu entry. Currently, it only contains three files: + +- An empty :file:`__init__.py` file to make Python treat the :file:`real_estate` directory as a + package. +- The :file:`__manifest__.py` file that declares the :file:`real_estate` Python package as an Odoo + module. +- The optional :file:`static/description/icon.png` file that serves as the app icon. + +The first two files are the minimum requirements for a directory to be considered an Odoo module. + +.. exercise:: + Search for each key of the :file:`real_estate/__manifest__.py` file in the :ref:`reference + documentation ` and understand the base metadata that defines the + `real_estate` module. + +.. seealso:: + `The manifest file of the Sales app <{GITHUB_PATH}/addons/sale/__manifest__.py>`_ + +.. _tutorials/server_framework_101/create_first_model: + +Create the first model +====================== + +Now that our module is recognized by Odoo, it's time to build towards the business features. Data is +essential for any real-world application, and our real estate application won't be an exception. + +To store data effectively, we need two things: a way to define the structure of that data, and a +system to store and manipulate it. Fortunately, the server framework of Odoo comes equipped with the +perfect tool: the :abbr:`ORM (Object Relational Mapper)` layer. It's the ORM that takes care of +preparing and executing SQL queries for you when you manipulate data in Python. + +The ORM simplifies data access and manipulation by allowing you to define **models**. Models act as +blueprints for your data; they define the structure and organization of information within your +module. You can see them as templates that specify what kind of data your module will handle. For +example, real estate properties, owners, or tenants. In Odoo, models are represented by Python +classes that inherit from the `odoo.models.Model` class provided by the ORM, and they are identified +by their `_name` attribute. In addition to `_name`, models also accept the `_description` attribute +to define a human-readable description of the model. + +Each model is made of smaller components called **fields**. You can see them as the individual +characteristics that describe your data. Fields allow you to define the relevant data your +application needs to capture and manage. In the case of real estate properties, some example fields +could be the property name, the type (house, apartment, etc.), and the floor area. Within model +classes, fields are defined as class attributes. Each field is an instance of a class from the +`odoo.fields` package. For example, `Char`, `Float`, `Boolean`, each designed to handle different +types of data. When defining a field, developers can pass various arguments to finely control how +data is handled and presented in Odoo. For example, `string` defines the label for the field in the +user interface, `help` provides a tooltip when hovering the field in the user interface, and +`required` makes filling in the field mandatory. + +Individual data entries are called **records**. They are based on the structure defined by models +and contain the actual data for each field specified in the model. In Python, records are +represented as instances of the model's class, allowing developers to interact with data using +object-oriented programming techniques. For example, in a real estate application using a tenant +model, each specific tenant (such as "Bafien Carpink") would be a separate record of that model. + +.. seealso:: + For the full list of fields and their attributes, see the :ref:`reference documentation + `. + +.. example:: + Before we dive into creating our own models, let's take a look at a basic example of a model that + represents storable products. It defines a `product` model with the `Product` class inheriting + from `models.Model`. Within this class, several fields are defined to capture product data: + + .. code-block:: python + + from odoo import fields, models + + + class Product(models.Model): + _name = 'product' + _description = "Storable Product" + + name = fields.Char(string="Name", required=True) + description = fields.Text(string="Description") + price = fields.Float(string="Sale Price", required=True) + category = fields.Selection( + string="Category", + help="The category of the product; if none are suitable, select 'Other'.", + selection=[ + ('apparel', "Clothing") + ('electronics', "Electronics"), + ('home_decor', "Home Decor"), + ('other', "Other"), + ], + required=True, + default='apparel', + ) + + .. note:: + - `name` is a `Char` field while `description` is a `Text` field; `Char` fields are typically + used for short texts, whereas `Text` fields can hold longer content and multiple lines. + - The label of the `price` field is arbitrary and doesn't have to be the upper-cased version + of the attribute name. + - `category` is a `Selection` field with predefined options, each defined by a technical code + and a corresponding label. Since it is required, a default value is provided. + +Building on these new concepts, let's now create the first model for our real estate app. We'll +create a model with some fields to represent real estate properties and their characteristics. + +.. exercise:: + + #. Create a new :file:`real_estate_property.py` file at the root of the `real_estate` module. + #. Update the :file:`real_estate/__init__.py` file to relatively import the + :file:`real_estate_property.py` file, like so: + + .. code-block:: python + + from . import real_estate_property + + #. Define a new model with `real.estate.property` as `_name` and a short `_description`. + #. Add fields to represent the following characteristics: + + - Name (required) + - Description + - Image (max 600x400 pixels) + - Active (default to true) + - State (new, offer received, under option, or sold; required; default to new) + - Type (house, apartment, office building, retail space, or warehouse; required; default to + house) + - Selling Price (without currency; with help text; required) + - Availability Date (default to creation date + two months) + - Floor Area (in square meters; with help text) + - Number of Bedrooms (default to two) + - Whether there is a garden + - Whether there is a garage + + .. tip:: + - The class name doesn't matter, but the convention is to use the model's upper-cased `_name` + (without dots). + - Rely on the reference documentation for :ref:`fields ` to select the + right class and attributes for each field. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `__init__.py` + + from . import real_estate_property + + .. code-block:: python + :caption: `real_estate_property.py` + + from odoo import fields, models + from odoo.tools import date_utils + + + class RealEstateProperty(models.Model): + _name = 'real.estate.property' + _description = "Real Estate Property" + + name = fields.Char(string="Name", required=True) + description = fields.Text(string="Description") + image = fields.Image(string="Image") + active = fields.Boolean(string="Active", default=True) + state = fields.Selection( + string="State", + selection=[ + ('new', "New"), + ('offer_received', "Offer Received"), + ('under_option', "Under Option"), + ('sold', "Sold"), + ], + required=True, + default='new', + ) + type = fields.Selection( + string="Type", + selection=[ + ('house', "House"), + ('apartment', "Apartment"), + ('office', "Office Building"), + ('retail', "Retail Space"), + ('warehouse', "Warehouse"), + ], + required=True, + default='house', + ) + selling_price = fields.Float( + string="Selling Price", help="The selling price excluding taxes.", required=True + ) + availability_date = fields.Date( + string="Availability Date", default=date_utils.add(fields.Date.today(), months=2) + ) + floor_area = fields.Integer( + string="Floor Area", help="The floor area in square meters excluding the garden." + ) + bedrooms = fields.Integer(string="Number of bedrooms", default=2) + has_garden = fields.Boolean(string="Garden") + has_garage = fields.Boolean(string="Garage") + +Congrats, you have just defined the first model of our real estate app! However, the changes have +not yet been applied to the database. To do so, you must add the `-u real_estate` argument to the +server start-up command and restart the server. The :option:`-u ` argument +instructs the server to update the specified modules at start-up. + +.. _tutorials/server_framework_101/inspect_sql_table: + +Inspect the SQL table +===================== + +Earlier, we quickly introduced models as a convenient way to store and handle data in Odoo. In fact, +these models not only define the structure and behavior of data in Python, but they also correspond +to SQL tables in the database. The `_name` attribute of their model is taken (with dots replaced by +underscores) as the name of the corresponding table. For example, the `real.estate.property` model +is linked to the `real_estate_property` table. + +The same goes for fields that become columns in the SQL table of their model. The name of the class +attribute representing the field is taken as the column name while the field's class determines the +column type. + +Once the server is running again, let's take a look in the database and see how the model and fields +you created translate into a new SQL table. We will use `psql`, the CLI +:dfn:`command-line interface` allowing to browse and interact with PostgreSQL databases. + +.. exercise:: + + #. In your terminal, execute the command :command:`psql -d tutorials`. + #. Enter the command :command:`\\d real_estate_property` to print the description of the + `real_estate_property` table. + #. For each field of the `real.estate.property` model, try to understand how the field's + attributes alter the table. + #. Enter the command :command:`exit` to exit `psql`. + +.. spoiler:: Solution + + .. code-block:: text + + $ psql -d tutorials + + tutorials=> \d real_estate_property + Table "public.real_estate_property" + Column | Type | Collation | Nullable | Default + -------------------+-----------------------------+-----------+----------+-------------------------------------------------- + id | integer | | not null | nextval('real_estate_property_id_seq'::regclass) + floor_area | integer | | | + bedrooms | integer | | | + create_uid | integer | | | + write_uid | integer | | | + name | character varying | | not null | + state | character varying | | not null | + type | character varying | | not null | + availability_date | date | | | + description | text | | | + active | boolean | | | + has_garden | boolean | | | + has_garage | boolean | | | + create_date | timestamp without time zone | | | + write_date | timestamp without time zone | | | + selling_price | double precision | | not null | + Indexes: + "real_estate_property_pkey" PRIMARY KEY, btree (id) + Foreign-key constraints: + "real_estate_property_create_uid_fkey" FOREIGN KEY (create_uid) REFERENCES res_users(id) ON DELETE SET NULL + "real_estate_property_write_uid_fkey" FOREIGN KEY (write_uid) REFERENCES res_users(id) ON DELETE SET NULL + + exit + + - Each field, except the image that is saved as an attachment, is represented in the + `real_estate_property` SQL table by a column whose type is determined by the field's class: + + +--------------------+----------------------+ + | Field class | Column type | + +====================+======================+ + | `fields.Integer` | `integer` | + +--------------------+----------------------+ + | `fields.Float` | `double precision` | + +--------------------+----------------------+ + | `fields.Char` | `character varying` | + +--------------------+----------------------+ + | `fields.Text` | `text` | + +--------------------+----------------------+ + | `fields.Selection` | `character varying` | + +--------------------+----------------------+ + | `fields.Boolean` | `boolean` | + +--------------------+----------------------+ + | `fields.Date` | `date` | + +--------------------+----------------------+ + + - The `required` attribute of a field prevents the corresponding column to be nullable. + + - The `default` attribute of a field does *not* set a default value on the column; instead, it's + the ORM that takes care of setting default values for newly created records. + +You might be surprised to find that the generated SQL table contains more columns than just the +fields you defined. That is because Odoo automatically adds several readonly :dfn:`you can read but +not write` fields to each model for internal purposes. Here are some additional fields you'll +typically find: + +- `id`: A unique identifier that is automatically computed for each new record. +- `create_date`: The timestamp of when the record was created. +- `create_uid`: The ID of the user who created the record. +- `write_date`: The timestamp of the last modification to the record. +- `write_uid`: The ID of the user who last modified the record. + +.. seealso:: + :ref:`Reference documentation for automatic fields ` + +.. _tutorials/server_framework_101/load_data_files: + +Load data files +=============== + +Now that we have created our first model, let's consider an important question: What's missing from +our database? The answer is simple: data! + +While we could create new records directly from the user interface, this approach has some +limitations. It would be quite tedious and time-consuming, especially for large amounts of data, and +the changes would only affect the current database. + +.. _tutorials/server_framework_101/xml_data_files: + +XML data files +-------------- + +Fortunately, the server framework allows for a different approach: describe data operations in XML +format in so-called **data files** that the server automatically loads at start-up in sequential +order. This automates the process of populating the database, saving time and effort, and allows +developers to include default data or configurations directly in their modules. + +The most common data operation is creating new records through the `record` and `field` XML +elements, but other operations exist, such as `delete`, which deletes previously created records, or +even `function`, which allows executing arbitrary code. + +.. seealso:: + :doc:`Reference documentation for XML data files <../../reference/backend/data>` + +Some data operations require their data elements to be uniquely identified by the system. This is +achieved by means of the `id` attribute, also known as the **XML ID** or **external identifier**. It +provides a way for other elements to reference it with the `ref` attribute and links data elements +to the records they create or update. XML IDs are automatically prefixed with their module name when +created from a data file so that records can be referenced by their full XML ID `.`. + +.. example:: + Let's again take the `product` model as an example and describe a few product records in a data + file. + + .. code-block:: xml + + + + + + Coffee table + A dark wood table easy to match with other furnishing. + 275 + home_decor + + + + T-shirt + 29.99 + + + + + + .. note:: + As we can see, data files are rather straightforward: + + - The root element must be `odoo`. + - Multiple data operations can be described inside a single `odoo` element. + - The `id` attribute can be written with the module prefix included for clarity. + - Required fields must be provided a value if they don't have a default one. + - Non-required fields can be omitted. + - The `ref` attribute is used to reference other records by their XML ID and use their record + ID as value. + +Let's now load some default real estate properties in our database. + +.. exercise:: + + #. Create a new :file:`real_estate_property_data.xml` file at the root of the `real_estate` + module. + #. Update the manifest to let the server know that it should load our data file. To do so, have + the `data` key list our data file name. + #. Use the `record` and `field` data operation to describe at least three default properties + records. Try to vary property types and set different values than the default ones. Add the + image files for the various properties at the root of the `real_estate` module and assign them + to the properties' Image field. + #. Restart the server, again with the `-u real_estate` argument, to load the module data at + server start-up. + #. In the terminal, execute the command `psql -d tutorials` and enter the command + `SELECT * FROM real_estate_property;` to verify that the records were loaded. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 2 + + 'data': [ + 'real_estate_property_data.xml', + ], + + .. code-block:: text + :caption: `country_house.png` + + + + .. code-block:: text + :caption: `loft.png` + + + + .. code-block:: text + :caption: `mixed_use_commercial.png` + + + + .. code-block:: xml + :caption: `real_estate_property_data.xml` + + + + + + Country house + In the charming village of Grand-Rosière-Hottomont, 5 minutes from all facilities (shops, schools, public transport, ...), we offer this superb newly renovated country house! + + house + 745000 + 416 + 5 + True + True + + + + Loft + Located on the 1st floor of a small, fully renovated building, magnificent 195 m² three-bedroom apartment with parking space. + + apartment + 339000 + 2025-01-01 + 195 + 3 + False + True + + + + Mixed use commercial building + The property is a former bank agency which consists of a retail ground floor, a basement and 2 extra office floors. + + retail + 335000 + 2024-10-02 + 370 + 0 + False + False + + + + +.. _tutorials/server_framework_101/csv_data_files: + +CSV data files +-------------- + +In addition to XML data files, the server framework allows loading data files in CSV format. This +format is often more convenient for describing records with simple field values belonging to the +same model. It also loads faster, making it the go-to format when performance matters most. + +.. seealso:: + :ref:`Reference documentation for CSV data files ` + +.. example:: + See below for an example of how a subset of `country states can be loaded into Odoo + <{GITHUB_PATH}/odoo/addons/base/data/res.country.state.csv>`_. + + .. code-block:: csv + :caption: `res.country.state.csv` + + "id","country_id:id","name","code" + state_ca_ab,ca,"Alberta","AB" + state_ca_bc,ca,"British Columbia","BC" + state_ca_mb,ca,"Manitoba","MB" + state_ca_nb,ca,"New Brunswick","NB" + state_ca_nl,ca,"Newfoundland and Labrador","NL" + state_ca_nt,ca,"Northwest Territories","NT" + state_ca_ns,ca,"Nova Scotia","NS" + state_ca_nu,ca,"Nunavut","NU" + state_ca_on,ca,"Ontario","ON" + state_ca_pe,ca,"Prince Edward Island","PE" + state_ca_qc,ca,"Quebec","QC" + state_ca_sk,ca,"Saskatchewan","SK" + state_ca_yt,ca,"Yukon","YT" + + .. note:: + + - The file name must match the model name. + - The first line lists the model fields to populate. + - XML IDs are specified via the special `id` field. + - The `:id` suffix is used to reference other records by their XML ID and use their record ID + as value. + - Each subsequent line describes one new record. + +In business applications like Odoo, one of the first questions to consider is who can access the +data. By default, access to newly created models is restricted until it is explicitly granted. +Granting access rights is done by creating records of the `ir.model.access` model, which specifies +who has access to which model. + +The topic of security will be covered in detail in :doc:`../restrict_data_access`. For now, we'll +just give ourselves access rights to the `real.estate.property` model to get rid of the warning that +began being logged at server start-up after creating the model: + +.. code-block:: text + + WARNING tutorials odoo.modules.loading: The models ['real.estate.property'] have no access rules [...] + +.. exercise:: + + #. Create a new :file:`ir.model.access.csv` file at the root of the `real_estate` module. + #. Declare it in the manifest as you did for the :file:`real_estate_property_data.xml` file. + #. Grant access to the `real.estate.property` model to all administrators of the database by + adding new access rights with the following specifications: + + - XML ID: `real_estate_property_system` + - `name`: `real.estate.property.system` + - `model_id`: The record ID of `model_real_estate_property` + - `group_id`: The record ID of `base.group_system` + - `perm_read`, `perm_write`, `perm_create`, and `perm_unlink`: `1` + + .. tip:: + In Odoo, modules and models are automatically given an XML ID computed by prefixing their name + with `module_` and `model_` respectively. + +.. spoiler:: Solution + + .. code-block:: py + :caption: `__manifest__.py` + :emphasize-lines: 2 + + 'data': [ + 'ir.model.access.csv', + 'real_estate_property_data.xml', + ], + + .. code-block:: csv + :caption: `ir.model.access.csv` + + id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + real_estate_property_system,real.estate.property.system,model_real_estate_property,base.group_system,1,1,1,1 + +After restarting the server, the warning should no longer appear. + +---- + +In the next chapter, we'll :doc:`create user interface elements <03_build_user_interface>` to +interact with the property model. 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 new file mode 100644 index 000000000..2000b89da --- /dev/null +++ b/content/developer/tutorials/server_framework_101/03_build_user_interface.rst @@ -0,0 +1,709 @@ +=================================== +Chapter 3: Build the user interface +=================================== + +In this chapter, we will bring our application to life by building a :abbr:`UI (User Interface)` +that allows users to interact with the models. This will require defining menu items, actions, and +views. By the end of this chapter, we will have a fully functional interface to manage real estate +properties! + +.. _tutorials/server_framework_101/menu_items: + +Add menus items +=============== + +**Menus items** are the first thing users see and interact with. They give users access to the +different parts of Odoo and can be nested to form a hierarchical structure. This allows the +functionalities of complex applications to be organized into categories and sub-categories and makes +them easier to navigate. The top level of the menu structure typically contains the menu items for +the main applications (like "Contacts", "Sales", and "Accounting"). These top-level menu items can +also be visually enhanced with custom icons for better recognition. + +Menu items can take on two distinct roles: + +- **Category menu items**: They serve as organizational containers and simply expand to show their + submenu when clicked. +- **Action menu items**: They trigger a specific action in the UI when clicked. While menu items + often trigger actions, they are separate entities. The same action can be triggered by different + menu items, or even buttons. + +In Odoo, menu items are actually records of the `ir.ui.menu` model whose key fields are: + +.. rst-class:: o-definition-list + +`name` (required) + The title of the menu item. +`sequence` + Determines the ordering of same-level menu items. +`parent_id` + The ID of the parent menu item's record. +`web_icon` + The path to the icon file to use as a menu item icon. +`action` + The action to trigger when the menu item is clicked. + +Like for any other model, one can automatically create records of the `ir.ui.menu` model by means of +a data file. 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. + + - 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: + + - `base.open_module_tree` that opens the list of modules. + - `base.action_client_base_menu` that opens the general settings. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 3 + + 'data': [ + 'ir.model.access.csv', + 'menus.xml', + 'real_estate_property_data.xml', + ], + + .. code-block:: xml + :caption: `menus.xml` + + + + + + Real Estate + real_estate,static/description/icon.png + + + + Properties + 10 + + + + + + Settings + 20 + + + + + + +If you go to the app switcher :dfn:`the top-level menu of Odoo`, you should now see a menu item for +our real estate app! Click it to open the app and automatically trigger the first action in its +sub-menu. If you referenced the `base.open_module_tree` action, you should now see a list of Odoo +modules. + +.. _tutorials/server_framework_101/menuitem_shortcut: + +Use the `menuitem` shortcut +--------------------------- + +As an application grows in size, so do its menus, and it becomes increasingly complicated to define +and nest menu items. While defining menu items using the `record` data operation works perfectly +fine, the server framework provides a shortcut that makes the process easier and more intuitive, +especially for nesting menu items: the `menuitem` data operation. + +The `menuitem` tag is a special XML element that is specifically designed for creating menu items; +it simplifies the syntax and automatically handles some technical details for you. + +.. example:: + Our fictional `product` module could define menu items as follows: + + .. code-block:: xml + + + + + + + .. note:: + - The outer `menuitem` data operation creates the top-level "Product" menu item. + - The specifications (`name`, `web_icon`, `sequence`, `action`, ...) of menu items are set + through attributes of the XML element. + - The menu items hierarchy is defined by nesting their XML elements. + +Why keep complex code when you can simplify it? It's already time for our first **code +refactoring**! + +.. exercise:: + Rewrite the description of the menu items of our real estate app using the `menuitem` data + operation instead of `record`. + +.. spoiler:: Solution + + .. code-block:: xml + :caption: `menus.xml` + :emphasize-lines: 4-21 + + + + + + + + + + + +.. _tutorials/server_framework_101/define_window_actions: + +Define window actions +===================== + +**Actions** define what happens when a user interacts with the UI, such as clicking a menu item. +They connect the user interface with the underlying business logic. There exist different types of +actions in Odoo, the most common one being **window actions** (`ir.actions.act_window`), that +display the records of a specific model in a view. Other types of actions allow for different +behaviors, like **URL actions** that open URLs (`ir.actions.act_url`) or **server actions** +(`ir.actions.server`) that execute custom code. + +In Odoo, actions can be stored in the database as records or returned as Python dictionaries +interpreted as action descriptors when business logic is executed. Window actions are described by +the `ir.actions.act_window` model whose key fields include: + +.. rst-class:: o-definition-list + +`name` (required) + The title of the action; is often used as the page title. +`res_model` (required) + The model on which the action operates. +`view_mode` + A comma-separated list of view types to enable for this action; for example, `list,form,kanban`. +`help` + An optional help text for the users when there are no records to display. + +.. seealso:: + :doc:`Reference documentation for actions <../../reference/backend/actions>` + +.. example:: + The example below defines an action to open existing products in either list or form view. + + .. code-block:: xml + + + Products + product + list,form + +

+ Create a new product. +

+
+
+ + .. note:: + The content of the `help` field can be written in different formats thanks to the `type` + attribute of the :ref:`field ` data operation. + +As promised, we'll finally get to interact with our real estate properties in the UI. All we need +now is an action to assign to the menu item. + +.. exercise:: + + #. Create and declare a new :file:`actions.xml` file at the root of the `real_estate` module. + #. Describe a new "Properties" window action that opens `real.estate.property` records in list + and form views, and assign it to the "Properties" menu item. Be creative with the help text! + For reference, the list of supported classes can be found in the `view.scss + <{GITHUB_PATH}/addons/web/static/src/views/view.scss>`_ file. + + .. tip:: + Pay attention to the declaration order of data files in the manifest; you might introduce a + data operation that depends on another one. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 2,4 + + 'data': [ + 'actions.xml', + 'ir.model.access.csv', + 'menus.xml', # Depends on `actions.xml` + 'real_estate_property_data.xml', + ], + + .. code-block:: xml + :caption: `actions.xml` + + + + + + Properties + real.estate.property + list,form + + +

+ Create a new property. +

+
+
+ +
+ + .. code-block:: xml + :caption: `menus.xml` + :emphasize-lines: 5 + + + +Clicking the "Properties" menu item now displays a list view of the default properties we created +earlier. As we specified in the action that both list and form views were allowed, you can click any +property record to display its form view. Delete all three records to see the help text you created. + +.. _tutorials/server_framework_101/create_custom_views: + +Create custom views +=================== + +**Views** are the UI's building blocks, defining how model data is displayed on screen. They are +structures written in XML that describe the layout and behavior of various UI components. + +Odoo supports different types of views, each serving a different purpose. The most common types +include **list views** for listing multiple records in a table-like format, **form views** for +displaying and editing individual records, **kanban views** for presenting records in a card layout, +and **search views** for defining search and filtering options. + +In Odoo, views are records of the `ir.ui.view` model. Each view is associated with a specific model, +determining which data it displays and interacts with. Key fields include: + +.. rst-class:: o-definition-list + +`name` (required) + A unique name for the view. +`model` (required) + The model the view is associated with. +`arch` (required) + The view architecture as an XML string. + +The `arch` field holds the view's XML architecture, which is composed of a root element determining +the type of the view, and various inner components that depend on the view type. The root element +(e.g., `list`, `form`, `search`) defines the view type, while the inner components describe the +structure and content of the view. These components can be structural (like `sheet` that makes the +layout responsive, or `group` that defines column layouts) or semantic (like `field` that displays +field labels and values). + +.. seealso:: + - :doc:`Reference documentation for view records <../../reference/user_interface/view_records>` + - :doc:`Reference documentation for view architectures + <../../reference/user_interface/view_architectures>` + +.. example:: + The following examples demonstrate how to define simple list, form and search views for the + `product` model. + + .. code-block:: xml + :caption: A list view for `product` + + + Product List + product + + + + + + + + + + .. code-block:: xml + :caption: A form view for `product` + + + Product Form + product + +
+ + + + + + + + +
+
+
+ + .. code-block:: xml + :caption: A search view for `product` + + + Product Search + product + + + + + + + + + .. note:: + + - The XML structure differs between view types. + - The `description` field is omitted from the list view because it wouldn't fit visually. + +In :ref:`the previous section `, we defined +the `view_mode` of our action to display `real.estate.property` records in list and form view. +Although we haven't created the corresponding views yet, the server framework had our back and +automatically provided generic views. The generic list and form views were hard to miss, but a +generic search view was also provided; when searching for properties, you are in fact searching on +property names because it's the only field of the generic view. + +However convenient, we should almost never rely on these generic views in business applications. +They are incomplete, badly structured, and often use the wrong field widgets. Let's create our own +custom views for a better :abbr:`UX (User experience)`. + +.. _tutorials/server_framework_101/list_view: + +List view +--------- + +For a start, the list view could use more fields than just the name. + +.. exercise:: + + #. Create a new :file:`real_estate_property_views.xml` file at the root of the `real_estate` + module. + #. Create a custom list view to display the following fields of the `real.estate.property` model + in the given order: name, state, type, selling price, availability date, floor area, number of + bedrooms, presence of a garden, and presence of a garage. + #. Make the visibility of the floor area and all following fields optional so that only the floor + area is visible by default, while the remaining fields are hidden by default and must be + displayed by accessing the view's column selector (:icon:`oi-settings-adjust` button). + #. After restarting the server to load the new data, refresh the browser to see the result. + + .. tip:: + Rely on the reference documentation for :ref:`the field component in list views + `. + + The final result should look like this: + + .. image:: 03_build_user_interface/custom-list-view.png + :align: center + +.. spoiler:: Solution + + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 6 + + 'data': [ + 'actions.xml', + 'ir.model.access.csv', + 'menus.xml', # Depends on `actions.xml` + 'real_estate_property_data.xml', + 'real_estate_property_views.xml', + ], + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + + + + + + Property List + real.estate.property + + + + + + + + + + + + + + + + + +.. _tutorials/server_framework_101/form_view: + +Form view +--------- + +.. exercise:: + + In the :file:`real_estate_property_views.xml` file, create a custom form view to display all + fields of the `real.estate.property` model in a well-structured manner: + + - The state should be displayed as a status bar in the header and should be able to be updated + with a click. + - The form should have margins (hint: use the `sheet` component). + - The name should be displayed as the title of the form, should have its label on top, and should + have a placeholder. + - The image should be displayed as a thumbnail on the right side of the form. + - The fields should be grouped in two sections displayed next to each other: + + - Listing Information: Type, Selling Price, Availability Date, Active + - Building Specifications: Floor Area, Number of Bedrooms, Garden, Garage + + - The description should be displayed at the bottom of the form in its own section, should have + no label, should have a placeholder, and should take the full width. + + .. tip:: + - Rely on the reference documentation for :ref:`structural components + ` and :ref:`the field component + ` in form views. + - Add the :option:`--dev xml ` argument to the server start-up command to + instruct the server to load records defined in XML from your filesystem rather than from the + database. This avoids restarting the server after modifying an XML file. + + The final result should look like this: + + .. image:: 03_build_user_interface/custom-form-view.png + :align: center + +.. spoiler:: Solution + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + + + Property Form + real.estate.property + +
+
+ +
+ + +
+
+ + + + + + + + + + + + + + + + +
+
+
+
+ +.. _tutorials/server_framework_101/search_view: + +Search view +----------- + +The `name` and `active` fields we added earlier to the model are not ordinary fields; they're +examples of **reserved fields**. When set on a model, these special fields enable specific +pre-defined behaviors. For example, the `active` field enables **archiving** (`active = False`) and +**unarchiving** (`active = True`) records through the :icon:`oi-archive` :guilabel:`Archive` and +:icon:`oi-unarchive` :guilabel:`Unarchive` buttons in the action menu. Archived records are +automatically excluded from searches. You can observe this behavior by deselecting the +:guilabel:`Active` checkbox for one of your property records: you'll notice the record no longer +appears upon returning to the list view. + +.. seealso:: + :ref:`Reference documentation for the list of reserved field names + ` + +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:`(, , )`. + +.. example:: + The example search domain below selects only products of the category "Home Decor" whose price is + less than 1000. + + .. code-block:: python + + [('category', '=', 'home_decor'), ('price', '<', 1000)] + +By default, domain criteria are combined with an implicit logical `&` (AND) operator, meaning +*every* criterion must be satisfied for a record to match a domain. Criteria can also be combined +with the logical `|` (OR) and `!` (NOT) operators in prefix form :dfn:`the operator is inserted +before its operands`. + +.. example:: + The example search domain below selects only products that belong to the category "Electronics" + *or* whose price is *not* between 1000 and 2000. + + .. code-block:: python + + ['|', ('category', '=', 'electronics'), '!', '&', ('price', '>=', 1000), ('price', '<', 2000)] + +.. seealso:: + :ref:`Reference documentation for search domains ` + +All the generic search view only allows for is searching on property names; that's the bare minimum. +Let's enhance the search capabilities. + +.. exercise:: + + #. Create a custom search view with the following features: + + - Enable searching on the these fields: + + - Name: Match records whose name contain the search value. + - Description: Match records whose description *or* name contains the search value. + - Selling price: Match records with a price *less than or equal to* the search value. + - Floor area: Match records with a floor area *at least* the search value. + - Number of bedrooms: Match records with *at least* the given number of bedrooms. + + - Implement these filters: + + - For Sale: The state is "New" or "Offer Received". + - Availability Date: Display a list of pre-defined availability date values. + - Garden: The property has a garden. + - Garage: The property has a garage. + - Archived: The property is archived. + + - Combine selected filters with a logical AND, except for Garden and Garage, which should use + OR when both are selected. + - Enable grouping properties by state and type. + + #. Modify the window action to display only properties available for sale by default. + #. Make sure that everything works! + + .. tip:: + + - Rely on the reference documentation for :ref:`search view components + `, :ref:`search domains + `, and :ref:`search defaults + `. + - In XML, use entity references to avoid parsing errors: `<` for `<`, `>` for `>`, and + `&` for `&`. + + The final result should look like this: + + .. image:: 03_build_user_interface/custom-search-view-fields.png + :align: center + + .. image:: 03_build_user_interface/custom-search-view-filters.png + :align: center + +.. spoiler:: Solution + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + + + Property Search + real.estate.property + + + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: xml + :caption: `actions.xml` + :emphasize-lines: 4 + + + Properties + real.estate.property + {'search_default_filter_for_sale': True} + list,form + + +

+ Create a new property. +

+
+
+ +---- + +We now have a shiny UI to manage real estate properties, but our information model is still quite +basic. We have a limited set of property types and a few building specifications, but that's not +enough for a good real estate application. In the next chapter, we'll :doc:`connect properties to +new models <04_relational_fields>` to transform our basic real estate app into a feature-rich tool. 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 new file mode 100644 index 0000000000000000000000000000000000000000..3bb3bd923fe8b9d9ed6a2888ca61b1a18799e046 GIT binary patch literal 26402 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 Sz3A|$JuP)TwTcJUul^4YG;eYM literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..1fd824406657390cbe83e7d9282ff805f24d96c6 GIT binary patch literal 16697 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 DU$Zae literal 0 HcmV?d00001 diff --git a/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-fields.png b/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-fields.png new file mode 100644 index 0000000000000000000000000000000000000000..3cd2ffae1c3bed5e03d5e195b49ee5e822582671 GIT binary patch literal 7594 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 Jl?oQ`{tHHR=Q98R literal 0 HcmV?d00001 diff --git a/content/developer/tutorials/server_framework_101/04_relational_fields.rst b/content/developer/tutorials/server_framework_101/04_relational_fields.rst new file mode 100644 index 000000000..83e50beec --- /dev/null +++ b/content/developer/tutorials/server_framework_101/04_relational_fields.rst @@ -0,0 +1,996 @@ +================================== +Chapter 4: Extend the model family +================================== + +In Odoo, data rarely exists in isolation. The true power of an application lies in its ability to +connect and relate different pieces of information. In this chapter, we'll explore the three +fundamental types of model relationships in Odoo: Many2One, One2Many, and Many2Many. Mastering these +connections will allow us to create rich, interconnected data structures that will form the backbone +of our real estate application. + +.. _tutorials/server_framework_101/module_structure: + +Module structure +================ + +As our `real_estate` module grows, you may notice that we've already created a dozen files for just +one model, along with its menu items, actions and views. With more models on the horizon, our module +directory could quickly become cluttered. To address this potential issue, Odoo provides **module +structure guidelines** that offer several benefits: + +- **Improved maintainability**: A well-organized directory structure makes it easier to navigate the + module and locate specific files. +- **Scalability**: Proper organization prevents the module from becoming cluttered as it grows in + complexity and size. +- **Collaboration**: A standardized structure facilitates understanding among contributors and + ensures easier integration with the Odoo ecosystem. + +.. seealso:: + :ref:`Coding guidelines on module directories + ` + +.. example:: + Let's consider a possible structure for our example `product` module: + + .. code-block:: text + + product/ + │ + ├── data/ + │ └── product_data.xml + │ + ├── models/ + │ ├── __init__.py + │ └── product.py + │ + ├── security/ + │ └── ir.model.access.csv + │ + ├── views/ + │ ├── menus.xml + │ └── product_views.xml + │ + ├── static/ + │ ├── description/ + │ │ └── icon.png + │ │ + │ └── img/ + │ ├── coffee_table.png + │ └── t_shirt.png + │ + ├── __init__.py + └── __manifest__.py + + .. note:: + + - The :file:`models` directory contains its own :file:`__init__.py` file, simplifying Python + imports. The root :file:`__init__.py` file imports the :file:`models` Python package, which + in turns imports individual model files. + - Security-related files, such as :file:`ir.model.access.csv`, are placed in the dedicated + :file:`security` directory. + - UI files (:file:`menus.xml`, and view definitions) are organized within the :file:`views` + directory. + - There is no :file:`actions.xml` file. That is because managing actions is easier when they + are defined in the same file as the views they're linked to. + - The app icon resides in :file:`static/description`, while other image assets are stored in + :file:`static/img`. + - The :file:`__init__.py` and :file:`__manifest__.py` files remain in the module's root + directory. + +.. exercise:: + + Restructure the `real_estate` module according to the guidelines. + + .. tip:: + Use `[CLN]` for your :ref:`commit message tag + `. + +.. spoiler:: Solution + + .. code-block:: text + :caption: Module structure + + real_estate/ + │ + ├── data/ + │ └── real_estate_property_data.xml.xml + │ + ├── models/ + │ ├── __init__.py + │ └── real_estate_property.py + │ + ├── security/ + │ └── ir.model.access.csv + │ + ├── views/ + │ ├── menus.xml + │ └── real_estate_property_views.xml + │ + ├── static/ + │ ├── description/ + │ │ └── icon.png + │ │ + │ └── img/ + │ ├── country_house.png.png + │ ├── loft.png + │ └── mixed_use_commercial.png.png + │ + ├── __init__.py + └── __manifest__.py + + .. code-block:: python + :caption: `models/__init__.py` + + from . import real_estate_property + + .. code-block:: python + :caption: `__init__.py` + :emphasize-lines: 1 + + from . import models + + .. code-block:: xml + :caption: `data/real_estate_property_data.xml` + :emphasize-lines: 3,9,15 + + + [...] + + [...] + + + + [...] + + [...] + + + + [...] + + [...] + + + .. code-block:: xml + :caption: `data/real_estate_property_views.xml` + :emphasize-lines: 4-15 + + + + + + Properties + real.estate.property + {'search_default_filter_for_sale': True} + list,form + + +

+ Create a new property. +

+
+
+ + [...] + +
+ + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 2-10 + + 'data': [ + # Model data + 'data/real_estate_property_data.xml', + + # Security + 'security/ir.model.access.csv', + + # Views + 'views/real_estate_property_views.xml', + 'views/menus.xml', # Depends on `real_estate_property_views.xml` + ], + +.. _tutorials/server_framework_101/many2one: + +Many-to-one +=========== + +As promised at the end of :doc:`the previous chapter <03_build_user_interface>`, we'll now expand +our app's capabilities by adding new models to manage additional information. This expansion +naturally leads us to an important question: How will our `real.estate.property` model connect to +these new models? + +In relational databases, including Odoo's, **many-to-one relationships** play a crucial role. These +relationships allow you to link *multiple* records in one model to a *single* record in another +model. + +In Odoo, many-to-one relationships are established by adding a `Many2one` field to the model +representing the *many* side of the relationship. The field is represented in the database by a +`foreign key `_ that references the ID of the connected +record. By convention, `Many2one` field names end with the `_id` suffix, indicating that they store +the referenced record's ID. + +.. seealso:: + :ref:`Reference documentation for Many2one fields ` + +.. example:: + In the example below, the `Selection` field of the `product` model is replaced by a `Many2one` + field to create a more flexible and scalable model structure. + + .. code-block:: py + + from odoo import fields, models + + + class Product(models.Model): + _name = 'product' + _description = "Storable Product" + + [...] + category_id = fields.Many2one( + string="Category", comodel_name='product.category', ondelete='restrict', required=True + ) + + class ProductCategory(models.Model): + _name = 'product.category' + _description = "Product Category" + + name = fields.Char(string="Name") + + .. note:: + + - The relationship only needs to be declared on the *many* side to be established. + - The `ondelete` argument on the `Many2one` field defines what happens when the referenced + record is deleted. + +In our real estate app, we currently have a fixed set of property types. To increase flexibility, +let's replace the current `type` field with a many-to-one relationship to a separate model for +managing property types. + +.. exercise:: + + #. Create a new `real.estate.property.type` model. + + - Update the :file:`ir.model.access.csv` file to grant all database administrators access to + the model. + - Replace the dummy :guilabel:`Settings` menu item with a new :menuselection:`Configuration + --> Property Types` menu item. + - Create a window action to browse property types only in list view. + - Create the list view for property types. + - In a data file, describe at least as many default property types as the `type` field of the + `real.estate.property` model supports. + + #. Replace the `type` field on the `real.estate.property` model by a many-to-one relationship to + the `real.estate.property.type` model. Prevent deleting property types if a property + references them. + + .. tip:: + + - As the window action doesn't allow opening property types in form view, clicking the + :guilabel:`New` button does nothing. To allow editing records in-place, rely on the + reference documentation for :ref:`root attributes of list views + ` + - The server will throw an error at start-up because it can't require a value for the new, + currently empty field. To avoid fixing that manually in the database, run the command + :command:`dropdb tutorials` to delete the database and start from scratch. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property_type.py` + + from odoo import fields, models + from odoo.tools import date_utils + + + class RealEstatePropertyType(models.Model): + _name = 'real.estate.property.type' + _description = "Real Estate Property Type" + + name = fields.Char(string="Name", required=True) + + .. code-block:: py + :caption: `__init__.py` + :emphasize-lines: 2 + + from . import real_estate_property + from . import real_estate_property_type + + .. code-block:: csv + :caption: `ir.model.access.csv` + :emphasize-lines: 3 + + id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + real_estate_property_system,real.estate.property.system,model_real_estate_property,base.group_system,1,1,1,1 + real_estate_property_type_system,real.estate.property.type.system,model_real_estate_property_type,base.group_system,1,1,1,1 + + .. code-block:: xml + :caption: `menus.xml` + :emphasize-lines: 3-9 + + + + + + + + + .. code-block:: xml + :caption: `real_estate_property_type_views.xml` + + + + + + Property Types + real.estate.property.type + list + + + + Property Type List + real.estate.property.type + + + + + + + + + + .. code-block:: xml + :caption: `real_estate_property_type_data.xml` + + + + + + House + + + + Apartment + + + + Office Building + + + + Retail Space + + + + Warehouse + + + + + .. code-block:: py + :caption: `__manifest__.py` + :emphasize-lines: 3,4,7,9 + + 'data': [ + # Model data + 'data/real_estate_property_type_data.xml', + 'data/real_estate_property_data.xml', # Depends on `real_estate_property_type_data.xml` + [...] + # Views + 'views/real_estate_property_type_views.xml', + 'views/real_estate_property_views.xml', + 'views/menus.xml', # Depends on actions in views. + ], + + .. code-block:: py + :caption: `real_estate_property.py` + :emphasize-lines: 1-3 + + type_id = fields.Many2one( + string="Type", comodel_name='real.estate.property.type', ondelete='restrict', required=True + ) + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + :emphasize-lines: 5,14,27 + + + [...] + + [...] + + [...] + + [...] + + + + [...] + + + + + + + [...] + + + + [...] + + [...] + + + + [...] + + + .. code-block:: xml + :caption: `real_estate_property_data.xml` + :emphasize-lines: 3,9,15 + + + [...] + + [...] + + + + [...] + + [...] + + + + [...] + + [...] + + +.. _tutorials/server_framework_101/generic_models: + +Generic models +-------------- + +In the previous exercise, we created a many-to-one relationship with a custom model within the +`real_estate` module. However, Odoo provides several generic models that can extend your app's +capabilities without defining new models. These generic models are part of the default `base` module +and are typically prefixed with `res` or `ir`. + +Two frequently used models in Odoo are: + +- `res.users`: Represents user accounts in the database. They determine access rights to records and + can be `internal` (have access to the backend), `portal` (have access to the portal, e.g., to view + their invoices), or `public` (not logged in). +- `res.partner`: Represents physical or legal entities. They can be a company, an individual, or a + contact address. + +.. seealso:: + `The list of generic models in the base module <{GITHUB_PATH}/odoo/addons/base/models>`_ + +To make our real estate properties more informative, let's add two pieces of information: the seller +of the property and the salesperson managing the property. + +.. exercise:: + + #. Add the following fields to the `real.estate.property` model: + + - Seller (required): The person putting their property on sale; it can be any individual. + - Salesperson: The employee of the real estate agency overseeing the sale of the property. + + #. Modify the form view of properties to include a notebook component. The property description + should be in the first page, and the two new fields should be in the second page. + + .. tip:: + You don't need to define any new UI component to browse the seller you assigned to your + default properties! Just go to :menuselection:`Apps` and install the :guilabel:`Contacts` app. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 1-2 + + seller_id = fields.Many2one(string="Seller", comodel_name='res.partner', required=True) + salesperson_id = fields.Many2one(string="Salesperson", comodel_name='res.users') + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + :emphasize-lines: 3-18 + + + [...] + + + + + + + + + + + + + + [...] + + + .. code-block:: xml + :caption: `res_partner_data.xml` + + + + + + Bafien Carpink + + + + Antony Petisuix + + + + AmyFromTheVideos + + + + + .. code-block:: xml + :caption: `real_estate_property_data.xml` + :emphasize-lines: 3,8,13 + + + [...] + + + + + [...] + + + + + [...] + + + + .. code-block:: py + :caption: `__manifest__.py` + :emphasize-lines: 3,5,6 + + 'data': [ + # Model data + 'data/res_partner_data.xml', + 'data/real_estate_property_type_data.xml', + # Depends on `res_partner_data.xml`, `real_estate_property_type_data.xml` + 'data/real_estate_property_data.xml', + [...] + ], + +.. _tutorials/server_framework_101/one2many: + +One-to-many +=========== + +After exploring how to connect multiple records to a single one with many-to-one relationships, +let's consider their counterparts: **one-to-many relationships**. These relationships represent the +inverse of the many-to-one relationships we just discussed, enabling a *single* record in one model +to be associated with *multiple* records in another model. + +In Odoo, one-to-many relationships are established by adding a `One2many` field to the model +representing the *one* side of **an already existing** many-to-one relationship. It's important to +note that `One2many` fields don't store data in the database; instead, they provide a virtual field +that Odoo computes based on the referenced `Many2one` field. By convention, `One2many` field names +end with the `_ids` suffix, indicating that they allow accessing the IDs of the connected records. + +.. seealso:: + :ref:`Reference documentation for One2many fields ` + +.. example:: + In the example below, a `One2many` field is added to the `product.category` model to allow quick + access to the connected products from the product category. + + .. code-block:: py + + from odoo import fields, models + + + class Product(models.Model): + _name = 'product' + _description = "Storable Product" + + [...] + category_id = fields.Many2one( + string="Category", comodel_name='product.category', ondelete='restrict', required=True + ) + + class ProductCategory(models.Model): + _name = 'product.category' + _description = "Product Category" + + name = fields.Char(string="Name") + product_ids = fields.One2many( + string="Products", comodel_name='product', inverse_name='category_id' + ) + + .. note:: + + The `One2many` field must reference its `Many2one` counterpart through the `inverse_name` + argument. + +A good use case for a one-to-many relationship in our real estate app would be to connect properties +to a list of offers received from potential buyers. + +.. exercise:: + + #. Create a new `real.estate.offer` model. It should have the following fields: + + - Amount (required): The amount offered to buy the property. + - Buyer (required): The person making the offer. + - Date (required; default to creation date): When the offer was made. + - Validity (default to 7): The number of days before the offer expires. + - State (required): Either "Waiting", "Accepted", or "Refused". + + #. Create a list and form views for the `real.estate.offer` model. It's not necessary to create + menu items or actions, as offers will be accessible from properties, but feel free to do it + anyway! + #. Allow connecting properties to multiple offers. + #. Modify the form view of properties to display offers in a new notebook page titled "Offers". + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_offer.py` + + from odoo import fields, models + + + class RealEstateOffer(models.Model): + _name = 'real.estate.offer' + _description = "Real Estate Offer" + + amount = fields.Float(string="Amount", required=True) + buyer_id = fields.Many2one(string="Buyer", comodel_name='res.partner', required=True) + date = fields.Date(string="Date", required=True, default=fields.Date.today()) + validity = fields.Integer( + string="Validity", help="The number of days before the offer expires.", default=7 + ) + state = fields.Selection( + string="State", + selection=[ + ('waiting', "Waiting"), + ('accepted', "Accepted"), + ('refused', "Refused"), + ], + required=True, + default='waiting', + ) + property_id = fields.Many2one( + string="Property", comodel_name='real.estate.property', required=True + ) + + .. code-block:: python + :caption: `__init__.py` + :emphasize-lines: 1 + + from . import real_estate_offer + from . import real_estate_property + from . import real_estate_property_type + + .. code-block:: csv + :caption: `ir.model.access.csv` + :emphasize-lines: 2 + + id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + real_estate_offer_system,real.estate.offer.system,model_real_estate_offer,base.group_system,1,1,1,1 + real_estate_property_system,real.estate.property.system,model_real_estate_property,base.group_system,1,1,1,1 + real_estate_property_type_system,real.estate.property.type.system,model_real_estate_property_type,base.group_system,1,1,1,1 + + .. code-block:: xml + :caption: `real_estate_offer_views.xml` + + + + + + Offer List + real.estate.offer + + + + + + + + + + + + + Offer Form + real.estate.offer + +
+ + + + + + + + + + + + + +
+
+
+ +
+ + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 4 + + 'data': [ + [...] + # Views + 'views/real_estate_offer_views.xml', + 'views/real_estate_property_type_views.xml', + 'views/real_estate_property_views.xml', + 'views/menus.xml', # Depends on actions in views. + ], + + .. code-block:: py + :caption: `real_estate_property.py` + :emphasize-lines: 1-3 + + offer_ids = fields.One2many( + string="Offers", comodel_name='real.estate.offer', inverse_name='property_id' + ) + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + :emphasize-lines: 3-5 + + + [...] + + + + [...] + + +.. _tutorials/server_framework_101/many2many: + +Many-to-many +============ + +After the many-to-one and one-to-many relationships, let's consider a more complex use case: +**many-to-many relationships**. These relationships enable *multiple* records in one model to be +associated with *multiple* records in another model, creating a bidirectional connection between +sets of records. + +In Odoo, many-to-many relationships are established by adding a `Many2many` field to one or both of +the models. The server framework implements many-to-many relationships by automatically creating an +intermediate (junction) table in the database. This table stores pairs of IDs, each pair +representing a connection between a record of the first model and a record of the second model. By +convention, `Many2many` field names end with the `_ids` suffix, like for `One2many` fields. + +.. seealso:: + :ref:`Reference documentation for Many2many fields ` + +.. example:: + In the example below, a many-to-many relationship is established between the `product` model and + the `res.partner` model, which is used to represent sellers offering products for sale. + + .. code-block:: py + + from odoo import fields, models + + + class Product(models.Model): + _name = 'product' + _description = "Storable Product" + + [...] + seller_ids = fields.Many2many( + string="Sellers", + help="The sellers offering the product for sale.", + comodel_name='res.partner', + relation='product_seller_rel', + column1='product_id', + column2='partner_id', + ) + + .. note:: + + - It is not necessary to add a `Many2many` field to both models of the relationship. + - The optional `relation`, `column1`, and `column2` field arguments allow specifying the name + of the junction table and of its columns. + +Let's conclude this extension of the model family by allowing to associate multiple description tags +with each property. + +.. exercise:: + + #. Create a new `real.estate.tag` model. It should have the following fields: + + - Name (required): The label of the tag. + - Color: The color code to use for the tag, as an integer. + + #. In a data file, describe various default property tags. For example, "Renovated". + #. Create all necessary UI components to manage tags from the :guilabel:`Configuration` category + menu item. + #. Allow connecting properties to multiple tags, and tags to multiple properties. + #. Modify the form view of properties to display their associated tags. It should not be possible + to create new tags from the form view of properties. + + .. tip:: + Rely on the reference documentation for :ref:`the field component + ` in form views to find a nice display for property + tags. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `real_estate_tag.py` + + from odoo import fields, models + + + class RealEstateTag(models.Model): + _name = 'real.estate.tag' + _description = "Real Estate Tag" + + name = fields.Char(string="Label", required=True) + color = fields.Integer(string="Color") + + .. code-block:: python + :caption: `__init__.py` + :emphasize-lines: 4 + + from . import real_estate_offer + from . import real_estate_property + from . import real_estate_property_type + from . import real_estate_tag + + .. code-block:: csv + :caption: `ir.model.access.csv` + :emphasize-lines: 3 + + id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + [...] + real_estate_tag_system,real.estate.tag.system,model_real_estate_tag,base.group_system,1,1,1,1 + + .. code-block:: xml + :caption: `real_estate_tag_data.xml` + + + + + + Eco Passive + 1 + + + + Modern + 2 + + + + Renovated + 3 + + + + Rural + 4 + + + + Suburban + 5 + + + + Urban + 6 + + + + Waterfront + 7 + + + + + .. code-block:: xml + :caption: `menus.xml` + :emphasize-lines: 3-7 + + + [...] + + + + .. code-block:: xml + :caption: `real_estate_tag_views.xml` + + + + + + Tags + real.estate.tag + list + + + + Tag List + real.estate.tag + + + + + + + + + + + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 3,5 + + 'data': [ + [...] + 'data/real_estate_tag_data.xml', + [...] + 'views/real_estate_tag_views.xml', + [...] + ], + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 1 + + tag_ids = fields.Many2many(string="Tags", comodel_name='real.estate.tag') + + .. code-block:: xml + :caption: `real_estate_property_views.xml` + :emphasize-lines: 3-7 + + + [...] + + [...] + + +---- + +Congratulations! You've learned the art of forging connections between your Odoo models. You're now +well-equipped to build complex, interconnected data structures. In the next chapter, we'll +:doc:`add custom business logic to the models <05_connect_the_dots>`, turning your application from +a simple data management tool into a smart, automated system that can handle complex business +processes. diff --git a/content/developer/tutorials/server_framework_101/05_business_logic.rst b/content/developer/tutorials/server_framework_101/05_business_logic.rst new file mode 100644 index 000000000..79255bfb2 --- /dev/null +++ b/content/developer/tutorials/server_framework_101/05_business_logic.rst @@ -0,0 +1,35 @@ +========================= +Chapter 5: Business logic +========================= + +tmp + +.. todo: constraints, defaults, onchanges, computes +.. todo: model actions ("assign myself as salesperson" action, "view offers" statbutton) +.. todo: explain the env (self.env.cr, self.env.uid, self.env.user, self.env.context, self.env.ref(xml_id), self.env[model_name]) +.. todo: explain the thing about `self` +.. todo: explain magic commands +.. todo: copy=False on some fields +.. todo: introduce lambda functions for defaults :point_down: + +There is a problem with the way we defined our Date fields: their default value relies on +:code:`fields.Date.today()` or some other static method. When the code is loaded into memory, the +date is computed once and reused for all newly created records until the server is shut down. You +probably didn't notice it, unless you kept your server running for several days, but it would be +much more visible with Datetime fields, as all newly created records would share the same timestamp. + +That's where lambda functions come in handy. As they generate an anonymous function each time +they're evaluated at runtime, they can be used in the computation of default field values to return +an updated value for each new record. + +.. todo: salesperson_id = fields.Many2one(default=lambda self: self.env.user) +.. todo: real.estate.offer.amount::default -> property.selling_price +.. todo: real.estate.tag.color -> default=_default_color ; def _default_color(self): return random.randint(1, 11) +.. todo: 6,0,0 to associate tags to properties in data +.. todo: unique tag + +.. todo: odoo-bin shell section + +---- + +.. todo: add incentive for chapter 6 diff --git a/content/developer/tutorials/server_framework_101/06_security.rst b/content/developer/tutorials/server_framework_101/06_security.rst new file mode 100644 index 000000000..ced1d4f2d --- /dev/null +++ b/content/developer/tutorials/server_framework_101/06_security.rst @@ -0,0 +1,9 @@ +=================== +Chapter 6: Security +=================== + +tmp + +---- + +.. 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 new file mode 100644 index 000000000..1e2021cac --- /dev/null +++ b/content/developer/tutorials/server_framework_101/07_advanced_views.rst @@ -0,0 +1,15 @@ +========================= +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: add incentive for next chapter diff --git a/content/developer/tutorials/server_framework_101/08_inheritance.rst b/content/developer/tutorials/server_framework_101/08_inheritance.rst new file mode 100644 index 000000000..bc78cf63c --- /dev/null +++ b/content/developer/tutorials/server_framework_101/08_inheritance.rst @@ -0,0 +1,11 @@ +====================== +Chapter 8: Inheritance +====================== + +tmp + +.. todo:: inherit from mail.tread mixin and add a chatter + +---- + +.. todo: add incentive for next chapter diff --git a/content/developer/tutorials/server_framework_101/09_portal.rst b/content/developer/tutorials/server_framework_101/09_portal.rst new file mode 100644 index 000000000..e7d95f179 --- /dev/null +++ b/content/developer/tutorials/server_framework_101/09_portal.rst @@ -0,0 +1,9 @@ +================= +Chapter 9: Portal +================= + +Controllers + QWeb + +---- + +.. todo: add incentive for next chapter diff --git a/content/developer/tutorials/server_framework_101/10_unit_testing.rst b/content/developer/tutorials/server_framework_101/10_unit_testing.rst new file mode 100644 index 000000000..e2e66e0b9 --- /dev/null +++ b/content/developer/tutorials/server_framework_101/10_unit_testing.rst @@ -0,0 +1,9 @@ +======================== +Chapter 10: Unit testing +======================== + +tmp + +---- + +.. todo: add incentive for next chapter diff --git a/content/developer/tutorials/server_framework_101_legacy.rst b/content/developer/tutorials/server_framework_101_legacy.rst new file mode 100644 index 000000000..27e55ebb9 --- /dev/null +++ b/content/developer/tutorials/server_framework_101_legacy.rst @@ -0,0 +1,48 @@ +:show-content: +:orphan: + +============================= +Server framework 101 (legacy) +============================= + +.. danger:: + This tutorial is outdated. We recommend reading :doc:`server_framework_101` instead. + +.. toctree:: + :titlesonly: + :glob: + + server_framework_101_legacy/* + +Welcome to the Server framework 101 tutorial! If you reached this page that means you are +interested in the development of your own Odoo module. It might also mean that you recently +joined the Odoo company for a rather technical position. In any case, your journey to the +technical side of Odoo starts here. + +The goal of this tutorial is for you to get an insight of the most important parts of the Odoo +development framework while developing your own Odoo module to manage real estate assets. The +chapters should be followed in their given order since they cover the development of a new Odoo +application from scratch in an incremental way. In other words, each chapter depends on the previous +one. + +.. important:: + Before going further, make sure you have prepared your development environment with the + :doc:`setup guide `. + +Ready? Let's get started! + +* :doc:`server_framework_101_legacy/01_architecture` +* :doc:`server_framework_101_legacy/02_newapp` +* :doc:`server_framework_101_legacy/03_basicmodel` +* :doc:`server_framework_101_legacy/04_securityintro` +* :doc:`server_framework_101_legacy/05_firstui` +* :doc:`server_framework_101_legacy/06_basicviews` +* :doc:`server_framework_101_legacy/07_relations` +* :doc:`server_framework_101_legacy/08_compute_onchange` +* :doc:`server_framework_101_legacy/09_actions` +* :doc:`server_framework_101_legacy/10_constraints` +* :doc:`server_framework_101_legacy/11_sprinkles` +* :doc:`server_framework_101_legacy/12_inheritance` +* :doc:`server_framework_101_legacy/13_other_module` +* :doc:`server_framework_101_legacy/14_qwebintro` +* :doc:`server_framework_101_legacy/15_final_word` diff --git a/content/developer/tutorials/server_framework_101/01_architecture.rst b/content/developer/tutorials/server_framework_101_legacy/01_architecture.rst similarity index 95% rename from content/developer/tutorials/server_framework_101/01_architecture.rst rename to content/developer/tutorials/server_framework_101_legacy/01_architecture.rst index 5c5f3ca6f..cdebf629d 100644 --- a/content/developer/tutorials/server_framework_101/01_architecture.rst +++ b/content/developer/tutorials/server_framework_101_legacy/01_architecture.rst @@ -1,9 +1,15 @@ -.. _tutorials/server_framework_101/01_architecture: +.. _tutorials/server_framework_101_legacy/01_architecture: ================================ Chapter 1: Architecture Overview ================================ +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + Multitier application ===================== diff --git a/content/developer/tutorials/server_framework_101_legacy/01_architecture/three_tier.svg b/content/developer/tutorials/server_framework_101_legacy/01_architecture/three_tier.svg new file mode 100644 index 000000000..5c98f57e0 --- /dev/null +++ b/content/developer/tutorials/server_framework_101_legacy/01_architecture/three_tier.svg @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Database + Storage + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + >GET SALES TOTAL + >GET SALES TOTAL + GET LIST OF ALLSALES MADELAST YEAR + ADD ALL SALESTOGETHER + 4 TOTAL SALES + QUERY + SALE 1SALE 2SALE 3SALE 4 + + + Data tier + Presentation tier + Logic tier + This layer coordinates the application, processes commands, makes logical decisions and evaluations, and performs calculations. It also moves and processes data between the two surrounding layers. + Here information is stored and retrieved from a database or file system. The information is then passed back to the logic tier for processing, and then eventually back to the user. + The top-most level of the applicationis the user interface. The main functionof the interface is to translate tasks and results to something the user can understand. + + \ No newline at end of file diff --git a/content/developer/tutorials/server_framework_101/02_newapp.rst b/content/developer/tutorials/server_framework_101_legacy/02_newapp.rst similarity index 95% rename from content/developer/tutorials/server_framework_101/02_newapp.rst rename to content/developer/tutorials/server_framework_101_legacy/02_newapp.rst index 31e02dcfd..70d1ffaec 100644 --- a/content/developer/tutorials/server_framework_101/02_newapp.rst +++ b/content/developer/tutorials/server_framework_101_legacy/02_newapp.rst @@ -2,6 +2,12 @@ Chapter 2: A New Application ============================ +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + The purpose of this chapter is to lay the foundation for the creation of a completely new Odoo module. We will start from scratch with the minimum needed to have our module recognized by Odoo. In the upcoming chapters, we will progressively add features to build a realistic business case. diff --git a/content/developer/tutorials/server_framework_101/02_newapp/app_in_list.png b/content/developer/tutorials/server_framework_101_legacy/02_newapp/app_in_list.png similarity index 100% rename from content/developer/tutorials/server_framework_101/02_newapp/app_in_list.png rename to content/developer/tutorials/server_framework_101_legacy/02_newapp/app_in_list.png diff --git a/content/developer/tutorials/server_framework_101/02_newapp/overview_form_view_01.png b/content/developer/tutorials/server_framework_101_legacy/02_newapp/overview_form_view_01.png similarity index 100% rename from content/developer/tutorials/server_framework_101/02_newapp/overview_form_view_01.png rename to content/developer/tutorials/server_framework_101_legacy/02_newapp/overview_form_view_01.png diff --git a/content/developer/tutorials/server_framework_101/02_newapp/overview_form_view_02.png b/content/developer/tutorials/server_framework_101_legacy/02_newapp/overview_form_view_02.png similarity index 100% rename from content/developer/tutorials/server_framework_101/02_newapp/overview_form_view_02.png rename to content/developer/tutorials/server_framework_101_legacy/02_newapp/overview_form_view_02.png diff --git a/content/developer/tutorials/server_framework_101/02_newapp/overview_list_view_01.png b/content/developer/tutorials/server_framework_101_legacy/02_newapp/overview_list_view_01.png similarity index 100% rename from content/developer/tutorials/server_framework_101/02_newapp/overview_list_view_01.png rename to content/developer/tutorials/server_framework_101_legacy/02_newapp/overview_list_view_01.png diff --git a/content/developer/tutorials/server_framework_101/03_basicmodel.rst b/content/developer/tutorials/server_framework_101_legacy/03_basicmodel.rst similarity index 98% rename from content/developer/tutorials/server_framework_101/03_basicmodel.rst rename to content/developer/tutorials/server_framework_101_legacy/03_basicmodel.rst index fcdbceaeb..ae823f3ec 100644 --- a/content/developer/tutorials/server_framework_101/03_basicmodel.rst +++ b/content/developer/tutorials/server_framework_101_legacy/03_basicmodel.rst @@ -2,6 +2,12 @@ Chapter 3: Models And Basic Fields ================================== +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + At the end of the :doc:`previous chapter <02_newapp>`, we were able to create an Odoo module. However, at this point it is still an empty shell which doesn't allow us to store any data. In our real estate module, we want to store the information related to the diff --git a/content/developer/tutorials/server_framework_101/04_securityintro.rst b/content/developer/tutorials/server_framework_101_legacy/04_securityintro.rst similarity index 96% rename from content/developer/tutorials/server_framework_101/04_securityintro.rst rename to content/developer/tutorials/server_framework_101_legacy/04_securityintro.rst index 5c4c2317f..47af31398 100644 --- a/content/developer/tutorials/server_framework_101/04_securityintro.rst +++ b/content/developer/tutorials/server_framework_101_legacy/04_securityintro.rst @@ -2,6 +2,12 @@ Chapter 4: Security - A Brief Introduction ========================================== +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + In the :doc:`previous chapter <03_basicmodel>`, we created our first table intended to store business data. In a business application such as Odoo, one of the first questions to consider is who\ [#who]_ can access the data. Odoo provides a security mechanism to allow access diff --git a/content/developer/tutorials/server_framework_101/05_firstui.rst b/content/developer/tutorials/server_framework_101_legacy/05_firstui.rst similarity index 98% rename from content/developer/tutorials/server_framework_101/05_firstui.rst rename to content/developer/tutorials/server_framework_101_legacy/05_firstui.rst index f79e0d0bc..ae81a9765 100644 --- a/content/developer/tutorials/server_framework_101/05_firstui.rst +++ b/content/developer/tutorials/server_framework_101_legacy/05_firstui.rst @@ -2,6 +2,12 @@ Chapter 5: Finally, Some UI To Play With ======================================== +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + Now that we've created our new :doc:`model <03_basicmodel>` and its corresponding :doc:`access rights <04_securityintro>`, it is time to interact with the user interface. diff --git a/content/developer/tutorials/server_framework_101/05_firstui/attribute_and_default.gif b/content/developer/tutorials/server_framework_101_legacy/05_firstui/attribute_and_default.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/05_firstui/attribute_and_default.gif rename to content/developer/tutorials/server_framework_101_legacy/05_firstui/attribute_and_default.gif diff --git a/content/developer/tutorials/server_framework_101/05_firstui/estate_form_default.png b/content/developer/tutorials/server_framework_101_legacy/05_firstui/estate_form_default.png similarity index 100% rename from content/developer/tutorials/server_framework_101/05_firstui/estate_form_default.png rename to content/developer/tutorials/server_framework_101_legacy/05_firstui/estate_form_default.png diff --git a/content/developer/tutorials/server_framework_101/05_firstui/estate_menu_action.png b/content/developer/tutorials/server_framework_101_legacy/05_firstui/estate_menu_action.png similarity index 100% rename from content/developer/tutorials/server_framework_101/05_firstui/estate_menu_action.png rename to content/developer/tutorials/server_framework_101_legacy/05_firstui/estate_menu_action.png diff --git a/content/developer/tutorials/server_framework_101/05_firstui/estate_menu_root.png b/content/developer/tutorials/server_framework_101_legacy/05_firstui/estate_menu_root.png similarity index 100% rename from content/developer/tutorials/server_framework_101/05_firstui/estate_menu_root.png rename to content/developer/tutorials/server_framework_101_legacy/05_firstui/estate_menu_root.png diff --git a/content/developer/tutorials/server_framework_101/05_firstui/inactive.gif b/content/developer/tutorials/server_framework_101_legacy/05_firstui/inactive.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/05_firstui/inactive.gif rename to content/developer/tutorials/server_framework_101_legacy/05_firstui/inactive.gif diff --git a/content/developer/tutorials/server_framework_101/05_firstui/menu_01.png b/content/developer/tutorials/server_framework_101_legacy/05_firstui/menu_01.png similarity index 100% rename from content/developer/tutorials/server_framework_101/05_firstui/menu_01.png rename to content/developer/tutorials/server_framework_101_legacy/05_firstui/menu_01.png diff --git a/content/developer/tutorials/server_framework_101/05_firstui/menu_02.png b/content/developer/tutorials/server_framework_101_legacy/05_firstui/menu_02.png similarity index 100% rename from content/developer/tutorials/server_framework_101/05_firstui/menu_02.png rename to content/developer/tutorials/server_framework_101_legacy/05_firstui/menu_02.png diff --git a/content/developer/tutorials/server_framework_101/06_basicviews.rst b/content/developer/tutorials/server_framework_101_legacy/06_basicviews.rst similarity index 97% rename from content/developer/tutorials/server_framework_101/06_basicviews.rst rename to content/developer/tutorials/server_framework_101_legacy/06_basicviews.rst index f7f027b08..18edd5aad 100644 --- a/content/developer/tutorials/server_framework_101/06_basicviews.rst +++ b/content/developer/tutorials/server_framework_101_legacy/06_basicviews.rst @@ -2,6 +2,12 @@ Chapter 6: Basic Views ====================== +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + We have seen in the :doc:`previous chapter <05_firstui>` that Odoo is able to generate default views for a given model. In practice, the default view is **never** acceptable for a business application. Instead, we should at least organize the various fields in a logical diff --git a/content/developer/tutorials/server_framework_101/06_basicviews/form.png b/content/developer/tutorials/server_framework_101_legacy/06_basicviews/form.png similarity index 100% rename from content/developer/tutorials/server_framework_101/06_basicviews/form.png rename to content/developer/tutorials/server_framework_101_legacy/06_basicviews/form.png diff --git a/content/developer/tutorials/server_framework_101/06_basicviews/list.png b/content/developer/tutorials/server_framework_101_legacy/06_basicviews/list.png similarity index 100% rename from content/developer/tutorials/server_framework_101/06_basicviews/list.png rename to content/developer/tutorials/server_framework_101_legacy/06_basicviews/list.png diff --git a/content/developer/tutorials/server_framework_101/06_basicviews/search_01.png b/content/developer/tutorials/server_framework_101_legacy/06_basicviews/search_01.png similarity index 100% rename from content/developer/tutorials/server_framework_101/06_basicviews/search_01.png rename to content/developer/tutorials/server_framework_101_legacy/06_basicviews/search_01.png diff --git a/content/developer/tutorials/server_framework_101/06_basicviews/search_02.png b/content/developer/tutorials/server_framework_101_legacy/06_basicviews/search_02.png similarity index 100% rename from content/developer/tutorials/server_framework_101/06_basicviews/search_02.png rename to content/developer/tutorials/server_framework_101_legacy/06_basicviews/search_02.png diff --git a/content/developer/tutorials/server_framework_101/06_basicviews/search_03.png b/content/developer/tutorials/server_framework_101_legacy/06_basicviews/search_03.png similarity index 100% rename from content/developer/tutorials/server_framework_101/06_basicviews/search_03.png rename to content/developer/tutorials/server_framework_101_legacy/06_basicviews/search_03.png diff --git a/content/developer/tutorials/server_framework_101/07_relations.rst b/content/developer/tutorials/server_framework_101_legacy/07_relations.rst similarity index 98% rename from content/developer/tutorials/server_framework_101/07_relations.rst rename to content/developer/tutorials/server_framework_101_legacy/07_relations.rst index 1d2130d83..86384d260 100644 --- a/content/developer/tutorials/server_framework_101/07_relations.rst +++ b/content/developer/tutorials/server_framework_101_legacy/07_relations.rst @@ -2,6 +2,12 @@ Chapter 7: Relations Between Models =================================== +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + The :doc:`previous chapter <06_basicviews>` covered the creation of custom views for a model containing basic fields. However, in any real business scenario we need more than one model. Moreover, links between models are necessary. One can easily imagine one model containing diff --git a/content/developer/tutorials/server_framework_101/07_relations/property_many2many.png b/content/developer/tutorials/server_framework_101_legacy/07_relations/property_many2many.png similarity index 100% rename from content/developer/tutorials/server_framework_101/07_relations/property_many2many.png rename to content/developer/tutorials/server_framework_101_legacy/07_relations/property_many2many.png diff --git a/content/developer/tutorials/server_framework_101/07_relations/property_many2one.png b/content/developer/tutorials/server_framework_101_legacy/07_relations/property_many2one.png similarity index 100% rename from content/developer/tutorials/server_framework_101/07_relations/property_many2one.png rename to content/developer/tutorials/server_framework_101_legacy/07_relations/property_many2one.png diff --git a/content/developer/tutorials/server_framework_101/07_relations/property_offer.png b/content/developer/tutorials/server_framework_101_legacy/07_relations/property_offer.png similarity index 100% rename from content/developer/tutorials/server_framework_101/07_relations/property_offer.png rename to content/developer/tutorials/server_framework_101_legacy/07_relations/property_offer.png diff --git a/content/developer/tutorials/server_framework_101/07_relations/property_tag.png b/content/developer/tutorials/server_framework_101_legacy/07_relations/property_tag.png similarity index 100% rename from content/developer/tutorials/server_framework_101/07_relations/property_tag.png rename to content/developer/tutorials/server_framework_101_legacy/07_relations/property_tag.png diff --git a/content/developer/tutorials/server_framework_101/07_relations/property_type.png b/content/developer/tutorials/server_framework_101_legacy/07_relations/property_type.png similarity index 100% rename from content/developer/tutorials/server_framework_101/07_relations/property_type.png rename to content/developer/tutorials/server_framework_101_legacy/07_relations/property_type.png diff --git a/content/developer/tutorials/server_framework_101/08_compute_onchange.rst b/content/developer/tutorials/server_framework_101_legacy/08_compute_onchange.rst similarity index 97% rename from content/developer/tutorials/server_framework_101/08_compute_onchange.rst rename to content/developer/tutorials/server_framework_101_legacy/08_compute_onchange.rst index a5ac62f5d..33d4c4a51 100644 --- a/content/developer/tutorials/server_framework_101/08_compute_onchange.rst +++ b/content/developer/tutorials/server_framework_101_legacy/08_compute_onchange.rst @@ -2,6 +2,12 @@ Chapter 8: Computed Fields And Onchanges ======================================== +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + The :doc:`relations between models <07_relations>` are a key component of any Odoo module. They are necessary for the modelization of any business case. However, we may want links between the fields within a given model. Sometimes the value of one field is determined from @@ -50,7 +56,7 @@ method should set the value of the computed field for every record in By convention, :attr:`~odoo.fields.Field.compute` methods are private, meaning that they cannot be called from the presentation tier, only from the business tier (see -:ref:`tutorials/server_framework_101/01_architecture`). Private methods have a name starting with an +:ref:`tutorials/server_framework_101_legacy/01_architecture`). Private methods have a name starting with an underscore ``_``. Dependencies diff --git a/content/developer/tutorials/server_framework_101/08_compute_onchange/compute.gif b/content/developer/tutorials/server_framework_101_legacy/08_compute_onchange/compute.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/08_compute_onchange/compute.gif rename to content/developer/tutorials/server_framework_101_legacy/08_compute_onchange/compute.gif diff --git a/content/developer/tutorials/server_framework_101/08_compute_onchange/compute_inverse.gif b/content/developer/tutorials/server_framework_101_legacy/08_compute_onchange/compute_inverse.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/08_compute_onchange/compute_inverse.gif rename to content/developer/tutorials/server_framework_101_legacy/08_compute_onchange/compute_inverse.gif diff --git a/content/developer/tutorials/server_framework_101/08_compute_onchange/onchange.gif b/content/developer/tutorials/server_framework_101_legacy/08_compute_onchange/onchange.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/08_compute_onchange/onchange.gif rename to content/developer/tutorials/server_framework_101_legacy/08_compute_onchange/onchange.gif diff --git a/content/developer/tutorials/server_framework_101/09_actions.rst b/content/developer/tutorials/server_framework_101_legacy/09_actions.rst similarity index 96% rename from content/developer/tutorials/server_framework_101/09_actions.rst rename to content/developer/tutorials/server_framework_101_legacy/09_actions.rst index 430965192..42a5be5f7 100644 --- a/content/developer/tutorials/server_framework_101/09_actions.rst +++ b/content/developer/tutorials/server_framework_101_legacy/09_actions.rst @@ -2,6 +2,12 @@ Chapter 9: Ready For Some Action? ================================= +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + So far we have mostly built our module by declaring fields and views. We just introduced business logic in the :doc:`previous chapter <08_compute_onchange>` thanks to computed fields and onchanges. In any real business scenario, we would want to link some business diff --git a/content/developer/tutorials/server_framework_101/09_actions/offer_01.gif b/content/developer/tutorials/server_framework_101_legacy/09_actions/offer_01.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/09_actions/offer_01.gif rename to content/developer/tutorials/server_framework_101_legacy/09_actions/offer_01.gif diff --git a/content/developer/tutorials/server_framework_101/09_actions/offer_02.gif b/content/developer/tutorials/server_framework_101_legacy/09_actions/offer_02.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/09_actions/offer_02.gif rename to content/developer/tutorials/server_framework_101_legacy/09_actions/offer_02.gif diff --git a/content/developer/tutorials/server_framework_101/09_actions/property.gif b/content/developer/tutorials/server_framework_101_legacy/09_actions/property.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/09_actions/property.gif rename to content/developer/tutorials/server_framework_101_legacy/09_actions/property.gif diff --git a/content/developer/tutorials/server_framework_101/10_constraints.rst b/content/developer/tutorials/server_framework_101_legacy/10_constraints.rst similarity index 96% rename from content/developer/tutorials/server_framework_101/10_constraints.rst rename to content/developer/tutorials/server_framework_101_legacy/10_constraints.rst index b6f5f771e..7f25c5631 100644 --- a/content/developer/tutorials/server_framework_101/10_constraints.rst +++ b/content/developer/tutorials/server_framework_101_legacy/10_constraints.rst @@ -2,6 +2,12 @@ Chapter 10: Constraints ======================= +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + The :doc:`previous chapter <09_actions>` introduced the ability to add some business logic to our model. We can now link buttons to business code, but how can we prevent users from entering incorrect data? For example, in our real estate module nothing prevents diff --git a/content/developer/tutorials/server_framework_101/10_constraints/python.gif b/content/developer/tutorials/server_framework_101_legacy/10_constraints/python.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/10_constraints/python.gif rename to content/developer/tutorials/server_framework_101_legacy/10_constraints/python.gif diff --git a/content/developer/tutorials/server_framework_101/10_constraints/sql_01.gif b/content/developer/tutorials/server_framework_101_legacy/10_constraints/sql_01.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/10_constraints/sql_01.gif rename to content/developer/tutorials/server_framework_101_legacy/10_constraints/sql_01.gif diff --git a/content/developer/tutorials/server_framework_101/10_constraints/sql_02.gif b/content/developer/tutorials/server_framework_101_legacy/10_constraints/sql_02.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/10_constraints/sql_02.gif rename to content/developer/tutorials/server_framework_101_legacy/10_constraints/sql_02.gif diff --git a/content/developer/tutorials/server_framework_101/11_sprinkles.rst b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles.rst similarity index 99% rename from content/developer/tutorials/server_framework_101/11_sprinkles.rst rename to content/developer/tutorials/server_framework_101_legacy/11_sprinkles.rst index f7116fd92..0ea52b145 100644 --- a/content/developer/tutorials/server_framework_101/11_sprinkles.rst +++ b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles.rst @@ -2,6 +2,12 @@ Chapter 11: Add The Sprinkles ============================= +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + Our real estate module now makes sense from a business perspective. We created :doc:`specific views <06_basicviews>`, added several :doc:`action buttons <09_actions>` and diff --git a/content/developer/tutorials/server_framework_101/11_sprinkles/decoration.png b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles/decoration.png similarity index 100% rename from content/developer/tutorials/server_framework_101/11_sprinkles/decoration.png rename to content/developer/tutorials/server_framework_101_legacy/11_sprinkles/decoration.png diff --git a/content/developer/tutorials/server_framework_101/11_sprinkles/editable_list.gif b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles/editable_list.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/11_sprinkles/editable_list.gif rename to content/developer/tutorials/server_framework_101_legacy/11_sprinkles/editable_list.gif diff --git a/content/developer/tutorials/server_framework_101/11_sprinkles/form.gif b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles/form.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/11_sprinkles/form.gif rename to content/developer/tutorials/server_framework_101_legacy/11_sprinkles/form.gif diff --git a/content/developer/tutorials/server_framework_101/11_sprinkles/inline_view.png b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles/inline_view.png similarity index 100% rename from content/developer/tutorials/server_framework_101/11_sprinkles/inline_view.png rename to content/developer/tutorials/server_framework_101_legacy/11_sprinkles/inline_view.png diff --git a/content/developer/tutorials/server_framework_101/11_sprinkles/search.gif b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles/search.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/11_sprinkles/search.gif rename to content/developer/tutorials/server_framework_101_legacy/11_sprinkles/search.gif diff --git a/content/developer/tutorials/server_framework_101/11_sprinkles/stat_button.gif b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles/stat_button.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/11_sprinkles/stat_button.gif rename to content/developer/tutorials/server_framework_101_legacy/11_sprinkles/stat_button.gif diff --git a/content/developer/tutorials/server_framework_101/11_sprinkles/widget.png b/content/developer/tutorials/server_framework_101_legacy/11_sprinkles/widget.png similarity index 100% rename from content/developer/tutorials/server_framework_101/11_sprinkles/widget.png rename to content/developer/tutorials/server_framework_101_legacy/11_sprinkles/widget.png diff --git a/content/developer/tutorials/server_framework_101/12_inheritance.rst b/content/developer/tutorials/server_framework_101_legacy/12_inheritance.rst similarity index 98% rename from content/developer/tutorials/server_framework_101/12_inheritance.rst rename to content/developer/tutorials/server_framework_101_legacy/12_inheritance.rst index 4194546a3..aa25558a9 100644 --- a/content/developer/tutorials/server_framework_101/12_inheritance.rst +++ b/content/developer/tutorials/server_framework_101_legacy/12_inheritance.rst @@ -2,6 +2,12 @@ Chapter 12: Inheritance ======================= +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + A powerful aspect of Odoo is its modularity. A module is dedicated to a business need, but modules can also interact with one another. This is useful for extending the functionality of an existing module. For example, in our real estate scenario we want to display the list of a salesperson's properties diff --git a/content/developer/tutorials/server_framework_101/12_inheritance/create.gif b/content/developer/tutorials/server_framework_101_legacy/12_inheritance/create.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/12_inheritance/create.gif rename to content/developer/tutorials/server_framework_101_legacy/12_inheritance/create.gif diff --git a/content/developer/tutorials/server_framework_101/12_inheritance/inheritance_methods.png b/content/developer/tutorials/server_framework_101_legacy/12_inheritance/inheritance_methods.png similarity index 100% rename from content/developer/tutorials/server_framework_101/12_inheritance/inheritance_methods.png rename to content/developer/tutorials/server_framework_101_legacy/12_inheritance/inheritance_methods.png diff --git a/content/developer/tutorials/server_framework_101/12_inheritance/unlink.gif b/content/developer/tutorials/server_framework_101_legacy/12_inheritance/unlink.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/12_inheritance/unlink.gif rename to content/developer/tutorials/server_framework_101_legacy/12_inheritance/unlink.gif diff --git a/content/developer/tutorials/server_framework_101/12_inheritance/users.png b/content/developer/tutorials/server_framework_101_legacy/12_inheritance/users.png similarity index 100% rename from content/developer/tutorials/server_framework_101/12_inheritance/users.png rename to content/developer/tutorials/server_framework_101_legacy/12_inheritance/users.png diff --git a/content/developer/tutorials/server_framework_101/13_other_module.rst b/content/developer/tutorials/server_framework_101_legacy/13_other_module.rst similarity index 96% rename from content/developer/tutorials/server_framework_101/13_other_module.rst rename to content/developer/tutorials/server_framework_101_legacy/13_other_module.rst index f32e4a4f7..ae302b0ad 100644 --- a/content/developer/tutorials/server_framework_101/13_other_module.rst +++ b/content/developer/tutorials/server_framework_101_legacy/13_other_module.rst @@ -2,6 +2,12 @@ Chapter 13: Interact With Other Modules ======================================= +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + In the :doc:`previous chapter <12_inheritance>`, we used inheritance to modify the behavior of a module. In our real estate scenario, we would like to go a step further and be able to generate invoices for our customers. Odoo provides an Invoicing module, so it @@ -47,7 +53,7 @@ When the ``estate_account`` module appears in the list, go ahead and install it! the Invoicing application is installed as well. This is expected since your module depends on it. If you uninstall the Invoicing application, your module will be uninstalled as well. -.. _tutorials/server_framework_101/13_other_module/create: +.. _tutorials/server_framework_101_legacy/13_other_module/create: Invoice Creation ---------------- diff --git a/content/developer/tutorials/server_framework_101/13_other_module/create_inv.gif b/content/developer/tutorials/server_framework_101_legacy/13_other_module/create_inv.gif similarity index 100% rename from content/developer/tutorials/server_framework_101/13_other_module/create_inv.gif rename to content/developer/tutorials/server_framework_101_legacy/13_other_module/create_inv.gif diff --git a/content/developer/tutorials/server_framework_101/14_qwebintro.rst b/content/developer/tutorials/server_framework_101_legacy/14_qwebintro.rst similarity index 96% rename from content/developer/tutorials/server_framework_101/14_qwebintro.rst rename to content/developer/tutorials/server_framework_101_legacy/14_qwebintro.rst index 5a3b3416f..28d63e3ec 100644 --- a/content/developer/tutorials/server_framework_101/14_qwebintro.rst +++ b/content/developer/tutorials/server_framework_101_legacy/14_qwebintro.rst @@ -2,6 +2,12 @@ Chapter 14: A Brief History Of QWeb =================================== +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + So far the interface design of our real estate module has been rather limited. Building a list view is straightforward since only the list of fields is necessary. The same holds true for the form view: despite the use of a few tags such as ```` or ````, there diff --git a/content/developer/tutorials/server_framework_101/14_qwebintro/kanban.png b/content/developer/tutorials/server_framework_101_legacy/14_qwebintro/kanban.png similarity index 100% rename from content/developer/tutorials/server_framework_101/14_qwebintro/kanban.png rename to content/developer/tutorials/server_framework_101_legacy/14_qwebintro/kanban.png diff --git a/content/developer/tutorials/server_framework_101/15_final_word.rst b/content/developer/tutorials/server_framework_101_legacy/15_final_word.rst similarity index 86% rename from content/developer/tutorials/server_framework_101/15_final_word.rst rename to content/developer/tutorials/server_framework_101_legacy/15_final_word.rst index 57ae22961..9493df132 100644 --- a/content/developer/tutorials/server_framework_101/15_final_word.rst +++ b/content/developer/tutorials/server_framework_101_legacy/15_final_word.rst @@ -2,6 +2,12 @@ Chapter 15: The final word ========================== +.. danger:: + This tutorial is outdated. We recommend reading :doc:`../server_framework_101` instead. + +.. seealso:: + :doc:`Homepage of the tutorial <../server_framework_101_legacy>` + Coding guidelines ================= diff --git a/content/developer/tutorials/setup_guide.rst b/content/developer/tutorials/setup_guide.rst index a1d56720d..0d2b59082 100644 --- a/content/developer/tutorials/setup_guide.rst +++ b/content/developer/tutorials/setup_guide.rst @@ -10,8 +10,10 @@ Odoo community and Odoo employees alike, the preferred way is to perform a sourc Follow the :ref:`contributing/development/setup` section of the contributing guide to prepare your environment for pushing local changes to the Odoo repositories. -Adapt the environment for the tutorials -======================================= +.. _tutorials/setup_guide/adapt_env: + +Adapt the environment to the tutorials +====================================== By now, you should have downloaded the source code into two local repositories, one for `odoo/odoo` and one for `odoo/enterprise`. These repositories are set up to push changes to pre-defined @@ -29,10 +31,11 @@ will be part of the `addons-path` that references all directories containing Odo .. code-block:: console - $ git clone git@github.com:odoo/tutorials.git + $ git clone --branch {BRANCH} --single-branch git@github.com:odoo/tutorials.git #. Configure your fork and Git to push changes to your fork rather than to the main codebase. If you - work at Odoo, configure Git to push changes to the shared fork created on the account **odoo-dev**. + work at Odoo, configure Git to push changes to the shared fork created on the account + **odoo-dev**. .. tabs:: @@ -41,8 +44,8 @@ will be part of the `addons-path` that references all directories containing Odo #. Visit `github.com/odoo/tutorials `_ and click the :guilabel:`Fork` button to create a fork of the repository on your account. - #. In the command below, replace `` with the name of the GitHub account - on which you created the fork. + #. In the command below, replace `` with the name of the GitHub + account on which you created the fork. .. code-block:: console @@ -58,57 +61,67 @@ will be part of the `addons-path` that references all directories containing Odo $ git remote set-url --push origin you_should_not_push_on_this_repository That's it! Your environment is now prepared to run Odoo from the sources, and you have successfully -created a repository to serve as an addons directory. This will allow you to push your work to GitHub. +created a local repository to serve as an addons directory. This will allow you to push your work to +GitHub. .. important:: - **For Odoo employees only:** - #. Make sure to read very carefully :ref:`contributing/development/first-contribution`. In particular, - your branch name must follow our conventions. + #. Make sure to read very carefully :ref:`contributing/development/first-contribution`. In + particular: - #. Once you have pushed your first change to the shared fork on **odoo-dev**, create a - :abbr:`PR (Pull Request)`. Please put your quadrigram in the PR title (e.g., "abcd - Technical - Training"). - - This will enable you to share your upcoming work and receive feedback from your coaches. To ensure - a continuous feedback loop, we recommend pushing a new commit as soon as you complete a chapter - of the tutorial. Note that the PR is automatically updated with commits you push to **odoo-dev**, - you don't need to open multiple PRs. + - Your code must follow the :doc:`guidelines
`. + - Your commit messages must be :doc:`correctly written +
`. + - Your branch name must follow our conventions. + #. Once you have pushed your first change to the shared fork on **odoo-dev**, create a **draft** + :abbr:`PR (Pull Request)` with your quadrigram in the title. This will enable you to share + your upcoming work and receive feedback from your coaches. To ensure a continuous feedback + loop, push a new commit as soon as you complete a chapter of a tutorial. #. At Odoo we use `Runbot `_ extensively for our :abbr:`CI (Continuous Integration)` tests. When you push your changes to **odoo-dev**, Runbot creates a new build - and test your code. Once logged in, you will be able to see your branches `Tutorials project - `_. + and tests your code. Once logged in, you will be able to see your branch on the `Tutorials + project `_. .. note:: - The specific location of the repositories on your file system is not crucial. However, for the sake of simplicity, we will assume that you have cloned all the repositories under the same directory. If this is not the case, make sure to adjust the following commands accordingly, providing the appropriate relative path from the `odoo/odoo` repository to the `odoo/tutorials` repository. -Run the server -============== +.. _tutorials/setup_guide/start_server: -Launch with `odoo-bin` ----------------------- +Start the server +================ Once all dependencies are set up, Odoo can be launched by running `odoo-bin`, the command-line -interface of the server. +interface of the server, and passing the comma-separated list of repositories with the `addons-path` +argument. If you have access to the `odoo/enterprise` repository, add it to the `addons-path`. -.. code-block:: console +.. tabs:: - $ cd $HOME/src/odoo/ - $ ./odoo-bin --addons-path="addons/,../enterprise/,../tutorials" -d rd-demo + .. tab:: Run the community edition + + .. code-block:: console + + $ cd $HOME/src/odoo/ + $ ./odoo-bin --addons-path="addons/,../tutorials" -d tutorials + + .. tab:: Run the enterprise edition + + .. code-block:: console + + $ cd $HOME/src/odoo/ + $ ./odoo-bin --addons-path="addons/,../enterprise/,../tutorials" -d tutorials There are multiple :ref:`command-line arguments ` that you can use to run the server. In this training you will only need some of them. .. option:: -d - The database that is going to be used. + The database to use. .. option:: --addons-path @@ -117,66 +130,59 @@ the server. In this training you will only need some of them. .. option:: --limit-time-cpu - Prevent the worker from using more than CPU seconds for each request. + Prevent the worker from using more than `` CPU seconds for each request. .. option:: --limit-time-real - Prevent the worker from taking longer than seconds to process a request. + Prevent the worker from taking longer than `` seconds to process a request. .. tip:: - The :option:`--limit-time-cpu` and :option:`--limit-time-real` arguments can be used to prevent the worker from being killed when debugging the source code. - - | You may face an error similar to `AttributeError: module '' has no attribute - '<$ATTRIBUTE'>`. In this case, you may need to re-install the module with :command:`$ pip - install --upgrade --force-reinstall `. - | If this error occurs with more than one module, you may need to re-install all the - requirements with :command:`$ pip install --upgrade --force-reinstall -r requirements.txt`. - | You can also clear the python cache to solve the issue: - - .. code-block:: console - - $ cd $HOME/.local/lib/python3.8/site-packages/ - $ find -name '*.pyc' -type f -delete - - Other commonly used arguments are: - :option:`-i `: Install some modules before running the server - (comma-separated list). This is equivalent to going to :guilabel:`Apps` in the user interface, + (comma-separated list). This is equivalent to going to :guilabel:`Apps` in the user interface and installing the module from there. - :option:`-u `: Update some modules before running the server - (comma-separated list). This is equivalent to going to :guilabel:`Apps` in the user interface, - selecting a module, and upgrading it from there. + (comma-separated list). This is equivalent to going to :guilabel:`Apps` in the user interface + and updating the module from there. + +.. _tutorials/setup_guide/log_in: Log in to Odoo --------------- +============== -Open http://localhost:8069/ on your browser. We recommend using `Chrome +Open http://localhost:8069/ in your browser. We recommend using `Chrome `_, `Firefox `_, or any other browser with development tools. To log in as the administrator user, use the following credentials: -- email: `admin` -- password: `admin` +- Email: `admin` +- Password: `admin` -Enable the developer mode -========================= - -The developer or debug mode is useful for training as it gives access to additional (advanced) -tools. :ref:`Enable the developer mode ` now. Choose the method that you prefer; -they are all equivalent. +.. _tutorials/setup_guide/extra_tools: Extra tools =========== +.. _tutorials/setup_guide/extra_tools/dev_mode: + +Developer mode +-------------- + +:ref:`Enable the developer mode ` to get access to developer-oriented tools in the +interface. + +.. _tutorials/setup_guide/extra_tools/git_commands: + Useful Git commands ------------------- Here are some useful Git commands for your day-to-day work. -- | Switch branches: - | When you switch branches, both repositories (odoo and enterprise) must be synchronized, i.e. - both need to be in the same branch. +- Switch branches: .. code-block:: console @@ -186,6 +192,10 @@ Here are some useful Git commands for your day-to-day work. $ cd $HOME/src/enterprise $ git switch {BRANCH} + .. important:: + When you switch branches, both repositories (odoo and enterprise) must be synchronized, i.e. + both need to be in the same branch. + - Fetch and rebase: .. code-block:: console @@ -198,57 +208,53 @@ Here are some useful Git commands for your day-to-day work. $ git fetch --all --prune $ git rebase --autostash enterprise/{BRANCH} -Code Editor +.. _tutorials/setup_guide/extra_tools/code_editor: + +Code editor ----------- -If you are working at Odoo, many of your colleagues are using `VSCode +You are free to choose your code preferred editor. Most Odoo developers use `VSCode `_, `VSCodium `_ (the open source equivalent), `PyCharm `_, or `Sublime Text -`_. However, you are free to choose your preferred editor. +`_. It is important to configure your linters correctly. Using a linter helps you by showing syntax and -semantic warnings or errors. Odoo source code tries to respect Python's and JavaScript's standards, -but some of them can be ignored. +semantic warnings or errors. For JavaScript, we use ESLint and you can find a `configuration file +example here `_. -For Python, we use PEP8 with these options ignored: - -- `E501`: line too long -- `E301`: expected 1 blank line, found 0 -- `E302`: expected 2 blank lines, found 1 - -For JavaScript, we use ESLint and you can find a `configuration file example here -`_. +.. _tutorials/setup_guide/extra_tools/psql_tools: Administrator tools for PostgreSQL ---------------------------------- -You can manage your PostgreSQL databases using the command line as demonstrated earlier or using -a GUI application such as `pgAdmin `_ or `DBeaver -`_. +You can manage your PostgreSQL databases using the command line or a GUI application such as +`pgAdmin `_ or `DBeaver `_. -To connect the GUI application to your database we recommend you connect using the Unix socket. +We recommend you connect the GUI application to your database using the Unix socket. - Host name/address: `/var/run/postgresql` - Port: `5432` - Username: `$USER` -Python Debugging +.. _tutorials/setup_guide/extra_tools/python_debugging: + +Python debugging ---------------- -When facing a bug or trying to understand how the code works, simply printing things out can go a -long way, but a proper debugger can save a lot of time. +When facing a bug or trying to understand how the code works, simply printing things out can help a +lot, but a proper debugger can save a lot of time. -You can use a classic Python library debugger (`pdb `_, -`pudb `_ or `ipdb `_), or you can -use your editor's debugger. +You can use your editor's debugger, or a classic Python library debugger (`pdb +`_, `pudb `_, or `ipdb +`_). -In the following example we use ipdb, but the process is similar with other libraries. +In the following example, we use ipdb, but the process is similar to other libraries. #. Install the library: .. code-block:: console - pip install ipdb + $ pip install ipdb #. Place a trigger (breakpoint): diff --git a/redirects/18.0.txt b/redirects/18.0.txt index e4d3b411f..fdb184ff8 100644 --- a/redirects/18.0.txt +++ b/redirects/18.0.txt @@ -7,3 +7,21 @@ applications/finance/accounting/payments/internal_transfers.rst applications/fin # applications/point of sale content/applications/sales/point_of_sale/payment_methods/terminals/vantiv.rst content/applications/sales/point_of_sale/payment_methods/terminals.rst + +# developer/tutorials + +developer/tutorials/server_framework_101/01_architecture.rst developer/tutorials/server_framework_101_legacy/01_architecture.rst +developer/tutorials/server_framework_101/02_newapp.rst developer/tutorials/server_framework_101_legacy/02_newapp.rst +developer/tutorials/server_framework_101/03_basicmodel.rst developer/tutorials/server_framework_101_legacy/03_basicmodel.rst +developer/tutorials/server_framework_101/04_securityintro.rst developer/tutorials/server_framework_101_legacy/04_securityintro.rst +developer/tutorials/server_framework_101/05_firstui.rst developer/tutorials/server_framework_101_legacy/05_firstui.rst +developer/tutorials/server_framework_101/06_basicviews.rst developer/tutorials/server_framework_101_legacy/06_basicviews.rst +developer/tutorials/server_framework_101/07_relations.rst developer/tutorials/server_framework_101_legacy/07_relations.rst +developer/tutorials/server_framework_101/08_compute_onchange.rst developer/tutorials/server_framework_101_legacy/08_compute_onchange.rst +developer/tutorials/server_framework_101/09_actions.rst developer/tutorials/server_framework_101_legacy/09_actions.rst +developer/tutorials/server_framework_101/10_constraints.rst developer/tutorials/server_framework_101_legacy/10_constraints.rst +developer/tutorials/server_framework_101/11_sprinkles.rst developer/tutorials/server_framework_101_legacy/11_sprinkles.rst +developer/tutorials/server_framework_101/12_inheritance.rst developer/tutorials/server_framework_101_legacy/12_inheritance.rst +developer/tutorials/server_framework_101/13_other_module.rst developer/tutorials/server_framework_101_legacy/13_other_module.rst +developer/tutorials/server_framework_101/14_qwebintro.rst developer/tutorials/server_framework_101_legacy/14_qwebintro.rst +developer/tutorials/server_framework_101/15_final_word.rst developer/tutorials/server_framework_101_legacy/15_final_word.rst From 81929ceb1c14b742e3b357f0826751344c2438b6 Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Mon, 19 Aug 2024 17:01:46 +0200 Subject: [PATCH 2/5] chapter 5 - connect the dots --- .../development/coding_guidelines.rst | 2 + content/developer/reference/backend/orm.rst | 2 + .../tutorials/server_framework_101.rst | 2 +- .../02_lay_the_foundations.rst | 85 +- .../03_build_user_interface.rst | 115 +- .../custom-form-view.png | Bin 26402 -> 28310 bytes .../custom-list-view.png | Bin 16697 -> 15753 bytes .../custom-search-view-filters.png | Bin 9442 -> 26054 bytes .../04_relational_fields.rst | 158 +- .../05_business_logic.rst | 35 - .../05_connect_the_dots.rst | 1335 +++++++++++++++++ .../server_framework_101/06_security.rst | 9 +- .../07_advanced_views.rst | 16 +- .../server_framework_101/08_inheritance.rst | 8 +- content/developer/tutorials/setup_guide.rst | 2 +- 15 files changed, 1560 insertions(+), 209 deletions(-) delete mode 100644 content/developer/tutorials/server_framework_101/05_business_logic.rst create mode 100644 content/developer/tutorials/server_framework_101/05_connect_the_dots.rst 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 3bb3bd923fe8b9d9ed6a2888ca61b1a18799e046..7d798cdebf110e70a7cfc3f10c5451c66dca1aa0 100644 GIT binary patch literal 28310 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 Sz3A|$JuP)TwTcJUul^4YG;eYM 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 1fd824406657390cbe83e7d9282ff805f24d96c6..6b812913e2ac92de9d0790e9a0b3d84679f14310 100644 GIT binary patch literal 15753 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 literal 16697 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 DU$Zae 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 4011d84c2a466f58641f8c3e2b5fad0784f5b5a7..f3ab0cc1a8145eab707c927745bc5bac87513164 100644 GIT binary patch literal 26054 zcmb^ZWmMH|^frouh$!7D@Bku6N`t^c0YSREySp2tTR=)$T1vXRyE_)$-Oavvp8vb| zdB-^C{cy(S3&XV*zjeoqYhLr321`o{qaYF@!oa|wh

2g@JkD4g&+L|N0eprOtbk z7yNo@$1fuP8vMAw)(?V#`3FWs@Qb{2^8TWOJ+=~Q*U6Ek*l5c&e|J9N65dNmv4 z3~b|!3V+0rRrtBp?9Nb}_|?Nh^w+39KhRg}?jA8_;{B-Kz4L=^CG++UE!18Owdtoe z>LC^>7JPk8=7-L|uF{u$=#9<~{?Vg}c@189O?Jxp#{KyPbeH6pkmnBt@kqW%dwxfd zgvv+i`5p4em=>z%4@Y&2EMxuq*K6;N-~9VToB#VUlXu4Jt)bG6H#c@`D(E(E-W+9$ z{QEp}C#GY9>;v=iM&e3Y5i&KRUAzz9Jdw`C8B2N&jdmc;7=2*^-Np)kIB`-^K1%Nx z7}@>1pri29yAz&!+iz9XSV=!xDszWtHk?AqAexD&9%5Lk6RUcuky{6g5&;JI=Gs@yXc}98E>r`o0@u^!VLpr^r zq-&xBy>d76=7N_KWsGg_86 z9^k7w)UK%y+nux@NPcJjwyPsrB{r^9ZKGI+N`nPD6On#hMnXcN{6tQZTTiwmV{Wc4 z>GTD>bN$ao-9QIYmYnV=Em!6JJ zS3@G-+F_g)2O~A7pA4V)YM_?k^8Wtk;NSjbg&MT&l(bVA zBp9s?P*2abk)rTQO1UmCfj|=9#A<799fWA9p}PknP;SGiXmJ-igrg<_UD$qz07`TPQ!FZt{t zjJ*mzN6S$$@@Id3?z*^c8s@NCZ1;z-5d}vCw~n;@BQ`hr&RChp1a?vQGFyD|^g~>& z^ZDCQsv#;e2$|U2%k9NX3dmAw-BrM=BNAC$I@R6>gvguAg4Tpz_-D&2di(q4OSMbY zr8*Dn#hT5n_cx*2)79MX-@k_u6cqgE^{AW5gW`8Fk}2HnWJ>k^eVnUACX+uN8}STY<3 zO|j3d-EuFU!^4t7F`~N^T$)UKH_ITiW${o)Hx)W2W{3c!p3Y1yCS9kue?)1nUOn1n z7UL2|JS^;QnY*3+Ue1#S*m&!r8=Cds>P)-9IQe2}T<`KNhEnHF)fcI+fG-iSxuYA1 z;Z4)K9X$%rC{YWHwvoVaa&v8O@}fHTC6vm&cEJb%dn{68&5?Of{cw3O7oJ3?-Vox3 zMzXrP-s25fWF}UhtFRaXC+K|2`r`&EZlqd@17?#^xh*EwXT7{SQ(}HI@ed0nqW^cX z_>vX5K`ej%D1A+5=a;lfR$1nWnDSTo-=Tx^<}(#35MFLQQ`7I++3-!31v>ir9ht(g z`+UyGRKf_!Da1h$!JS8&7N(QOU9GLDH8mXtepjSCU7cb|`HGWRM6_c3?d>v+?!Urh zGtyNj`|t?}Z8v_A60tuBH!o>?WMgyLy1)h=>!e=i4391SM?Uy!OtZMoxV@tzA$|D= zYyV4lKH+?6{eDzdGKfzn)|TDczQSS!kx_}E*o>Hju`xb0GKoJix-$WC;`WQRuszG9 z{G{0q&AT3rlUV z(|P<(`I5&4+_>BA#ld4&#!|IRv!%pPa(zddSIP2vzMaU_-TKZMIPYvl6suEiZrOT6 zg|P6j8karPADpJ@dD^haDR1-C^WR=xR`IyM{gLJAQsT^2<)aLBHy zu3wuHyKp1<7D!W83qy-{XeZXg3;yUi@4L?T<~8unVcwq3jsuHbBwKP)sFP<3!M4pr7?qE5)26@%58ytSCRf?a>1=3i9~}rK)!ZqIgCJbw&FN z4&Ee-a~TtI&t{^^))4cPgH`;#xzce_@h$Pb$zmCkXm+&4(}75NiczhQco=T8x8mL@m;DW-!Tp2SMLE6#%ipYFkt|@-^AXRsM`yE$eZ)J7=+j6;f)!|Hc zB~djDr^Dt8$Wp9rP4&^x!UVP(_x!u@{R4AOk4ye&lSmRimrQjb5Ee5f%7q3rYLLAX z%kgBZ#NT-0vtj#VzCyiW@7&loH$OhMHt4$^O|)dG4GIc+7ia^!xUyLIcEjy!zWnO$ z?!c&Cu1`n%jHlr{*SDI>M{TAE|n3aiDJMpOufjLLn5H)0Os2j)dLrr-Du zd7V4>%s4VfO$pfZoT|>rXn3|zTNO%ndqWiVRGLhA9ZVJ0RyX#NA$C3@dMxaJYF+Lu z5o>C-jm#vBm0#YWl1yq`6pp@&VD0Sa`mIlUNBVSCQO&kq`Jd3kxi`wHYKXCS|S-vyk{_I3cCT8of3He@>? zfpwmll{H;_XLWzk%Wr0e)7+x)4GL1Gan$#n%d_p1arGMK*rhakatI{VKlHJ+zx)MG zU%Vvq(Ioso-~>t{mOmqHYvO1Vz3^>O$eFjN5tO^sumSYK7mgeP6l1`23`)k??edSY-+IUEM$l861>=HJ)1LdZ~L zX5iV?*ks{i4GM2ICgU(7^6ryJ&XmEyukgA88!ghoz4_|E!`;P7Gxnx43l(4_GnHHa zFfo;a%q^Ff&OKS60E@{tJVdNk>NZTqZnfwyn%xG14&sJbrZlzjQU&Dhl2m8omjpGo zwyt^dp$DvonL_(Wi?<%t5IQljufIQ6F~iV~6KfGw%SJ?QJ17ErGO_2(J<)fR)EqzF zZ1g^c#BV$QpIT@MGVjy`9*}J*O z2w65P{5GJVqo-q5&-HC|h`y@bQK<&ijBo1=3D0rDven@4@OOJ^X3OdFn*6IsLyfE9Rh^bJEqdqk~G{i3NNqsKqqR z`CphCY_6$+3WmIjJ=yb`x;_cx`MsXAV#u4U^dg5__?X^HfjS2yw~uQhOfWQ`xOLLU zVrXgopJBeRF-xr3(j~55>6M)O= z3qms6$n{w|3fPa6Emu`vq+a}feywA^fB*GT!G7$N?FW*vL1W_>>;L}tftm1y9PO3R zvKV7WHoI2ae^us`2wOuo)}!pNw0FnZICIg%{oh(*&_UV;+h__4Tc%`;*(6(7H_(D_ z3X53CmOr!*hJW588N|?vO*OuC`Rdl*pIs&dUMnqC^EgJJ5xnJxo)|wWP;JsERf6g- zlqtd#)re@?3#Vppk4(nM9~>6OPoX3E{4w(R4>*Wn;o*NPK6a0c{L{;$ktarRo{fJk zi|D3W6qk^oOv3B=JtYMNAbM)(>dJo)C%0oIM9I)udHL$q%6bAC9NYu8@U<+8M=zY# z>EA2YZ6s&Rp#1zdaaJea(7=~~qREn9(+`t`#7Cg%iEoJBWk&47IVHhwG5URz%3!ot z5g{R63j?e~b&UTW>X@FTp{Trk1w+!8IbVm8?H5|ENG6r4tg1zCyPmJguZK)D8&l#{ zo95|A?!dQ|X}PTD^YH6YAs|?BLPE_aW%{^@m$&&R&hv#RDD>4+SPAo_`Q)>T{fnm-aG_ zCii;BjgDL#{#~b_t!42|j9rD z`9J)DTUPvVw3Je|HjTro*}%$L9etHIGl^O#Un_PbHOL_AqRe2r$boMW+`_6hFj z#;f#cs4@948lAMz4{tum<0CmJ<6r)8RTSHi=TSK2C&)k7!^6QDT-P$n7#m+piW0D` z6GxjhVK7&&-CLbpE*dp#8y2Wf}x7kjeP7 zln^qhpt3UOxL1;JY#)xjfv&D@jm5A>AL-uAOqf&^Zt{U*Co|lO`w&+@HuodyKZ>aYx$d^!&T8cD0K0(SzyBy zCGq1;_1lB^o`j0i`Fa$O4ew}$zT@7!$NPtVbxheCxFTtqZO$z2SocO|1*dVLi@%kX zlFSnM@+0ML!VLxTZSCy`MR)Wach)y1|5SfxVSn)`s;~Z5&2m9s{=zixVyqeOF7BXy zd)y13skF4Fb~D#ef|Q(sqSkdV5{qhhtUx%o`oOcQ-tGzl!k5W(9f6}`)k}Eza)6^z ziMd)I5YTQ+!jh9HHOB-qf@_1{ccDpCkp9X$dncHD{p4mW2m;KT`TrXD};gqxYa9<7) zPde9&j+f0}Z%(<(Q!V(w7nYK2Nv!AjE9)+&%D`>aSPQ_+OrgkJ_t?4`+~RMI?}=xw z=BjImJgc`C^#0l4*;|%Re+wVtQnp-T0CNvAuIC0p-lh8Ta|)mp@w3PA}D4Mor%tQ(ZiU&2SI9(y*Ce3s331Uh@X+W1ou zot$ou(Uz){suHD+bVB0d$S?&{mL1h!h(h9ug&@*Qw7d943YKj=*_3Ok>N-C&bx?0IRz^$NH{Aj@+}W?N zwO<>We|Y4$KbJdS?ZL8Xw0M4l6u+`I|JEq-6r4grEmB!NBJfJOMjIBU?(#ki{Ec*(piG^kX*=*!6KFPLJcAZo z%h8lY)zM67Q`0ZYl=-oAOL{~cx|WNe=^6u(D^oE)7WJWH7a?a*TJB;OGQ-A*WR^roXzk)*=4NQgc z6uLiuYwAOw{RSE_myb|ph7253{6faP1{tYXIKy%1`UN6R0DN2qEA{!1#r&MIT)B|K z?d72;3Mu2|K6i(AD+R2vJm1OFa%$t#(^IXR)#?WV6s@8D9(Kp=9-9afzOQ6xPLQ1` zpFW(vm0x{OB$&t-mUSL#2frp^5|};MDVm8xGkjoq>m6EVdVa`ccJE0Z{P38$iyBRi z%Mhrm4>^_ofZ-Jb6D{b4FBNBuI=uKLe! zgCd7JsiZ#exj}gQVs+e8=HBiv`Z_3;oDCK?s(a!3Wr%hz)5PK{WD>>&1Qgb}$Cfzl zXBeE2i<>DkF5Z@CoWZ64QQF|x^!N7%MdaU)98%04b6KQJOiZsZh3mT?nNj5Aur3dk zLsN=SY-~;@EEXbg3Z_`nwa`T4=~D9@4yOxe?2<(Cme(@`*wOCoR12te zo*WOy>8lOq8L$!IB?u=6&)F|$y*29&sObF>ahK;ynnxNV!@_ju!fKtdt@Sp_T=L{y zkt22`vQ1Gy;(_;YbzC`Iyz|~~FOw5um~!6m{Lwk5Bjkuba8F+?nQd54!` zY`kT5TpB45U=!U)^3rZFWnHmp!8(NUWFk5DYBbrVJa2uv$UntmwN}!!GNcP-*ZO2k zA%d7MW~b$gy|;qslyz-x7l%iE?y$^cf$Zv0?vTMA`(|WlkMe4 z`9I2+MR6Xne`6v82=TY!MzZzHK2?|{>M!jDHp{LaZnl;Bs@<5K@X@JM{TdE%A3XJ% zD)YR6ZtwKIqFDL~T}Xx|vFm^PL8bLr9m?c($?|lr;+elEmODmSOG+pj&$f57HD)j& z${XOSw30+P*|Ur%i0E1aXMNwH0y%)P9JHByThcr|5Yj4oHIh;iFS8r3v_V8jkfTy; zh+?%USt8aw#e?KJ6rpi9<8(|tNI0G85c>Z9qa|X&wB_$#P6n1sbpq8x7SJ29?CWFd za}Ak4DI(w4j!i~cE?f%glGuy8@qA+<+%;*3lV+NGolYc_C$;dFYB zo?0t)frOa2D9O;m;7v^lsoNKKdm)j#|ENxt8?w{Pli(8KRiJ3X&v&j)DE)RRE z)YyKz>unKmi?Ip?mIUD&_DPU1?!!DAdR zQZJH&vq!fkj3};@*Sss`d-k|HG)$`!9eraMZ(muEj%22 z^cBvRZDe9|Z}|4@+wy%Ut+`6`dH7k1X0`Pw;^hG!f9v-{n1u!xqDs}TP413ICK8>2 zU7xdmH~7NEpb~E!mE&FaR=-z5K-2D9$rR^yO^&{F6t_^ubHl<4u$=D&xISUKz^1fk zCPUPT1F!3eGb9Qcp|`voHE^#73;d8ja}+(n*9J*2Z&FR(xDX!FYU9bKRhnCVH$3z+D1kK z@ew9zVi)s;e5KXp6m)c9zJZZ;TZbMYxE}-^(w?AwJLQkOvwuyv&896fjHCFS7311# zNhvJ$c`M#z)LkKZ;Qidae<%2MQes?CgMYZfHLgOb*j(iN{9L)t9p%;SZ9-zAExpQT zS(@1MgGqm@=Csc}q0+Cs=7W*Hy^%2GvYO1G|1r0Xh z<~@7xD4*o|f`5mVX9Vg^!rh&|=Ucv5FU6wvp()(qc&Sz8vSX+B;*a6sYL9xIe=hI| z38jBKV~~@Rk0ZOkc|*vcUe|V+;s$yGSzy0m_?I8prxYbOjz9k|Ex?fhZ)afck4hZj zThF$T(0GDMJ2W_Ui;YOE+=RKm4DAIj81d;k`;Wc@TrRJ|75k|+`ac#+(kBlLFfRzD zrD{;iG*APO+8G>URJ*bj+0+DKV37PttA<$F^l5Md;qt%-(8lA20~=|Rd!?17D|;oc zSnRF6G!ij8Lq8rxegICARJq5Yr>*T>6wWr;K>N%-561y7dOBDTb{2dG+#=!ANuc~N znSJk(Uuh|Z+2djue@07B&yN>}#mEE`@xy$3zCKM&#pc-|2KkyrhzMw;xo+`Q1dr~5 zOQ)vik{M-Rp*q;6cf+4`KDUL^cktZfN=)3)5wFm0uMDNxA0GbUuH?VesSPD3XBNHNC95ydBn%h2I-3+Lm^>wCP5$lAYg;#L z1=>M$NoDBaLSm66cboQ39m2bHX_{Q{(QC3|uXy1Bqm=wuQ4xkXxY}qT<3(XAahLhW6 z;p=~Foa^U^+&-Bjv|059je@kbQQhz1#Xv|h5|K@78uO%WP8^`sx}wF;;sk1r^YO4DJ+X?R+)H7gH*x;^6`G(CJd1Z5^CShCpQN zLgUkM_zviv`9?Zr)0wO_Yal3BnZjL>@U%^I__e_=;?LC6enrIViN5vkVZdY5(wq8j zuy9--o|=joe=m7{xRm-wf#$D6w`P?&G_=~tA9?zUz5jI-UREW2upo!k;zq%dr%wMb ze|-GyEnw7Ur-I9HBh%BlBEG63Ib56N{n2%iZ~5A<_@ufACSI2GZ3G7efdE%PeWC9y zzi!FM-;RHK!5M<{p(mjPA#>8ScE<}3OD{9|@E1BM_H0~7Aj335Qw$oZ4fi_yff%k- zd2&<+EJYydH=CvKr0PT-4-&vRmX?*Ji{0UZCX^tDS65*(r|T)*v-%@L`nWTf*JP=t z*zz%IEf@CMa{_%aNJm_}@Nm9Avs>zVw_eK``xrZ1$k(W|EbQ$ZsT$E9_8k@;UiZ;i z+~1>RCeXajQ*Od;e|6D1)kU*L8ir1=?6ilvf)N#lKSw%!F9|d8=P)*F;JK}OYJu{( z!0>{A%N_Fu3d z@Ky!lma4FENxnc^GhYL$w=@a^bOG@{<>9psYmMaee>RjN6~1AnggI{Zh#YEc*1lNf z0|2&Xai1AMjwMUI3)t(z+DE2-&;n8m)M5;Rd_BxjNAIwV5&z@A#YmD|c{n zp|fE7Lo92anr}(KIo!AMg9(Tpg+ROf^yuJZ;zM8ri`k?$cc-G8AI#dq}iJ!89fd-rZ6U&9Utv{C~>aFl6O6%-a;Tis8{Lgk+CSaw`Kegjkqp{hla z%<>k7rr(8Ol^ST`sg`$GF8BGqs}`+p9W{%VN@01X^j8tP<*W50fYOrPYF@(n7-@3= zKYr%|i=2Ka;zpV#)_kcd5Vny!Q@)9d=3qS`^23@CthS{k;%9J0)axq|K0c77s>9X# zw45@V4fn7xWI1)A%t_|+JsV{;m5(dk7{H~2@|)>fW|>GU&77NaD{;v0va0D56Y$5& z#neDU{)F{o&SwQX(T;o<{pr~cvDvv>!eBXA2fn@(+%%@jVv-*4=}&gU6D7K+5D}SiwO8!hU7$#iAnlD^e#*E zN?u&6? z2+txOb@jI?uBbXI(UTQslP_aM(O{ga@58QtYlS#eTP^yn@lDc^L{;h?_e_j zJIYdUXkg%HDHLU#H-dqcHLQG#F;{uIJY5@<0%8xJ*zh4WXfU_8G>HeLfd-xp} z^XBeqSa_vD$ir(?PoIJUme}Xs^~3R~oJm#6lu`2Br^iNfl#9Iy?PG5ek-kPm_e%Fa z%Q7v1`|q>9U7$oI;Yzezrd4TpJ??oE2X6F?oRzM(fwbv{Tq%IJhth|U;fwDPRLq40 zN8+!#dr$MVXC1ReaY3==ISD-BQ1uhgz`xEwak>Z+4KHZb$eI&qn zKo-ci_((uNc-lQd%H^{8f~fjX_vF+|rQDKM&hs(AHd_q+IRfPjZJyVLf`E`dCKr`H znxz~ME#fw2RjoQ&7`{xoix~jw(=vgxE{BsMm003>w7;f{ig85?Em<~Na&>~E^6|4y z$M~U2_x(UJ_e?eYLrzXJdCFnA1mtmXujrqigxMfuKHyLd7DHY~wU5okwY9Y+av5QC zOQlfI&=3NHi;oZVEO1fMz1a=-Yk1u`GU6>~#5h*#fdpf@P&eq`Kb<$4?pvHvbL|Xt zXicM!5>NF%={4?)+BAg>9Mu#HCOK#3G2gz8USa=KW!9BYI_`Y^(L3VcmrVIDDxkz% ztQx-!le6o8(v1G+SKR#&>Ox~$DYt1B_N}emjITQ8^@OWvfQypOdFq3EwHxUx8eZL1 ztUM4rGyWo&q_xM-oj3~!_#$7)WJoWol+vZ*^xKwRu-&3Q2FxPGF*$WPzZXv#Otg!~ zXU{_`W{@0su;Yp^Uiwx_5=(PJVKp}r-*sU|nW$9rL;ZxdEHFQqsVIpf>uZdw`^g38 zdsG5F@+%YzncLyHLRXKUE54raoGD+)je^ZV(CmBX2ws!)-lj)_he$Tl(J)59%Q zv>k!(DlJS-!)`#y+rEwA3v#;lPTvKO>-|gbvg{BY9m{9?{>Z`ctR1k;#u^q=DwuF~ zi<=HVZam#R51BTpB>b7bKMa~e9bHX#Mbb-#YE#qG)5LLuJv&ywPXC&o92MR>A?bBJ zh$;-9ieH=P*IDFV5abFLO#U7uT3jMZ~k;0p~}6wa4#h_^E3f0vt^E4+c2 zePee*10yaT{zlA^l%`qDNqT2zG*>wA=bvvj1WwcUJZNv=TLve&h+G*252M}H>JGQ0 z)?QWSsWY7z%X(5FXyD7xJm=mNAiUMl(Ghz1H0j`Vx44)*>$Hq7DS0DZFt$WlUtepz zJYvK8i$fqRC7cZaLeH%W*J+E{iH?`v$HzhPXgmC?GBspye60nxTmfO%!GV!4#w@st zhB<+d@da2$GM7_u4vGg$BLjgs38_1n;a5imKOzQVA`yHMUqE79ITjhgQuj7tCYCfeN0QLnT0 z1x(BzdC3OTQV>5QQaRfh<&4&LSN)z2-0BPt4$Pku@l06^W+$RdeE>)J?zCb5C9nMs z%g^U9fQuVZS($y93?VD)y9rgAt+L|-#8T_R#7~6C7s|DcEI9p?soZjfe-Qu|sC+u? zis({x8$ux^`o>W7!`p>HPC&DfX|)g6G#k z`d*2K1`}0VHcz_VIxD&6TFmy2lCZGI;WB7Q#Js9>x!w-8{J0)FIDhqHcnKE^OHVSb z(JwWX#LzHjrz&O1C^`BtMU}~Qft8Ma@T5>VA}^16LH$hcXE25P&JB7S$$D7}!pAaK z1tG=Zx?6xin5;I3#eYE}L3jib>ei8ASISy?F;`Js+5rbXF9yw8Nq2RgOGkG5y@?RW zB9`#yCc?2uk@2lkvm+gFf=nDN*{l6ALiF^51R!^Y#+6V4qy-~{LKM_kgpyQsV7NA= zp`4I3S}2oJsAfW@IW|2_b*zF8NLUIU@)v;FGQE}8`Fz9x^OIUJL8`9GpmR68qpM36 zTs745fNXBhY+7-1^|t^`Oh`D{|21koYW`@j z=`Iv`hXV)afrw+BEwK0ADr}W&yz={KeI2LMbUFEteu1;3Np?Eq2$>l+Q zelH8@=#LBK*~j|;>2`^M%=x~#s^HbV`TXWcJDgh?k&XeS4|D) zDL2>Yc(s=Hg0%V#lW2#Jngh)Yu{KhfYO;g!p?}Xiz#Y`5}K)-@51dcvLS!#++rW6PbL-AGh}~JFVX2O3~@U_Q!?UWPkdoRv6US zwry@`?k+aDG=i3F_h%46Kj9Z2%~~+&{*#Jv$Gzvb)A9R%~ILcoK zrB_S@g9SnxAqFtc*Vk_}n~D-2_LTeL>EE>MfE>qq?^gA2hv#&}tG}826=WR0VT+oEPWk~^OLdpArU}O*mNL^1jR3T=I?~# zwb!opmV=wl29ISTEfIaoYZC|I{RCrua*9HuKwXqnQlco=XlelS z$3>R7%h9*}Z;1p39`7AKYw_Iol zS1gdY9Wx&J{WWv+H!z_idOAA0x@P_$I{CqR%zh=Sf53j@;&9a)+{1Atc5_sh=F54o zyi!7eY`<%8zN4dKEZqQY@&4NHWk|Clz6zi|E^BtY5a1E9MJa)fC|fqetwfqSx&cNW z9;v1YK){BWxvUa^3Z^BrBFlGN*{*uFTLfXY~$J5=IaWgRqfatep7vur^rDKy|! zAiq?q`e*wS*yTh@$qH#{r7SMu@&`p~!gAnYBX;)xjlXuifVeEnQDfVFQwoYql+DjF z7wq(wNl+n5JtI5X~4>4$$X@OB0J85Gv!jYqM=cQlYg{zEaUX+ ze3~FE@54nLxWtg`y243P4LqKO{Jdah>LkjQn0)L`(|Vjz(T^572$Dfb`$ zzWd4J5|d9d!AAhXEfo1yoD8k{Sxm$qWDZnHL6P!{bCY>$^{Ri)l335PMFgN%GZbJ` zvtNtH{?mnlu`C1rZ{Mh);VGia<;%#iT8);pxkH)*adrPn=9ooaXnpFEdh{jrOoVL- z9r@^Zz$*@Z0^&3P&s2T1;U`i}N)DccG( zuyenj1|FPiFp1(HZ)NtVS@*twci?E>JG5LcZZdwf&Z4O37@#RpDKzgyJKR#2yVPn+ zv-N~v`GRRwM(EUpQ=>wyF^qpVhD z*zQ>8Xbx1WS3?e$+Y9fJT;p!)8hQHHHdG+}bFQc71R&y!l}gBcPYZnJdb}7l+AlTk z64?%NIS!lOgOd$vpcoE21{B`ktriT=8=aS06&>2j64z?q|Z8Yl63HlllBCQ9H1Y`R)L_psRfPQC1y8cR{$A;G^0!OFqXn(19|*cjl^oPXW{{i-U93w~QBpUxl&o@%>`- zn(U{$m!{r}kj9nFeFkGn?vQ0H7!XH?n&7%Qt*>8NsIm^6e`+cMOZs>aZ$-dv$^%G} z=z+VHB*u69Kv#X8w-Mapba`*^T|iU1AOH`B1YZ4vW(B%h)scaA8Usjea#i*=fEDov zY$yOGARlW(_j!>ZOHpL1AnFcS<0hRjYC03a$A=JaY#`xMliFZJ{Ufc6g@ z&}XTm!7~KneT&lS$`wE!S>aqq7sxlU(!_@7Q4O!CjVGPqmB^N==ZjBfOq#+D%CRHQms{5{Zn>3=I^u8cRi{F7$pfgMP}Y)o?P_K@b~XK z(0Ue8|L>|0O*)O*)Qq$XU2xf~7V-$*+)kv&gOPIElSRaAfl6e|%ps2gO@q?)L{h8tg$`a|C;ku`f<=;6$>*cG_?Qx6AiYNB#>&at!V`FB1A}!cgMsSF)&*W z{1u;cTbr^7Lq;hDDm*U_*=;}7?5_Fp21~yq96P&l@e7KA29nzV)Lr_2^|1e-y=={1fAm8sL0Sv>!H{2D^U9x!giAX}?T065r>dY4+yrZ(?HDh-`RfcdY^+d)Oh z-7#2l;iB#WDErw7eLn>Un#YieiXX;3X?*`KK&_aiUFSe$q^m2g?A$>#KF?~^gQpd_ zaa=lXTtP?^%b4=$+Hih+f;CkL6-rT6kSUPgDUJjcx(cBRTJ(Ohs*s)@Tr6mAz-Vw+b+j+WJ~@)= zZ>0*<)N-zQZ4D;TL67SP0rGMt#P7Ioe#)>9{_UJ=T_3ydK&4b+{RNj^t^M#pcw~IM zeQGX5mNG=T$y_9v_>9S=iw7Vc!6L=nxhE>AG@H!@%GAJy<*q^QLe*+`D!_n~{BUNo zcHNV!_=~W|an#aF1`xsUNuhiksIN|sx`@Nx@6&)BKHLKzH!<-;V|}&JMzfmnSUP*rFScdi0^)Zbu)qOK}fIPgkYnFzc1mgof_P;17mx9QmOtK84wRw`z-n3j!+5E zF6dY%I5SqRHvE$&2XRvaY+Enz%x9?Qt>y`85UKjOaECMv*S)iw7Lu@(Yi^2e@s^=3 zLNMnhicR0~9qOe61}VZwu4qBYgiW(78aO4fv@5C)?wwzs_p7~q1*la{Z^bf4w>_I& zfj<1ZKpVB{3`Oo8T{M@?Esq}6W7jQ?om^uLzkc&GU~lkHzd9TKz4SWDu1uHvhe!Co zQt!yhYytq`vSst#8eIVnTypj}rf?^v1?^Y||w9hnwpeZhUzG zMc6gbMo&W}@{P(M1CcDtq5KusBIWba)@6YfOB(gEYJa}&S^WHbS7_eJ3ja#m|BnOY z|9@2@|Jxk8)oT=_t;icOFa`R{G|`}d-FjuL@@j?_EW%0*47IPK45z}E>vw+o{rk5Z zeYx{ZGobdjc`p(Fr&o7WDFHGI(*KtVr>f5N0hbrSv(sBMVXo~m|OqQ~2 zF<^kfm`q<@Hjh8YCeUIljjbtSx7jie#-&u8vcI_m|uJ&pf1HcGFwWL{-lMt^&Hm?t0 zZ66KSx2R(g4;S3N+1u|@qyt2nVhL@Gd;yR6Tm%Hm=8rTgQDci}!u%okM{M~2tOGt$ zzk99&PS`<2o3UGdU7izeFq3BkP*hl5Wx>v7cUKpa$CTFd0OjrNE_QQ55g7kzjxVBQ zrU@CN3;|3{lCBzK(qr1rP7WMtW%Ybr>KU z8cHF6PtzSr>))9u<`Q`28NF-7bLMrh=#ngPvDOXv2RoyH*09Ael?0|R-n)O^-FWoM zLchH$bCwPr&y z&6Q>cNgJ+>?4Gzgxp5z=9XC8{k4OW0Ak3SyT4swj{Jv|Ka!P#!Im3aqHh-~~v1r=s z{T^?Qui#{T*Q@ucjwh3)h%YKUa`1+oEQJHTs&%100^+~e1IxFDu&{_i)1*vpFAn?K zFU*YQ(J)*-zI^qw^JCba4x>YTX&U%17y@jLa_zsaFR3+JVolN@$TXUMRjoGfI-T4b zhi@vy$^on4Mc$nc|NRxQT<5IHe!4`Z**}tMI7>J_H(#~(#hn+D!SE8CN>$}kb$c?e zQ)-mJ-KVytgTM0!4Ua?4gdBz4f8%e_ad3Q%dRHS($27C?`CQ+@93JNTSnh_o6sng; z!FtrKu4o1k*fr@M2RS8jZNMS7FH0M9g2I!@+?o{hZ?Ct1s7UKk=hRmUO=^aI0Ykt9qpR@0^>k0@6uy%>J+$o|g>Bso z9po!=1zAm7cf+3;Ts@K zM*i&U%C<97$rl<|{M#$7xcc0qhLd}|Mn?mNq*yBnY{&hw^S3&hdm3m10F@aFvDaZD z6~VP`wYr6x;30%Jn^xD>?TYvv`6-P=(geakEzn5OFFx+|mPjx_Qqpc%4$P?2e2>%R zmTs~@Un@0kG4&WA`!+bWobRxaI5g!qj*ABk+ zVs^ek;!E9N(@dod4!hrrW#JG)FA@4`^l3VRcRY`@rHh_#ymzd+s2evzO&S- zRM|Ptb;JD5ftGNhpf_WLV#dmSv{rvPbkTVBKFYpZmEB8FM`|6C-9n{nTQF_$o`E4X zCz4O>!SEmtT1ebXimG?uq*gk6pqwoG#PTCLI@*F5x1C=!KOV$rdh85RDu2F?>v{nC(#~&d|7`S1te81y$JbUM~o>*gHB(IyF!2$qlP4W=feb zN~Yb!bab4X0X`S2_N`5f!=U3^q?~eHhcd%@TxM=bNl8$SZ7Cluo=%zh=BpKaf^jfY z*eaTSwdI;b9?gw?EhtV0I#u*!+yv}3Xj-0^QF=+IC){92H8IVL>oWm#Ou0yMf3BQy z-u1=(0~qX_Rx>UCziRvPa4Orb?_H7-iG&CdAu=TMRFX`QxDA=-WFE53+zFXenMK{E zWXibBlsQwOjN3d9n{C|Nu)S;F?{hrw_YTMR9mjV(?_XSpy|3#!&vTt?{eHi-&PB0Y zg_dXaEB#iyX{i-6O$0sf_gpQBhNdEwEOOtCUFWob3g&Z#8aL^<7Zu&r?5npNw*%Je zgQ6{LdfqR5Yf*i-r6DKd!^PrYm<|dN9Y21)qKI;1PjyfrrMs0t63UJAQ>pgEvKvNM z${QLU-W>FroHYBsk00|e!3->F3S4~jJLYGGalMU0csn{r#Fza#U5THV$7-4FFomve zg1c4Ri|@rAGkXICM(@?vMsMLu1(em)*gwf5rXFZyN}+59&l5MH@AlHd?PrfdXJ_p= zIBjI)7gkrhzI^rboIzu2aeYi>0h=z;qW|F&)YbGbW@ z;@peuiihp_o>V74`{M>8cYTZ3aELiDe2(N*B~8cJecm$^8`p$*lCq;-ENy7pT|aUX zy|a9esgz!mO)&8E*MMU`KiH^SYBj&rQpyASZH_Z-XZH09baq)O>XypU>2|%Zj{Wc0 z7CqL^qiOzJo>J+{`9@s>Mf$-Xoh&XPfyIQrwLQRDh)d`zwF|ysxX9FIW5So|9T$G+ zu}Gy;fbL4UwvJXx34Oq3t$q}Eh-)`dO5{G3S~=DtV`G~nxm4#OnPlF;L6#EVU*2%~ddj4gY<;C4by>tG z^Tb=3aqHa|R*^@RjLOQ&v;sQsz_WcBwjU_ceS6ohM%d}6>%3q26{cR%!vV42v1^w`u?`-b;>8%r%6?E2>@r9L-MRpr$ip^}vQ=fC*3 zq}F1-%Vafh#-;{`FlQ+ge?#rC$QDdpq^718k|33ZAJlLA8D*(s13p1%`_F2t(ppT& ztHeM>9Q0CAP*eX?1Aa8m!rBlodv4p|fXOCGAcffER_9%tFO8kM^w*QPtFS`ymrd4d zF==pWUnx!K{$xZYjdHL5H7MB4G+6M!;k_@qaK4f8p%VAMrUkc}y=xVIdwZ!&0wmcI z)87gz@!a#`s+c1QeH|EIgmArytUMguvBPYn}Gczqk{ThOLdZlCe zSFN-^1Tx8_`7riA=B#wa-kRa+ZtN^QB~$Hw6|(9DqaL>^osc(9lfHTV8gZw4Iqob4 zcSK&^xxC-@vr$Od>69Yt*YFPPg$kSLgrdh8-kjq;b_^kA8-xePGmF@_#H}8Yz`iJE zNvk1)6@aPnlqF=9d9l^ zK9~~RJ&q@|^#-jBKuGE>HpzNS+SbsSz}uNjX#fftEu;@^u$v;#4P+(=sW|EDH!@WB z=tLX-99y}6F_zYnYAI0z42^^ii)sm_x{`)o5S zM$E_j$n|3aaFTf2;e5^Rp^C5XNu9il^|D#QMsI(^d*fZF22&;dqEC{JAznpAkxeS$ z{tj2vySMhQ9Wp>yhHT}C=xB?M!9wgpa0Jploj5$?ay^xA8mFphxg@p!Do4z1a&=t~ z&RgL)+(4kH<21#$Z!yBpaCgFKv8Lm?yy89>--;a;IhW+YaTaJ~!t6OaXF5GS?Pa#H z61_Q8=?P}pwd-w@Ck!>q?vWelNle<22=O^pQ}CR^LjKznh24FZ?7a3TVr-bc$u~v! zeZH@?{JZdcpoVSl6%rFYoJ+pC`u6f~q0Y@)_2a8(8Lv_s)2%wOzZqN&KD4s1-5Rifob?hE>zs(JejJOS2$-osNa0yte z!^FYFTNeBi4&%`Q`mBaVhJuj;6l-((Z+f{JdU49a3?lm%;VWmh_cAPMe(M*C(Zb7g z+*EXREkQe?-g=Y{+eh~@rJx-brstIESb3O4Q=)X%d~PkaIF>!bJ~c)@*u3*}Nb|@K zoJAoxEtN3yU?B>Uh?eOg$5p0~SX<$iGbYC30aG+`tNTf=R!88q^Fd&L*{twlWx3@$ zEaYbIUyaC6ev>fE;qT8=WqerjO5--?zHct!P~19Y&c?`xV80y9r1etT65Oxx=>cyB z(HGz?t9WE&peTmj+?CvyXb{(P8oBs9;B()s6P znE3W~ZtET+wRWl+fLx?|^1+{UIA8hK)jVn)lq#wrj`NL;8Z{0)QD5oA85$azB{)Io zUq7y37-k7ZcJ;>Z&Fo7#kzk_YVok^ z;t~`Zk&;*boTb`%6yV{1WT=vXlsjDQz!h}hkz@Yo>e;iocH6D$i9J+O zyMmvb#Nc>ES4j70-!J^pt`UR>Ns z3veEL6|Z_3KOf4MC?cfWK#ZHQWtfA;J#bwdoSf9Oi|I37bWI_s5myiWC!?Jn<8x;*uxwP+VYS5<^p#XW;S`M{a%+{+qW6 zR=y{=_5Whnc_w%2&QErhZ5AA(O%EXqY1`Z2wLr90)I2yZj~Zq~1m_k>o6P?CjQd*; z76&T#}-|IlCB5g zkn_Q$>WZRI!Zr3w#LzyQR<%x&Zqq>np!y}NRL8Zb7yWTYVUf@bh9%JDqRj<5|5VK; zvd5XP$hPXAd>wXMC@mPAe#dxK>+$0fnYL%;eFbh($zp!uXzXfsypfS1qS9k+q}xf2 zcyIoy^@*m1HHj6?fa6u2Y z=9U(z9<0DhC#-QUEv=Fn6_OwwAk-##uCnT9uAjm6fL+e8Fy&K!oVj|q80NkpeQYnK z7)3~=X$hw>%sCyVtjxi2{pWOXYnAK22c>V&3Z-o{Vz|Fm$YPg0yZ1@^uTM|O>71K- zQMd2Tb)FE+4A5xxzx7^s;_x-ZWk9CDCec?`)&^_Nv0#Gr%Iq&u>$3BJ(|w=(%h|1y z>dw{!)>cQ9Fx)M^&@ci(En)qq;@_TVGse8RwPn<40p(umP5=H?CO^}UiBKpxulVsL z!?Tipz-j64t#0xUEcRcZ+5g^Uk9QFh1*MdCQ3`Dp=iNDy&FXVti&fKUWU@yZe-IaV zyzgBhY}8jBJR49W|0*#Rbc28SrIp1&w2W`vGrzR5BiOyrREJYzgbplXOLMZbb^bN; z;r156zOyYd<(t8}MpsO}r3D_2*=4RHyicq?R zsB6SOBlWkcNs#@3@gLMgG(I&{qS{>TlZ=vS6*?J>2- zzn#H>4plqYRBwtfRrIX|>*5-?syCmm^yCkJxfHmVs5kB(z}`I~-DcrXy)ll}9${*euGJazN>w%ey+^T`k3Wk@JB8$mOOk0s0)fb*u+kAHRmYv)pcxezyn< zcXVG8c?m~7vZavggo&jwH^CU`@vsFG&!mkEF3iXtjDX!dnY#Yhil}CR>jl{Hna?|| z`Aq84=3``6xhG2Z5CAvFQ<{{Dhc|Yx-@pPraih>!OAoO~~oouW`1_S7v(_vu`h9 z`9jPD?JzWo^?0^()>g)-!7u*IyBMj(Avr*jvAP*vmHrO=#^E0(UTaG4G&d}QsT-iy zEO7yCCHCXmDzpOMmxLD>aNkpI+x5LB_@yn3rPC*hAeAxx`oxL81Kgl=bS?i$X_zru z^5lkP-n{gn-5JP zVm#M1EQjS^OTyGDQZ5UcuPH(*tUXR4UdkxW^{34*^iIr;49bBTtL+4i3^@3@U`Lj^}{ z4Dwlf%@yjT+R1>}pC*xQ6=_lJl9*!!htG5mhmPgm^3{L|OUDTC~QktcjCiZVNDz2Lv^CAT6LGNviVczW=9 zdLD0ogPzg~UH6qXsS3M$3YN6j6r<)2)@p`|59pybFs@Mg`t|4plYkfpIYKigeb0rk z?_Tux+&RPGo-^G$Q>aA(vWlA3L#9C+)%No=G#5*2f14Kz&SfCEO$zxexYQP)4WGrY zk%!0us=QxeqBm1w-@K8}qkG8QRwu2xiJ#pI3yZQESk%C+C!T$2KM~v=zkk~qYkofw zLS<)F4(i$xq5`?eKxS+H?h=A^iHVJwM?|VXv)l1t2Ju0it*CDNBF>6 zOliB^s`G51j`s>ewKAJsPfBu#o z1-$Q$F%lM$Lw9VeG3Pj!7+b~+lKU>ftNl|M(3<>DLczb_vWiS}|v;U z)6!2Q0|>!td;A&;-ackiZAth!bASAf7KceF$fZ~=aX)1+%(*zYze2c zUi$vreQBFkCr_)ee5bRh0o#|C!82o!4C<3snUa}$nrvs4oWSgplqv_HT+W^8K;BCf zgb=N@copO4%EXX!vj0?lCv~j-d3ojw-1vDfzs z34{LnicR38^M02^8fGM&RG90+CmegI$=|MdDSbNO8v-d=WT=Qo!F)lre3%V62Wl(SmcObDlGd-<;xPz*_ zRVk)X_U!x}(>s^bmP7;J%dI`n-{aM+Whl4PCY=P8j+hGb&?r0gLA!&-9V3xw(Warv z|3NMO2o60sF72XxYXT2#vpq{^j6dC}CO95oxxfsY%FbqVq|3n(G;J$L{MmyR93yHw zDK1c{9_2m=PEZLWcz<&Ae+uRXi=!(N>hEPkyR9?4e}xbKcG=w4mn?U3Q(YTg%T1?i zs>~or{xe-;HQ0OLxw}-Mo&Sh`bW0;uQuVQG$+fN%I8PtV5(O8KrEKtlp$Rt1?AMOH z=enY(n<0%fTp_$OgKSTiYp|FQNnde54^jqPkx(=@e_W05y2%*~Nh72-5h5STz?%F= zVkQNH%j%@A-AM0&giMZLzq+oqr4V0c=8$8q46Bm{BKnGya`&=+QCR7d_eyFiJr&Ue zzv{j+7eP1rf)5WSrYJCjL6BB3eJ!CdQk5p?bj?&!N-C$uWMwCJAr({X`yn9S4(Iyj zkA&3s1+=@|&1GjT`k0X}F7*XOcsq-!TqiN{l|N}9?{Nzfke~m++j-xXydF=ucih?A zaOHz*)}4GEE|0b0&={0%{0=NLAfF=XpVjz>qxqTzH22AUHd<;qrMJI+;u~zf1P~0+ zscU}DKawwmJ-ja+pRsbzt^@a@A5n{mL+%UzpnT(gWMI6cjY)0`RnAzNxx>=5wC*1A?A9JTB>QIDzB(b!u(4t6B^z!{sDJwFDMczkLZnY-|#EM&%Cu zm>&{SQo=CmaZjYQK>haVwRGPrG4iCuj5OqC1l>&ql10g_=TH-n+?R*+8z*|@3HU!q zC1yW~8$b>Q2ealln;{W5;;F7qt8l=ITor7LY0k8SuVpuB6(nsp3?p)>9J-7ZK=)@n zHBqy-bAp!lNg{8o?(pu6#%)Kvgb$l@iNXOqDfP$HTwGjGx+I@*`uQ2|mCg-K)*l(2 z{qXUZAQ1L_x@**yXAB;0b{uH?M0FG!2oAA4t0!(l;tEb%3LR$2Zm*z8LC)iQQW5@& zHx?A790048Rau4lJ^UH$ufO`9V|}7l1HGmQ9x3LP0-!npxT^`Kq0Ed}`dMW`DKr~R-XF5e{;O)c0qo-Q2e$sRmio~Dp~eS^Bf)!VC|2(s{@LCAr$G}T=VF*7aJyD}Q( z*Ccq$fok>NiM2FzV1_CEZ#j?uC$XO8|EZdAUtu;~v9J@o-9+p$bh!1bD!TCNKT|FIosBuRhVaqY166hW%{FRTQaR#PK!rhvUA%#e z<`gkAGn+o;QJTWJ_O*<3_{e~GuT5k1s+$%brIymKwJL=XDcv4OZytbC?5Gy&(%61M zLysy<#dy=g^t76`d)ND&N5n4?K$BrGG)!ea2w0Kv72{Vwuw^*S-_v7Q=^WOcr^&cP zgn?BLz}|rXRyt47z~7d#9fbX@TV6$IK+S_k$%wtEL3XcQ$DmTZ2N;g!>fS6Jku$T{ zpUxF7=+WWTvEikQAcYzCvo1fO=Y+b;m3J|urLkprUzdTyP485n@4R8oku?{%#aCmY zj-qqk8R9efpDkvJ1lx8@kz9UXveC+&jwl-`?WYWfi$<-v=*Tw5FXQ5D77VIBd~SiN1z)vPCUI zsAv!3&bwO}`6p$&=&F_UvCeXEirUirJ6UEvy2!}>s?$kLX34K}sAXUZyna|Em9`72 zAm7H;RCy0_Xd96DAQuUqfj6V<97HnK0)>#Gas z1?62#)`Th|x4)_tY9+1D_>+4P2b$YA_m@4(7~+b?{J6MBsqPm`PV_w{RLOXcd2yye z5z<1=LMD16^l!4Mq~o|`iyqZ$NI*9+i!o1i+WMJ{Y^~X5y9$Rq2qq3JHs6YV-;wUH h&)BV$y8f7qyzaK=hI#!S{9_h`@_luMLV42{{{{m?@|get literal 9442 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 Jl?oQ`{tHHR=Q98R 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 From df1e582b9310b4d919f6c285e36b79d08044e9ec Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Wed, 19 Feb 2025 11:47:07 +0100 Subject: [PATCH 3/5] documentation for -> documentation on --- .../02_lay_the_foundations.rst | 10 ++-- .../03_build_user_interface.rst | 18 ++++---- .../04_relational_fields.rst | 13 +++--- .../05_connect_the_dots.rst | 46 +++++++++---------- 4 files changed, 43 insertions(+), 44 deletions(-) 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 8cedcfd32..47dd4220b 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 @@ -150,8 +150,8 @@ create a model with some fields to represent real estate properties and their ch .. tip:: - The class name doesn't matter, but the convention is to use the model's upper-cased `_name` (without dots). - - Rely on the reference documentation for :ref:`fields ` to select the - right class and attributes for each field. + - Refer to the documentation on :ref:`fields ` to select the right class + and attributes for each field. .. spoiler:: Solution @@ -317,7 +317,7 @@ typically find: - `write_uid`: The ID of the user who last modified the record. .. seealso:: - :ref:`Reference documentation for automatic fields ` + :ref:`Reference documentation on automatic fields ` .. _tutorials/server_framework_101/load_data_files: @@ -387,7 +387,7 @@ created from a data file so that records can be referenced by their full XML ID ID as value. .. seealso:: - :doc:`Reference documentation for XML data files <../../reference/backend/data>` + :doc:`Reference documentation on XML data files <../../reference/backend/data>` Let's now load some default real estate properties in our database. @@ -518,7 +518,7 @@ same model. It also loads faster, making it the go-to format when performance ma - Each subsequent line describes one new record. .. seealso:: - :ref:`Reference documentation for CSV data files ` + :ref:`Reference documentation on 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. 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 a5e08baae..8e7ec6468 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 @@ -219,7 +219,7 @@ the `ir.actions.act_window` model whose key fields include: attribute of the :ref:`field ` data operation. .. seealso:: - :ref:`Reference documentation for window actions ` + :ref:`Reference documentation on 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. @@ -374,8 +374,8 @@ field labels and values). - 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 + - :doc:`Reference documentation on view records <../../reference/user_interface/view_records>` + - :doc:`Reference documentation on view architectures <../../reference/user_interface/view_architectures>` In :ref:`the previous section `, we defined @@ -411,7 +411,7 @@ For a start, the list view could use more fields than just the name. #. After restarting the server to load the new data, refresh the browser to see the result. .. tip:: - Rely on the reference documentation for :ref:`the field component in list views + Refer to the documentation on :ref:`the field component in list views `. The final result should look like this: @@ -486,7 +486,7 @@ Form view no label, should have a placeholder, and should take the full width. .. tip:: - - Rely on the reference documentation for :ref:`structural components + - Refer to the documentation on :ref:`structural components ` and :ref:`the field component ` in form views. - Add the :option:`--dev xml ` argument to the server start-up command to @@ -592,9 +592,9 @@ before its operands`. ['|', ('category', '=', 'electronics'), '!', '&', ('price', '>=', 1000), ('price', '<', 2000)] .. seealso:: - - :ref:`Reference documentation for search views ` - - :ref:`Reference documentation for search domains ` - - :ref:`Reference documentation for the list of reserved field names + - :ref:`Reference documentation on search views ` + - :ref:`Reference documentation on search domains ` + - :ref:`Reference documentation of the list of reserved field names ` All the generic search view only allows for is searching on property names; that's the bare minimum. @@ -631,7 +631,7 @@ Let's enhance the search capabilities. .. tip:: - - Rely on the reference documentation for :ref:`search view components + - Refer to the documentation on :ref:`search view components `, :ref:`search domains `, and :ref:`search defaults `. 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 db385d94a..fb1cdd35c 100644 --- a/content/developer/tutorials/server_framework_101/04_relational_fields.rst +++ b/content/developer/tutorials/server_framework_101/04_relational_fields.rst @@ -239,7 +239,7 @@ the referenced record's ID. record is deleted. .. seealso:: - :ref:`Reference documentation for Many2one fields ` + :ref:`Reference documentation on 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 @@ -262,10 +262,9 @@ managing property types. property references them. .. tip:: - - As the window action doesn't allow opening property types in form view, clicking the - :guilabel:`New` button does nothing. To allow editing records in-place, rely on the - reference documentation for :ref:`root attributes of list views + :guilabel:`New` button does nothing. To allow editing records in-place, refer to the + documentation on :ref:`root attributes of list views ` - The server will throw an error at start-up because it can't require a value for the new, currently empty field. To avoid fixing that manually in the database, run the command @@ -652,7 +651,7 @@ end with the `_ids` suffix, indicating that they allow accessing the IDs of the argument. .. seealso:: - :ref:`Reference documentation for One2many fields ` + :ref:`Reference documentation on 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. @@ -847,7 +846,7 @@ convention, `Many2many` field names end with the `_ids` suffix, like for `One2ma of the junction table and of its columns. .. seealso:: - :ref:`Reference documentation for Many2many fields ` + :ref:`Reference documentation on Many2many fields ` Let's conclude this extension of the model family by allowing to associate multiple description tags with each property. @@ -866,7 +865,7 @@ with each property. to create new tags from the form view of properties. .. tip:: - Rely on the reference documentation for :ref:`the field component + Refer to the documentation on :ref:`the field component ` in form views to find a nice display for property tags. 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 index 22e8b05fa..08eec6701 100644 --- a/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst +++ b/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst @@ -86,9 +86,9 @@ that computed fields remain consistent. recordset in memory. .. seealso:: - - :ref:`Reference documentation for computed fields ` - - :ref:`Reference documentation for recordsets ` - - Reference documentation for the :meth:`@api.depends() ` decorator + - :ref:`Reference documentation on computed fields ` + - :ref:`Reference documentation on recordsets ` + - Reference documentation on the :meth:`@api.depends() ` decorator - :ref:`Coding guidelines on naming and ordering the members of model classes ` @@ -493,7 +493,7 @@ argument, just like regular computed fields. category_name = fields.Char(string="Category Name", related='category_id.name') .. seealso:: - :ref:`Reference documentation for related fields ` + :ref:`Reference documentation on 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 @@ -641,11 +641,11 @@ through `self`. If field values are modified, the changes are automatically refl - Blocking user errors are raised as exceptions. .. seealso:: - - Reference documentation for the :meth:`@api.onchange() ` decorator + - Reference documentation on 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 + - Reference documentation on the :class:`UserError ` exception + - :ref:`Reference documentation on the environment object ` + - Reference documentation on 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. @@ -757,9 +757,9 @@ expression to validate, and the error message to display if the constraint is vi ] .. seealso:: - - Reference documentation for the :attr:`_sql_constraints + - Reference documentation on the :attr:`_sql_constraints ` class attribute - - `Reference documentation for PostgreSQL's constraints + - `Reference documentation on PostgreSQL's constraints `_ .. exercise:: @@ -857,8 +857,8 @@ perform custom validation and raise blocking validation errors if the constraint ) .. seealso:: - - Reference documentation for the :meth:`@api.constrains ` decorator - - Reference documentation for the :class:`ValidationError ` + - Reference documentation on the :meth:`@api.constrains ` decorator + - Reference documentation on the :class:`ValidationError ` exception .. exercise:: @@ -956,7 +956,7 @@ creation process. it's done in data files. .. seealso:: - Reference documentation for the :meth:`ref ` method. + Reference documentation on 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. @@ -1088,13 +1088,13 @@ accessed directly using :code:`record.field`. is equivalent to calling :code:`recordset.write({'field': value})`. .. seealso:: - - Reference documentation for the :meth:`@api.model_create_multi ` + - Reference documentation on 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. + - Reference documentation on the :meth:`create ` method. + - Reference documentation on the :meth:`write ` method. + - Reference documentation on the :meth:`unlink ` method. + - Reference documentation on the :meth:`browse ` method. + - Reference documentation on the :meth:`exists ` method. .. exercise:: #. Move a property to the :guilabel:`Offer Received` state when its first offer is received. @@ -1205,7 +1205,7 @@ action, a `button` element must be added to the view, with its `type` attribute - Prevent users from creating or editing products when browsing them through the button. .. seealso:: - Reference documentation for :ref:`button containers + Reference documentation on :ref:`button containers `. .. exercise:: @@ -1218,7 +1218,7 @@ action, a `button` element must be added to the view, with its `type` attribute - Allow users to browse offers in list and form views. .. tip:: - - Rely on the reference documentation for :ref:`action buttons + - Refer to the documentation on :ref:`action buttons ` in form views. - Find icon codes (`fa-`) in the `Font Awesome v4 catalog `_. @@ -1315,8 +1315,8 @@ action descriptor. #. tmp ... in the header. .. tip:: - - Rely on the reference documentation for :ref:`headers - ` in form views. + - Refer to the documentation on :ref:`headers ` in + form views. .. todo: accept/refuse offer buttons -> auto refuse others when accepting (write) .. todo: multi-checkbox refuse offers in bulk From 5fcfde48a432693fbe2031b5ad171e410650891f Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Wed, 19 Feb 2025 11:47:21 +0100 Subject: [PATCH 4/5] model actions --- .../custom-form-view.png | Bin 28310 -> 28300 bytes .../custom-list-view.png | Bin 15753 -> 15742 bytes .../custom-search-view-fields.png | Bin 7594 -> 7563 bytes .../custom-search-view-filters.png | Bin 26054 -> 9453 bytes .../05_connect_the_dots.rst | 268 ++++++++++++++---- .../07_advanced_views.rst | 4 + .../server_framework_101/08_inheritance.rst | 2 - .../server_framework_101/10_unit_testing.rst | 4 +- 8 files changed, 216 insertions(+), 62 deletions(-) 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 7d798cdebf110e70a7cfc3f10c5451c66dca1aa0..15e0e5795dd0c0b2fe69c03d35734ce7bb73f894 100644 GIT binary patch literal 28300 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 Wgz<{HC!2>^E6Zy literal 28310 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 literal 15753 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 diff --git a/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-fields.png b/content/developer/tutorials/server_framework_101/03_build_user_interface/custom-search-view-fields.png index 3cd2ffae1c3bed5e03d5e195b49ee5e822582671..9d191e7517e79979c0657289a5ec6f8783d13cb2 100644 GIT binary patch literal 7563 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 delta 7541 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 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 f3ab0cc1a8145eab707c927745bc5bac87513164..e22c73761e8cab0e90a1a21e580cd0d20b977501 100644 GIT binary patch literal 9453 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 literal 26054 zcmb^ZWmMH|^frouh$!7D@Bku6N`t^c0YSREySp2tTR=)$T1vXRyE_)$-Oavvp8vb| zdB-^C{cy(S3&XV*zjeoqYhLr321`o{qaYF@!oa|wh

2g@JkD4g&+L|N0eprOtbk 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 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 index 08eec6701..92539670e 100644 --- a/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst +++ b/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst @@ -1131,10 +1131,9 @@ accessed directly using :code:`record.field`. .. code-block:: python :caption: `real_estate_property.py` - :emphasize-lines: 1,5-27 + :emphasize-lines: 1,4-26 address_id = fields.Many2one(string="Address", comodel_name='res.partner') - [...] @api.model_create_multi @@ -1174,43 +1173,61 @@ 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 +action, add a `button` element to the view, with its `type` attribute set to `action`. Use the +`name` attribute to reference the XML ID of the action to execute, 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. + In the following example, a button is added to the product form view to display all other + products in the same category. .. code-block:: xml -

- -
-
-
- + + Products + product + + [("id", "!=", active_id), ("category_id", "=", context.get('current_category_id'))] + + list,form + + + +
+ +
+
+
+
+
.. 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. + - Pass the current category ID to the action which filters out products from other + categories. - Prevent users from creating or editing products when browsing them through the button. .. seealso:: - Reference documentation on :ref:`button containers - `. + - Reference documentation on :ref:`button containers + `. + - Reference documentation on :ref:`environment variables for Python expressions in views + `. .. exercise:: - Replace the property form view's :guilabel:`Offers` notebook page with a **stat button**. This - button should: + Replace the :guilabel:`Offers` notebook page in the property form view 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. @@ -1222,22 +1239,21 @@ action, a `button` element must be added to the view, with its `type` attribute ` 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. + - Ensure your count computations :ref:`remain efficient as the number of records to process + grow `. + - Use the `default_` context key to set default values when creating new records + through that button. .. spoiler:: Solution .. code-block:: python :caption: `real_estate_property.py` - :emphasize-lines: 4,8-15 + :emphasize-lines: 4,7-14 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') @@ -1272,21 +1288,26 @@ action, a `button` element must be added to the view, with its `type` attribute .. code-block:: xml :caption: `real_estate_property_views.xml` - :emphasize-lines: 2-12 + :emphasize-lines: 4-14 - -
- -
+ [...] + +
+ +
+ [...] +
+ [...] +
.. _tutorials/server_framework_101/model_actions: @@ -1298,38 +1319,169 @@ methods that execute custom business logic. These methods enable more complex wo 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. +To link a button to a model-defined action, set its `type` attribute to `object`, and use the `name` +attribute to specify the model method that should be called when the button is clicked. The method +receives the current recordset through `self` and should return a value indicating the result of the +action. .. example:: - In the following example, + In the following example, a button is added to the product category form view to ensure that all + products in the category have a positive margin. + + .. code-block:: xml + +