From 32cba451c1a73569cb011f059aa7b7b12bbeb004 Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Mon, 6 May 2024 17:52:16 +0200 Subject: [PATCH] [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