diff --git a/content/developer/howtos.rst b/content/developer/howtos.rst index 9cb865cf5..822b590c1 100644 --- a/content/developer/howtos.rst +++ b/content/developer/howtos.rst @@ -5,6 +5,7 @@ Tutorials .. toctree:: :titlesonly: + howtos/rdtraining howtos/themes howtos/website howtos/backend diff --git a/content/developer/howtos/rdtraining.rst b/content/developer/howtos/rdtraining.rst new file mode 100644 index 000000000..febf21f68 --- /dev/null +++ b/content/developer/howtos/rdtraining.rst @@ -0,0 +1,74 @@ +:show-content: + +.. _howto/rdtraining: + +=============== +Getting Started +=============== + +Welcome to the Getting Started Odoo 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. + +This training is split in two parts. The first part is the 'Core Training': its objective is to +give you an insight of the most important parts of the Odoo development framework. +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. + +The second part covers a set of 'Advanced' topics. Each topic can be followed independently, but +requires the 'Core Training'. Note that some advanced topics cover basic features +of an Odoo module, so it's a good idea to give them a quick look. + +All topics are built around a business case we will enhance along the way. The reader is expected +to actively take part in the training by writing the solution for each exercise. + +Ready? Let's get started! + +Core Training +============= + +.. toctree:: + :caption: Advanced Topics + :titlesonly: + :glob: + + rdtraining/0* + rdtraining/1* + +* :doc:`rdtraining/01_architecture` +* :doc:`rdtraining/02_setup` +* :doc:`rdtraining/03_newapp` +* :doc:`rdtraining/04_basicmodel` +* :doc:`rdtraining/05_securityintro` +* :doc:`rdtraining/06_firstui` +* :doc:`rdtraining/07_basicviews` +* :doc:`rdtraining/08_relations` +* :doc:`rdtraining/09_compute_onchange` +* :doc:`rdtraining/10_actions` +* :doc:`rdtraining/11_constraints` +* :doc:`rdtraining/12_sprinkles` +* :doc:`rdtraining/13_inheritance` +* :doc:`rdtraining/14_other_module` +* :doc:`rdtraining/15_qwebintro` +* :doc:`rdtraining/16_guidelines_pr` + +Advanced topics +=============== + +.. toctree:: + :caption: Advanced Topics + :titlesonly: + + rdtraining/B_acl_irrules + rdtraining/C_data + rdtraining/E_unittest + rdtraining/J_reports + rdtraining/K_dashboard + +* :doc:`rdtraining/B_acl_irrules` +* :doc:`rdtraining/C_data` +* :doc:`rdtraining/E_unittest` +* :doc:`rdtraining/J_reports` +* :doc:`rdtraining/K_dashboard` diff --git a/content/developer/howtos/rdtraining/01_architecture.rst b/content/developer/howtos/rdtraining/01_architecture.rst new file mode 100644 index 000000000..76be120e1 --- /dev/null +++ b/content/developer/howtos/rdtraining/01_architecture.rst @@ -0,0 +1,131 @@ +.. _howto/rdtraining/01_architecture: + +================================ +Chapter 1: Architecture Overview +================================ + +Multitier application +===================== + +Odoo follows 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/media/three_tier.svg + :align: center + :alt: Three-tier architecture + +The presentation tier 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 RDBMS. + +Depending on the scope of your module, Odoo development 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. Advanced topics will require more knowledge in the other subjects. There are +plenty of tutorials freely accessible, so we cannot recommend one over another since it depends +on your background. + +For reference this is the official `Python tutorial`_. + +Odoo modules +============ + +Both server and client extensions are packaged as *modules* which are +optionally loaded in a *database*. A module is a collection of functions and data that target a +single purpose. + +Odoo modules can either add brand new business logic to an Odoo system or +alter and extend existing business logic. One module can be created to add your +country's accounting rules to Odoo's generic accounting support, while +a different module can add support for real-time visualisation of a bus fleet. + +Everything in Odoo starts and ends with modules. + +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 aren't Apps. *Modules* +may also be referred to as *addons* and the directories where the Odoo server finds them +form the ``addons_path``. + +Composition of a module +----------------------- + +An Odoo module **can** contain a number of elements: + +:ref:`Business objects ` + A business object (e.g. an invoice) is declared as a Python class. The fields defined in + these classes are automatically mapped to database columns thanks to the + :abbr:`ORM (Object-Relational Mapping)` layer. + +:ref:`Object views ` + Define UI display + +:ref:`Data files ` + XML or CSV files declaring the model data: + + * :ref:`views ` or :ref:`reports `, + * configuration data (modules parametrization, :ref:`security rules `), + * demonstration data + * and more + +:ref:`Web controllers ` + Handle requests from web browsers + +Static web data + Images, CSS or JavaScript files used by the web interface or website + +None of these elements are mandatory. Some modules may only add data files (e.g. country-specific +accounting configuration), while others may only add business objects. During this training, we will +create business objects, object views and data files. +:ref:`Web controllers ` and +:ref:`static web data ` are advanced topics. + +Module structure +---------------- + +Each module is a directory within a *module directory*. Module directories +are specified by using the :option:`--addons-path ` +option. + +An Odoo module is declared by its :ref:`manifest `. + +When an Odoo module includes business objects (i.e. Python files), they are organized as a +`Python package `_ +with a ``__init__.py`` file. This file contains import instructions for various Python +files in the module. + +Here is a simplified module directory: + +.. code-block:: bash + + module + ├── models + │ ├── *.py + │ └── __init__.py + ├── data + │ └── *.xml + ├── __init__.py + └── __manifest__.py + +Odoo Editions +============= + +Odoo is available in `two versions`_: Odoo Enterprise (licensed & shared sources) and Odoo Community +(open-source). In addition to services such as support or upgrades, the Enterprise version provides extra +functionalities to Odoo. From a technical point-of-view, these functionalities are simply +new modules installed on top of the modules provided by the Community version. + +Ready to start? Before writing actual code, let's go to the +:ref:`next chapter ` to review the Odoo installation process. Even if +Odoo is already running on your system, we strongly suggest you go through this chapter +to make sure we start on the same page during the development of our new application. + +.. _multitier architecture: + https://en.wikipedia.org/wiki/Multitier_architecture + +.. _Python tutorial: + https://docs.python.org/3.6/tutorial/ + +.. _two versions: + https://www.odoo.com/page/editions diff --git a/content/developer/howtos/rdtraining/01_architecture/media/three_tier.svg b/content/developer/howtos/rdtraining/01_architecture/media/three_tier.svg new file mode 100644 index 000000000..5c98f57e0 --- /dev/null +++ b/content/developer/howtos/rdtraining/01_architecture/media/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/howtos/rdtraining/02_setup.rst b/content/developer/howtos/rdtraining/02_setup.rst new file mode 100644 index 000000000..1f851b39a --- /dev/null +++ b/content/developer/howtos/rdtraining/02_setup.rst @@ -0,0 +1,452 @@ +.. _howto/rdtraining/02_setup: + +========================================= +Chapter 2: Development Environment Set-up +========================================= + +There are multiple ways to install Odoo depending on the intended use case. + +This document attempts to describe the installation options for an internal Odoo R&D developer. We +assume that you are installing your development environment on a standard Odoo laptop with Linux +Mint installed and up-to-date. At the time of writing, we are using a vanilla Linux Mint 20 +(Ubuntu 20.04) as a starting point. + + +If you are using another environment, you can refer to :ref:`setup/install/source`. + + +Fetch the sources & configure git +================================= + +Install and configure git +------------------------- + +The very first step of the installation process is to install the `git version control system `__ +because the Odoo source code is managed on GitHub. Once installed, you can set your name and email: + +.. code-block:: console + + $ sudo apt install git + $ git config --global user.name "Your full name" + $ git config --global user.email "xyz@odoo.com" + +Configure GitHub +---------------- + +To fetch the sources and contribute to Odoo's development you will need a GitHub account. We +recommend using your trigram (xyz) followed by '-odoo' as your username: 'xyz-odoo'. If you prefer, +you can also use your personal GitHub account. + + +The easiest way to authenticate with GitHub is to use an SSH connection. Using the SSH +authentication will allow you to connect to GitHub without supplying your username and +password every time. + + +The following instructions are based on the official `GitHub documentation `__. + + +Here is a step-by-step procedure: + + +- Generate a new SSH key, add it to the ssh-agent and copy the SSH key to your clipboard. + + .. code-block:: console + + $ ssh-keygen -t ed25519 -C "xyz@odoo.com" + $ ssh-add ~/.ssh/id_ed25519 + $ sudo apt-get install xclip + $ xclip -sel clip < ~/.ssh/id_ed25519.pub + + +In Github: + + +- In the upper-right corner of any page, click your profile photo, then click Settings + + .. image:: 02_setup/media/userbar-account-settings.png + +- In the user settings sidebar, click SSH and GPG keys. + + .. image:: 02_setup/media/settings-sidebar-ssh-keys.png + +- Click New SSH key or Add SSH key. + + .. image:: 02_setup/media/ssh-add-ssh-key.png + +- In the "Title" field, add a descriptive label for the new key. +- Paste your key into the "Key" field. + + .. image:: 02_setup/media/ssh-key-paste.png + +- Click Add SSH key. + + +Fetch the sources +----------------- + +All the Odoo sources will be located in `$HOME/src/` + +.. code-block:: console + + $ mkdir -p $HOME/src + $ cd $HOME/src + $ git clone git@github.com:odoo/odoo.git + $ git clone git@github.com:odoo/enterprise.git + +.. tip:: Cloning the repositories will take a while, enjoy a cup of coffee while you wait. + +.. tip:: You may need to ask your manager for read rights to fetch the enterprise repository. + +.. _howto/rdtraining/02_setup/development_repository: + +Configure development repository +-------------------------------- + +To contribute to Odoo's development you will need to +`fork the repository `__, create a branch containing +your code in the fork and submit a +`Pull Request `__ +to the Odoo repository. + +If you are lucky enough to work at Odoo, the forks already exist. They are called +`odoo-dev/odoo` and `odoo-dev/enterprise`. + +.. code-block:: console + + $ cd $HOME/src/odoo + $ git remote add odoo-dev git@github.com:odoo-dev/odoo.git #add odoo-dev as a new remote + $ git remote rename origin odoo #change the name of origin (the odoo repository) to odoo + $ git remote set-url --push odoo no_push #remove the possibility to push to odoo (you can only push to odoo-dev) + + $ cd $HOME/src/enterprise + $ git remote add enterprise-dev git@github.com:odoo-dev/enterprise.git + $ git remote rename origin enterprise + $ git remote set-url --push enterprise no_push + + +Useful git commands +------------------- + +Here are some useful git commands for your day-to-day work. + +* Change branch: + When you change branches, both repositories (odoo and enterprise) must be synchronized, i.e. both + need to be in the same branch. + + .. code-block:: console + + $ cd $HOME/src/odoo + $ git checkout 14.0 + + $ cd $HOME/src/enterprise + $ git checkout 14.0 + +* Fetch and rebase: + + .. code-block:: console + + $ cd $HOME/src/odoo + $ git fetch --all --prune + $ git rebase --autostash odoo/14.0 + + $ cd $HOME/src/enterprise + $ git fetch --all --prune + $ git rebase --autostash enterprise/14.0 + + +Install the dependencies +======================== + +Python +------ + +Odoo requires Python 3.6 or later, if your computer is up-to-date you should already be at this +version or higher. + +You can check your Python version with: + +.. code-block:: console + + $ python3 --version + +Install pip3 and libraries +-------------------------- + +For libraries using native code, installation of development tools and native dependencies is +required before installing the Python dependencies of Odoo. + +.. code-block:: console + + $ sudo apt install python3-pip python3-dev libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev libssl-dev libpq-dev libjpeg-dev + + +Install odoo requirements +------------------------- + +.. code-block:: console + + $ cd $HOME/src/odoo + $ pip3 install -r requirements.txt + +.. _howto/rdtraining/02_setup/install-wkhtmltopdf: + +Install wkhtmltopdf +------------------- + +wkhtmltopdf is a library to render HTML into PDF. Odoo uses it to create PDF reports. wkhtmltopdf +is not installed through pip and must be installed manually in version 0.12.5 to support +headers and footers. + +.. code-block:: console + + $ cd /tmp/ + $ sudo wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.focal_amd64.deb + $ sudo gdebi --n wkhtmltox_0.12.5-1.focal_amd64.deb + $ sudo ln -s /usr/local/bin/wkhtmltopdf /usr/bin + $ sudo ln -s /usr/local/bin/wkhtmltoimage /usr/bin + +Right-to-left interface support +------------------------------- + +In order to support right-to-left (RTL) languages, we need `rtlcss` to convert the CSS files: + +.. code-block:: console + + $ sudo apt-get install nodejs npm + $ sudo npm install -g rtlcss + +Install PostgreSQL +------------------ + +As seen in :ref:`howto/rdtraining/01_architecture`, Odoo uses PostgreSQL as a RDBMS. In the context of a +development machine, the easiest approach is to install it locally. Then we can create a PostgreSQL user +corresponding to our current user: + +.. code-block:: console + + $ sudo apt install postgresql postgresql-client + $ sudo -u postgres createuser -s $USER + + +Some useful SQL commands: + +.. code-block:: console + + $ createdb $DB_NAME #Create a database + $ dropdb $DB_NAME #Drop a database + + $ psql $DB_NAME #Connect to a database + \l #List all the available databases + \dt #List all the tables of the $DB_NAME database + \d $TABLE_NAME #Show the structure of the table $TABLE_NAME + \q #Quit the psql environment (ctrl + d) + +Run the server +============== + +Running odoo-bin +---------------- + +Once all dependencies are set up, Odoo can be launched by running odoo-bin, the command-line interface of the server. + +.. code-block:: console + + $ cd $HOME/src/odoo/ + $ ./odoo-bin --addons-path="addons/,../enterprise/" -d rd-demo + +There are multiple :ref:`command-line arguments ` that you can use to +configure the server. In this training you will only need some of them. + +.. option:: -d + + The database that is going to be used. + +.. option:: --addons-path + + A comma-separated list of directories in which modules are stored. These directories are scanned + for modules. + +.. option:: --limit-time-cpu + + Prevents the worker from using more than CPU seconds for each request. + +.. option:: --limit-time-real + + Prevents the worker from taking longer than seconds to process a request. + +The last two can be used to prevent the worker from being killed when debugging the source code. + +.. tip:: You may face an error similar to `AttributeError: module '$MODULE_NAME' has no attribute '$ATTRIBUTE'` + + In this case you may need to re-install the module with `$ pip install --upgrade --force-reinstall $MODULE_NAME` + + If this error occurs with more than one module then you may need to re-install all the + requirements with `$ pip3 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 + + +Log in to Odoo +-------------- + +Open `http://localhost:8069/` on your browser. We recommend you use: +`Firefox `__, +`Chrome `__ +(`Chromium `__ the open source equivalent) or any other browser with +development tools. + +To log in as the administrator user, you can use the following credentials: + +* email = `admin` +* password = `admin` + +The developer mode +================== + +The Developer or Debug Mode gives you access to additional (advanced) tools. + +This is useful for training and we assume that the user is in developer mode for the rest of the tutorials. + +To activate the developer or debug mode you can follow the steps `here `__. + +Extra tools +=========== + +Code Editor +----------- + +If you are working at Odoo, many of your colleagues are using `VSCode`_ (`VSCodium`_ the open source +equivalent), `Sublime Text`_, `Atom`_ or `PyCharm`_. However you are free to +choose your preferred editor. + +Don't forget to configure your linters correctly. Using a linter can help you by showing syntax and semantic +warnings or errors. Odoo source code tries to respect Python and JavaScript standards, but some of +them can be ignored. + +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`_. + +.. _configuration file example here: https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines#use-a-linter +.. _VSCode: https://code.visualstudio.com/ +.. _VSCodium: https://vscodium.com/ +.. _Sublime Text: https://www.sublimetext.com/ +.. _PyCharm: https://www.jetbrains.com/pycharm/download/#section=linux +.. _Atom: https://atom.io/ + +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 `__. + +To connect the GUI application to your database we recommend you connect using the Unix socket. + +* Host name/address = /var/run/postgresql +* Port = 5432 +* Username = $USER + + +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. + +You can use a classic Python library debugger (`pdb `__, +`pudb `__ or `ipdb `__) or you can +use your editor's debugger. To avoid difficult configurations in the beginning, it is +easier if you use a library debugger. + +In the following example we use ipdb, but the process is similar with other libraries. + +- Install the library: + + .. code-block:: console + + pip3 install ipdb + +- Trigger (breakpoint): + + .. code-block:: console + + import ipdb; ipdb.set_trace() + + Example: + + .. code-block:: python + :emphasize-lines: 2 + + def copy(self, default=None): + import ipdb; ipdb.set_trace() + self.ensure_one() + chosen_name = default.get('name') if default else '' + new_name = chosen_name or _('%s (copy)') % self.name + default = dict(default or {}, name=new_name) + return super(Partner, self).copy(default) + +Here is a list of commands: + +.. option:: h(elp) [command] + + Without an argument, print the list of available commands. With a command as an argument, print help + about that command. + +.. option:: pp expression + + The value of the ``expression`` is pretty-printed using the ``pprint`` module. + +.. option:: w(here) + + Print a stack trace, with the most recent frame at the bottom. + +.. option:: d(own) + + Move the current frame one level down in the stack trace (to a newer frame). + +.. option:: u(p) + + Move the current frame one level up in the stack trace (to an older frame). + +.. option:: n(ext) + + Continue the execution until the next line in the current function is reached or it returns. + +.. option:: c(ontinue) + + Continue the execution and only stop when a breakpoint is encountered. + +.. option:: s(tep) + + Execute the current line, stop at the first possible occasion (either in a function that is + called or on the next line in the current function). + +.. option:: q(uit) + + Quit the debugger. The program being executed is aborted. + +.. tip:: + + To avoid killing the worker when debugging, you can add these arguments when launching the + server: `--limit-time-cpu=9999 --limit-time-real=9999` + Another solution is to add them directly in the `~/.odoorc` file: + + .. code-block:: console + + $ cat ~/.odoorc + [options] + limit_time_cpu = 9999 + limit_time_real = 9999 + +Now that your server is running, it's time to start +:ref:`writing your own application `! diff --git a/content/developer/howtos/rdtraining/02_setup/media/settings-sidebar-ssh-keys.png b/content/developer/howtos/rdtraining/02_setup/media/settings-sidebar-ssh-keys.png new file mode 100644 index 000000000..67c61910a Binary files /dev/null and b/content/developer/howtos/rdtraining/02_setup/media/settings-sidebar-ssh-keys.png differ diff --git a/content/developer/howtos/rdtraining/02_setup/media/ssh-add-ssh-key.png b/content/developer/howtos/rdtraining/02_setup/media/ssh-add-ssh-key.png new file mode 100644 index 000000000..e8a103e32 Binary files /dev/null and b/content/developer/howtos/rdtraining/02_setup/media/ssh-add-ssh-key.png differ diff --git a/content/developer/howtos/rdtraining/02_setup/media/ssh-key-paste.png b/content/developer/howtos/rdtraining/02_setup/media/ssh-key-paste.png new file mode 100644 index 000000000..139047c57 Binary files /dev/null and b/content/developer/howtos/rdtraining/02_setup/media/ssh-key-paste.png differ diff --git a/content/developer/howtos/rdtraining/02_setup/media/userbar-account-settings.png b/content/developer/howtos/rdtraining/02_setup/media/userbar-account-settings.png new file mode 100644 index 000000000..90392e7b2 Binary files /dev/null and b/content/developer/howtos/rdtraining/02_setup/media/userbar-account-settings.png differ diff --git a/content/developer/howtos/rdtraining/03_newapp.rst b/content/developer/howtos/rdtraining/03_newapp.rst new file mode 100644 index 000000000..bda76da0e --- /dev/null +++ b/content/developer/howtos/rdtraining/03_newapp.rst @@ -0,0 +1,104 @@ +.. _howto/rdtraining/03_newapp: + +============================ +Chapter 3: A New Application +============================ + +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. + +The Real Estate Advertisement module +==================================== + +Our new module will cover a business area which is very specific and therefore not included in the +standard set of modules: real estate. It is worth noting that before +developing a new module, it is good practice to verify that Odoo doesn't already provide a way +to answer the specific business case. + +Here is an overview of the main list view containing some advertisements: + +.. image:: 03_newapp/media/overview_list_view_01.png + :align: center + :alt: List view 01 + +The top area of the form view summarizes important information for the property, such as the name, +the property type, the postcode and so on. The first tab contains information describing the +property: bedrooms, living area, garage, garden... + +.. image:: 03_newapp/media/overview_form_view_01.png + :align: center + :alt: Form view 01 + +The second tab lists the offers for the property. We can see here that potential buyers can make +offers above or below the expected selling price. It is up to the seller to accept an offer. + +.. image:: 03_newapp/media/overview_form_view_02.png + :align: center + :alt: Form view 02 + +Here is a quick video showing the workflow of the module. + +Hopefully, this video will be recorded soon :-) + +Prepare the addon directory +=========================== + +**Reference**: the documentation related to this topic can be found in +:ref:`manifest `. + +.. note:: + + **Goal**: the goal of this section is to have Odoo recognize our new module, which will + be an empty shell for now. It will be listed in the Apps: + + .. image:: 03_newapp/media/app_in_list.png + :align: center + :alt: The new module appears in the list + +The first step of module creation is to create a new directory. To ease the development, we +suggest you first create the directory ``/home/$USER/src/custom``. In this directory we add +another directory ``estate``, which is our module. + +A module must contain at least 2 files: the ``__manifest__.py`` file and a ``__init__.py`` file. +The ``__init__.py`` file can remain empty for now and we'll come back to it in the next chapter. +On the other hand, the ``__manifest__.py`` file must describe our module and cannot remain empty. +Its only required field is the ``name``, but it usually contains much more information. + +Take a look at the +`CRM file `__ +as an example. In addition to providing the description of the module (``name``, ``category``, +``summary``, ``website``...), it lists its dependencies (``depends``). A dependency means that the +Odoo framework will ensure that these modules are installed before our module is installed. Moreover, if +one of these dependencies is uninstalled, then our module and **any other that depends on it will also +be uninstalled**. Think about your favorite Linux distribution package manager +(``apt``, ``dnf``, ``pacman``...): Odoo works in the same way. + +.. exercise:: Create the required addon files. + + Create the following folders and files: + + - ``/home/$USER/src/custom/estate/__init__.py`` + - ``/home/$USER/src/custom/estate/__manifest__.py`` + + The ``__manifest__.py`` file should only define the name and the dependencies of our modules. + Two framework modules are necessary: ``base`` and ``web``. + + +Restart the Odoo server and add the ``custom`` folder to the ``addons-path``: + +.. code-block:: console + + $ ./odoo-bin --addons-path=../custom,../enterprise/,addons + +Go to Apps, click on Update Apps List, search for ``estate`` and... tadaaa, your module appears! +Did it not appear? Maybe try removing the default 'Apps' filter ;-) + +.. exercise:: Make your module an 'App'. + + Add the appropriate key to your ``__manifest__.py`` so that the module appears when the 'Apps' + filter is on. + +You can even install the module! But obviously it's an empty shell, so no menu will appear. + +All good? If yes, then let's :ref:`create our first model `! diff --git a/content/developer/howtos/rdtraining/03_newapp/media/app_in_list.png b/content/developer/howtos/rdtraining/03_newapp/media/app_in_list.png new file mode 100644 index 000000000..90baeeaca Binary files /dev/null and b/content/developer/howtos/rdtraining/03_newapp/media/app_in_list.png differ diff --git a/content/developer/howtos/rdtraining/03_newapp/media/overview_form_view_01.png b/content/developer/howtos/rdtraining/03_newapp/media/overview_form_view_01.png new file mode 100644 index 000000000..1ec543d7c Binary files /dev/null and b/content/developer/howtos/rdtraining/03_newapp/media/overview_form_view_01.png differ diff --git a/content/developer/howtos/rdtraining/03_newapp/media/overview_form_view_02.png b/content/developer/howtos/rdtraining/03_newapp/media/overview_form_view_02.png new file mode 100644 index 000000000..05ea60bc2 Binary files /dev/null and b/content/developer/howtos/rdtraining/03_newapp/media/overview_form_view_02.png differ diff --git a/content/developer/howtos/rdtraining/03_newapp/media/overview_list_view_01.png b/content/developer/howtos/rdtraining/03_newapp/media/overview_list_view_01.png new file mode 100644 index 000000000..935b84a0a Binary files /dev/null and b/content/developer/howtos/rdtraining/03_newapp/media/overview_list_view_01.png differ diff --git a/content/developer/howtos/rdtraining/04_basicmodel.rst b/content/developer/howtos/rdtraining/04_basicmodel.rst new file mode 100644 index 000000000..c876a75db --- /dev/null +++ b/content/developer/howtos/rdtraining/04_basicmodel.rst @@ -0,0 +1,298 @@ +.. _howto/rdtraining/04_basicmodel: + +================================== +Chapter 4: Models And Basic Fields +================================== + +At the end of the :ref:`previous chapter `, 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 properties +(name, description, price, living area...) in a database. The Odoo framework provides tools to +facilitate database interactions. + +Before moving forward in the exercise, make sure the ``estate`` module is installed, i.e. it +must appear as 'Installed' in the Apps list. + +Object-Relational Mapping +========================= + +**Reference**: the documentation related to this topic can be found in the +:ref:`reference/orm/model` API. + +.. note:: + + **Goal**: at the end of this section, the table ``estate_property`` should be created: + + .. code-block:: text + + $ psql -d rd-demo + rd-demo=# SELECT COUNT(*) FROM estate_property; + count + ------- + 0 + (1 row) + +A key component of Odoo is the `ORM`_ layer. +This layer avoids having to manually write most `SQL`_ +and provides extensibility and security services\ [#rawsql]_. + +Business objects are declared as Python classes extending +:class:`~odoo.models.Model`, which integrates them into the automated +persistence system. + +Models can be configured by setting attributes in their +definition. The most important attribute is +:attr:`~odoo.models.Model._name`, which is required and defines the name for +the model in the Odoo system. Here is a minimum definition of a +model:: + + from odoo import models + + class TestModel(models.Model): + _name = "test.model" + +This definition is enough for the ORM to generate a database table named ``test_model``. The +``.`` in the model ``_name`` is automatically converted into a ``_`` by the ORM. By convention all +models are located in a ``models`` directory and each model is defined in its own Python +file. + +Take a look at how the ``crm_recurring_plan`` table is defined and how the corresponding Python +file is imported: + +1. The model is defined in the file ``crm/models/crm_recurring_plan.py`` + (see `here `__) +2. The file ``crm_recurring_plan.py`` is imported in ``crm/models/__init__.py`` + (see `here `__) +3. The folder ``models`` is imported in ``crm/__init__.py`` + (see `here `__) + +.. exercise:: Define the real estate properties model. + + Based on example given in the CRM module, create the appropriate files and folder for the + ``estate_property`` table. + + When the files are created, add a minimum definition for the + ``estate.property`` model. + +Any modification of the Python files requires a restart of the Odoo server. When we restart +the server, we will add the parameters ``-d`` and ``-u``: + +.. code-block:: console + + $ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate + +``-u estate`` means we want to upgrade the ``estate`` module, i.e. the ORM will +apply database schema changes. In this case it creates a new table. ``-d rd-demo`` means +that the upgrade should be performed on the ``rd-demo`` database. ``-u`` should always be used in +combination with ``-d``. + +During the startup you should see the following warnings: + +.. code-block:: text + + ... + WARNING rd-demo odoo.models: The model estate.property has no _description + ... + WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules, consider adding one... + ... + +If this is the case, then you should be good! To be sure, double check with ``psql`` as demonstrated in +the **Goal**. + +.. exercise:: Add a description. + + Add a ``_description`` to your model to get rid of one of the warnings. + +Model fields +============ + +**Reference**: the documentation related to this topic can be found in the +:ref:`reference/orm/fields` API. + +Fields are used to define what the model can store and where they are stored. Fields are +defined as attributes in the model class:: + + from odoo import fields, models + + class TestModel(models.Model): + _name = "test.model" + _description = "Test Model" + + name = fields.Char() + +The ``name`` field is a :class:`~odoo.fields.Char` which will be represented as a Python +unicode ``str`` and a SQL ``VARCHAR``. + +Types +----- + +.. note:: + + **Goal**: at the end of this section, several basic fields should have been added to the table + ``estate_property``: + + .. code-block:: text + + $ psql -d rd-demo + + rd-demo=# \d estate_property; + Table "public.estate_property" + Column | Type | Collation | Nullable | Default + --------------------+-----------------------------+-----------+----------+--------------------------------------------- + id | integer | | not null | nextval('estate_property_id_seq'::regclass) + create_uid | integer | | | + create_date | timestamp without time zone | | | + write_uid | integer | | | + write_date | timestamp without time zone | | | + name | character varying | | | + description | text | | | + postcode | character varying | | | + date_availability | date | | | + expected_price | double precision | | | + selling_price | double precision | | | + bedrooms | integer | | | + living_area | integer | | | + facades | integer | | | + garage | boolean | | | + garden | boolean | | | + garden_area | integer | | | + garden_orientation | character varying | | | + Indexes: + "estate_property_pkey" PRIMARY KEY, btree (id) + Foreign-key constraints: + "estate_property_create_uid_fkey" FOREIGN KEY (create_uid) REFERENCES res_users(id) ON DELETE SET NULL + "estate_property_write_uid_fkey" FOREIGN KEY (write_uid) REFERENCES res_users(id) ON DELETE SET NULL + + +There are two broad categories of fields: 'simple' fields, which are atomic +values stored directly in the model's table, and 'relational' fields, which link +records (of the same or different models). + +Simple field examples are :class:`~odoo.fields.Boolean`, :class:`~odoo.fields.Float`, +:class:`~odoo.fields.Char`, :class:`~odoo.fields.Text`, :class:`~odoo.fields.Date` +and :class:`~odoo.fields.Selection`. + +.. exercise:: Add basic fields to the Real Estate Property table. + + Add the following basic fields to the table: + + ========================= ========================= + Field Type + ========================= ========================= + name Char + description Text + postcode Char + date_availability Date + expected_price Float + selling_price Float + bedrooms Integer + living_area Integer + facades Integer + garage Boolean + garden Boolean + garden_area Integer + garden_orientation Selection + ========================= ========================= + + The ``garden_orientation`` field must have 4 possible values: 'North', 'South', 'East' + and 'West'. The selection list is defined as a list of tuples, see + `here `__ + for an example. + +When the fields are added to the model, restart the server with ``-u estate`` + +.. code-block:: console + + $ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate + +Connect to ``psql`` and check the structure of the table ``estate_property``. You'll notice that +a couple of extra fields were also added to the table. We will revisit them later. + +Common Attributes +----------------- + +.. note:: + + **Goal**: at the end of this section, the columns ``name`` and ``expected_price`` should be + not nullable in the table ``estate_property``: + + .. code-block:: console + + rd-demo=# \d estate_property; + Table "public.estate_property" + Column | Type | Collation | Nullable | Default + --------------------+-----------------------------+-----------+----------+--------------------------------------------- + ... + name | character varying | | not null | + ... + expected_price | double precision | | not null | + ... + +Much like the model itself, fields can be configured by passing +configuration attributes as parameters:: + + name = field.Char(required=True) + +Some attributes are available on all fields, here are the most common ones: + +:attr:`~odoo.fields.Field.string` (``str``, default: field's name) + The label of the field in UI (visible by users). +:attr:`~odoo.fields.Field.required` (``bool``, default: ``False``) + If ``True``, the field can not be empty. It must either have a default + value or always be given a value when creating a record. +:attr:`~odoo.fields.Field.help` (``str``, default: ``''``) + Provides long-form help tooltip for users in the UI. +:attr:`~odoo.fields.Field.index` (``bool``, default: ``False``) + Requests that Odoo create a `database index`_ on the column. + +.. exercise:: Set attributes for existing fields. + + Add the following attributes: + + ========================= ========================= + Field Attribute + ========================= ========================= + name required + expected_price required + ========================= ========================= + + After restarting the server, both fields should be not nullable. + +Automatic Fields +---------------- + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/fields/automatic`. + +You may have noticed your model has a few fields you never defined. +Odoo creates a few fields in all models\ [#autofields]_. These fields are +managed by the system and can't be written to, but they can be read if +useful or necessary: + +:attr:`~odoo.fields.Model.id` (:class:`~odoo.fields.Id`) + The unique identifier for a record of the model. +:attr:`~odoo.fields.Model.create_date` (:class:`~odoo.fields.Datetime`) + Creation date of the record. +:attr:`~odoo.fields.Model.create_uid` (:class:`~odoo.fields.Many2one`) + User who created the record. +:attr:`~odoo.fields.Model.write_date` (:class:`~odoo.fields.Datetime`) + Last modification date of the record. +:attr:`~odoo.fields.Model.write_uid` (:class:`~odoo.fields.Many2one`) + User who last modified the record. + + +Now that we have created our first model, let's +:ref:`add some security `! + + +.. [#autofields] it is possible to :ref:`disable the automatic creation of some + fields ` +.. [#rawsql] writing raw SQL queries is possible, but requires caution as this + bypasses all Odoo authentication and security mechanisms. + +.. _database index: + https://use-the-index-luke.com/sql/preface +.. _ORM: + https://en.wikipedia.org/wiki/Object-relational_mapping +.. _SQL: + https://en.wikipedia.org/wiki/SQL diff --git a/content/developer/howtos/rdtraining/05_securityintro.rst b/content/developer/howtos/rdtraining/05_securityintro.rst new file mode 100644 index 000000000..77e9c9e40 --- /dev/null +++ b/content/developer/howtos/rdtraining/05_securityintro.rst @@ -0,0 +1,126 @@ +.. _howto/rdtraining/05_securityintro: + +========================================== +Chapter 5: Security - A Brief Introduction +========================================== + +In the :ref:`previous chapter `, 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 +to the data for specific groups of users. + +The topic of security is covered in more detail in :ref:`howto/rdtraining/B_acl_irrules`. This chapter +aims to cover the minimum required for our new module. + +Data Files (CSV) +================ + +Odoo is a highly data driven system. Although behavior is customized using Python code, part of a +module's value is in the data it sets up when loaded. One way to load data is through a CSV +file. One example is the +`list of country states `__ +which is loaded at installation of the ``base`` module. + +.. code-block:: text + + "id","country_id:id","name","code" + state_us_1,us,"Alabama","AL" + state_us_2,us,"Alaska","AK" + state_us_3,us,"Arizona","AZ" + state_us_4,us,"Arkansas","AR" + ... + +- ``id`` is an :term:`external identifier`. It can be used to refer to the record + (without knowing its in-database identifier). +- ``country_id:id`` refers to the country by using its :term:`external identifier`. +- ``name`` is the name of the state. +- ``code`` is the code of the state. + +These three fields are +`defined `__ +in the ``res.country.state`` model. + +By convention, a file importing data is located in the ``data`` folder of a module. When the data +is related to security, it is located in the ``security`` folder. When the data is related to +views and actions (we will cover this later), it is located in the ``views`` folder. +Additionally, all of these files must be declared in the ``data`` +list within the ``__manifest__.py`` file. Our example file is defined +`in the manifest of the base module `__. + +Also note that the content of the data files is only loaded when a module is installed or +updated. + +.. warning:: + + The data files are sequentially loaded following their order in the ``__manifest__.py`` file. + This means that if data ``A`` refers to data ``B``, you must make sure that ``B`` + is loaded before ``A``. + + In the case of the country states, you will note that the + `list of countries `__ + is loaded **before** the + `list of country states `__. + This is because the states refer to the countries. + +Why is all this important for security? Because all the security configuration of a model is loaded through +data files, as we'll see in the next section. + +Access Rights +============= + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/security/acl`. + +.. note:: + + **Goal**: at the end of this section, the following warning should not appear anymore: + + .. code-block:: text + + WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules... + +When no access rights are defined on a model, Odoo determines that no users can access the data. +It is even notified in the log: + +.. code-block:: text + + WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules, consider adding one. E.g. access_estate_property,access_estate_property,model_estate_property,base.group_user,1,0,0,0 + +Access rights are defined as records of the model ``ir.model.access``. Each +access right is associated with a model, a group (or no group for global +access) and a set of permissions: create, read, write and unlink\ [#unlink]_. Such access +rights are usually defined in a CSV file named +``ir.model.access.csv``. + +Here is an example for our previous ``test.model``: + +.. code-block:: text + + id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink + access_test_model,access_test_model,model_test_model,base.group_user,1,0,0,0 + +- ``id`` is an :term:`external identifier`. +- ``name`` is the name of the ``ir.model.access``. +- ``model_id/id`` refers to the model which the access right applies to. The standard way to refer + to the model is ``model_``, where ```` is the ``_name`` of the model + with the ``.`` replaced by ``_``. Seems cumbersome? Indeed it is... +- ``group_id/id`` refers to the group which the access right applies to. We will cover the concept + of groups in the :ref:`advanced topic ` dedicated to the security. +- ``perm_read,perm_write,perm_create,perm_unlink``: read, write, create and unlink permissions + +.. exercise:: Add access rights. + + Create the ``ir.model.access.csv`` file in the appropriate folder and define it in the + ``__manifest__.py`` file. + + Give the read, write, create and unlink permissions to the group ``base.group_user``. + + Tip: the warning message in the log gives you most of the solution ;-) + +Restart the server and the warning message should have disappeared! + +It's now time to finally :ref:`interact with the UI `! + +.. [#who] meaning which Odoo user (or group of users) + +.. [#unlink] 'unlink' is the equivalent of 'delete' diff --git a/content/developer/howtos/rdtraining/06_firstui.rst b/content/developer/howtos/rdtraining/06_firstui.rst new file mode 100644 index 000000000..cb37e41ec --- /dev/null +++ b/content/developer/howtos/rdtraining/06_firstui.rst @@ -0,0 +1,293 @@ +.. _howto/rdtraining/06_firstui: + +======================================== +Chapter 6: Finally, Some UI To Play With +======================================== + +Now that we've created our new :ref:`model ` and its corresponding +:ref:`access rights `, it is time to interact with +the user interface. + +At the end of this chapter, we will have created a couple of menus in order to access a default list +and form view. + +Data Files (XML) +================ + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/data`. + +In :ref:`howto/rdtraining/05_securityintro`, we added data through a CSV file. The CSV +format is convenient when the data to load has a simple format. When the format is more complex +(e.g. load the structure of a view or an email template), we use the XML format. For example, +this +`help field `__ +contains HTML tags. While it would be possible to load such data through a CSV file, it is more +convenient to use an XML file. + +The XML files must be added to the same folders as the CSV files and defined similarly in the +``__manifest__.py``. The content of the data files is also sequentially loaded when a module is installed or +updated, therefore all remarks made for CSV files hold true for XML files. +When the data is linked to views, we add them to the ``views`` folder. + +In this chapter we will load our first action and menus though an XML file. Actions and menus are +standard records in the database. + +.. note:: + + When performance is important, the CSV format is preferred over the XML format. This is the case in Odoo + where loading a CSV file is faster than loading an XML file. + +In Odoo, the user interface (actions, menus and views) is largely defined by creating +and composing records defined in an XML file. A common pattern is Menu > Action > View. +To access records the user navigates through several menu levels; the deepest level is an +action which triggers the opening of a list of the records. + +Actions +======= + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/actions`. + +.. note:: + + **Goal**: at the end of this section, an action should be loaded in the system. We won't see + anything yet in the UI, but the file should be loaded in the log: + + .. code-block:: text + + INFO rd-demo odoo.modules.loading: loading estate/data/estate_property_views.xml + +Actions can be triggered in three ways: + +1. by clicking on menu items (linked to specific actions) +2. by clicking on buttons in views (if these are connected to actions) +3. as contextual actions on object + +We will only cover the first case in this chapter. The second case will be covered in a +:ref:`later chapter ` while the last is the focus of an advanced topic. +In our Real Estate example, we would like to link a menu to the ``estate.property`` model, so we +are able to create a new record. The action can be viewed as the link between the menu and +the model. + +A basic action for our ``test.model`` is: + +.. code-block:: xml + + + Test action + test.model + tree,form + + +- ``id`` is an :term:`external identifier`. It can be used to refer to the record + (without knowing its in-database identifier). +- ``model`` has a fixed value of ``ir.actions.act_window`` (:ref:`reference/actions/window`). +- ``name`` is the name of the action. +- ``res_model`` is the model which the action applies to. +- ``view_mode`` are the views that will be available; in this case they are the list (tree) and form views. + We'll see :ref:`later ` that there can be other view modes. + +Examples can be found everywhere in Odoo, but +`this `__ +is a good example of a simple action. Pay attention to the structure of the XML data file since you will +need it in the following exercise. + +.. exercise:: Add an action. + + Create the ``estate_property_views.xml`` file in the appropriate folder and define it in the + ``__manifest__.py`` file. + + Create an action for the model ``estate.property``. + +Restart the server and you should see the file loaded in the log. + +Menus +===== + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/data/shortcuts`. + +.. note:: + + **Goal**: at the end of this section, three menus should be created and the default view is + displayed: + + .. image:: 06_firstui/media/estate_menu_root.png + :align: center + :alt: Root menus + + .. image:: 06_firstui/media/estate_menu_action.png + :align: center + :alt: First level and action menus + + .. image:: 06_firstui/media/estate_form_default.png + :align: center + :alt: Default form view + +To reduce the complexity in declaring a menu (``ir.ui.menu``) and connecting it to the corresponding action, +we can use the ```` shortcut . + +A basic menu for our ``test_model_action`` is: + +.. code-block:: xml + + + +The menu ``test_model_menu_action`` is linked to the action ``test_model_action``, and the action +is linked to the model ``test.model``. As previously mentioned, the action can be seen as the link +between the menu and the model. + +However, menus always follow an architecture, and in practice there are three levels of menus: + +1. The root menu, which is displayed in the App switcher (the Odoo Community App switcher is a + dropdown menu) +2. The first level menu, displayed in the top bar +3. The action menus + + .. image:: 06_firstui/media/menu_01.png + :align: center + :alt: Root menus + + .. image:: 06_firstui/media/menu_02.png + :align: center + :alt: First level and action menus + +The easiest way to define the structure is to create it in the XML file. A basic +structure for our ``test_model_action`` is: + +.. code-block:: xml + + + + + + + +The name for the third menu is taken from the name of the ``action``. + +.. exercise:: Add menus. + + Create the ``estate_menus.xml`` file in the appropriate folder and define it in the + ``__manifest__.py`` file. Remember the sequential loading of the data files ;-) + + Create the three levels of menus for the ``estate.property`` action created in the previous + exercise. Refer to the **Goal** of this section for the expected result. + +Restart the server and **refresh the browser**\ [#refresh]_. You should now see the menus, +and you'll even be able to create your first real estate property advertisement! + +Fields, Attributes And View +=========================== + +.. note:: + + **Goal**: at the end of this section, the selling price should be read-only and the number + of bedrooms and the availability date should have default values. Additionally the selling price + and availability date values won't be copied when the record is duplicated. + + .. image:: 06_firstui/media/attribute_and_default.gif + :align: center + :alt: Interaction between model and view + + The reserved fields ``active`` and ``state`` are added to the ``estate.property`` model. + +So far we have only used the generic view for our real estate property advertisements, but +in most cases we want to fine tune the view. There are many fine-tunings possible in Odoo, but +usually the first step is to make sure that: + +- some fields have a default value +- some fields are read-only +- some fields are not copied when duplicating the record + +In our real estate business case, we would like the following: + +- The selling price should be read-only (it will be automatically filled in later) +- The availability date and the selling price should not be copied when duplicating a record +- The default number of bedrooms should be 2 +- The default availability date should be in 3 months + +Some New Attributes +------------------- + +Before moving further with the view design, let's step back to our model definition. We saw that some +attributes, such as ``required=True``, impact the table schema in the database. Other attributes +will impact the view or provide default values. + +.. exercise:: Add new attributes to the fields. + + Find the appropriate attributes (see :class:`~odoo.fields.Field`) to: + + - set the selling price as read-only + - prevent copying of the availability date and the selling price values + +Restart the server and refresh the browser. You should not be able to set any selling prices. When +duplicating a record, the availability date should be empty. + +Default Values +-------------- + +Any field can be given a default value. In the field definition, add the option +``default=X`` where ``X`` is either a Python literal value (boolean, integer, +float, string) or a function taking a model and returning a value:: + + name = fields.Char(default="Unknown") + last_seen = fields.Datetime("Last Seen", default=lambda self: fields.Datetime.now()) + +The ``name`` field will have the value 'Unknown' by default while the ``last_seen`` field will be +set as the current time. + +.. exercise:: Set default values. + + Add the appropriate default attributes so that: + + - the default number of bedrooms is 2 + - the default availability date is in 3 months + + Tip: this might help you: :meth:`~odoo.fields.Date.today` + +Check that the default values are set as expected. + +Reserved Fields +--------------- + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/orm/fields/reserved`. + +A few field names are reserved for pre-defined behaviors. They should be defined on a +model when the related behavior is desired. + +.. exercise:: Add active field. + + Add the ``active`` field to the ``estate.property`` model. + +Restart the server, create a new property, then come back to the list view... The property will +not be listed! ``active`` is an example of a reserved field with a specific behavior: when +a record has ``active=False``, it is automatically removed from any search. To display the +created property, you will need to specifically search for inactive records. + +.. image:: 06_firstui/media/inactive.gif + :align: center + :alt: Inactive records + +.. exercise:: Set a default value for active field. + + Set the appropriate default value for the ``active`` field so it doesn't disappear anymore. + +Note that the default ``active=False`` value was assigned to all existing records. + +.. exercise:: Add state field. + + Add a ``state`` field to the ``estate.property`` model. Five values are possible: New, + Offer Received, Offer Accepted, Sold and Canceled. It must be required, should not be copied + and should have its default value set to 'New'. + + Make sure to use the correct type! + +The ``state`` will be used later on for several UI enhancements. + +Now that we are able to interact with the UI thanks to the default views, the next step is +obvious: we want to define :ref:`our own views `. + +.. [#refresh] A refresh is needed since the web client keeps a cache of the various menus + and views for performance reasons. diff --git a/content/developer/howtos/rdtraining/06_firstui/media/attribute_and_default.gif b/content/developer/howtos/rdtraining/06_firstui/media/attribute_and_default.gif new file mode 100644 index 000000000..318890988 Binary files /dev/null and b/content/developer/howtos/rdtraining/06_firstui/media/attribute_and_default.gif differ diff --git a/content/developer/howtos/rdtraining/06_firstui/media/estate_form_default.png b/content/developer/howtos/rdtraining/06_firstui/media/estate_form_default.png new file mode 100644 index 000000000..5923f8ac5 Binary files /dev/null and b/content/developer/howtos/rdtraining/06_firstui/media/estate_form_default.png differ diff --git a/content/developer/howtos/rdtraining/06_firstui/media/estate_menu_action.png b/content/developer/howtos/rdtraining/06_firstui/media/estate_menu_action.png new file mode 100644 index 000000000..858fb7821 Binary files /dev/null and b/content/developer/howtos/rdtraining/06_firstui/media/estate_menu_action.png differ diff --git a/content/developer/howtos/rdtraining/06_firstui/media/estate_menu_root.png b/content/developer/howtos/rdtraining/06_firstui/media/estate_menu_root.png new file mode 100644 index 000000000..aef7992af Binary files /dev/null and b/content/developer/howtos/rdtraining/06_firstui/media/estate_menu_root.png differ diff --git a/content/developer/howtos/rdtraining/06_firstui/media/inactive.gif b/content/developer/howtos/rdtraining/06_firstui/media/inactive.gif new file mode 100644 index 000000000..efac9fd81 Binary files /dev/null and b/content/developer/howtos/rdtraining/06_firstui/media/inactive.gif differ diff --git a/content/developer/howtos/rdtraining/06_firstui/media/menu_01.png b/content/developer/howtos/rdtraining/06_firstui/media/menu_01.png new file mode 100644 index 000000000..289bc1eee Binary files /dev/null and b/content/developer/howtos/rdtraining/06_firstui/media/menu_01.png differ diff --git a/content/developer/howtos/rdtraining/06_firstui/media/menu_02.png b/content/developer/howtos/rdtraining/06_firstui/media/menu_02.png new file mode 100644 index 000000000..c46b52af7 Binary files /dev/null and b/content/developer/howtos/rdtraining/06_firstui/media/menu_02.png differ diff --git a/content/developer/howtos/rdtraining/07_basicviews.rst b/content/developer/howtos/rdtraining/07_basicviews.rst new file mode 100644 index 000000000..a6542929b --- /dev/null +++ b/content/developer/howtos/rdtraining/07_basicviews.rst @@ -0,0 +1,231 @@ +.. _howto/rdtraining/07_basicviews: + +====================== +Chapter 7: Basic Views +====================== + +We have seen in the :ref:`previous chapter ` 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 +manner. + +Views are defined in XML files with actions and menus. They are instances of the +``ir.ui.view`` model. + +In our real estate module, we need to organize the fields in a logical way: + +- in the list (tree) view, we want to display more than just the name. +- in the form view, the fields should be grouped. +- in the search view, we must be able to search on more than just the name. Specifically, we want a + filter for the 'Available' properties and a shortcut to group by postcode. + +List +==== + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/views/list`. + +.. note:: + + **Goal**: at the end of this section, the list view should look like this: + + .. image:: 07_basicviews/media/list.png + :align: center + :alt: List view + +List views, also called tree views, display records in a tabular form. + +Their root element is ````. The most basic version of this view simply +lists all the fields to display in the table (where each field is a column): + +.. code-block:: xml + + + + + + +A simple example can be found +`here `__. + +.. exercise:: Add a custom list view. + + Define a list view for the ``estate.property`` model in the appropriate XML file. Check the + **Goal** of this section for the fields to display. + + Tips: + + - do not add the ``editable="bottom"`` attribute that you can find in the example above. We'll + come back to it later. + - some field labels may need to be adapted to match the reference. + + +As always, you need to restart the server (do not forget the ``-u`` option) and refresh the browser +to see the result. + +.. warning:: + + You will probably use some copy-paste in this chapter, therefore always make sure that the ``id`` + remains unique for each view! + +Form +==== + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/views/form`. + +.. note:: + + **Goal**: at the end of this section, the form view should look like this: + + .. image:: 07_basicviews/media/form.png + :align: center + :alt: Form view + +Forms are used to create and edit single records. + +Their root element is ``
``. They are composed of high-level structure +elements (groups and notebooks) and interactive elements (buttons and fields): + +.. code-block:: xml + + + + + + + + + + + + + + + + + +
+ +It is possible to use regular HTML tags such as ``div`` and ``h1`` as well as the the ``class`` attribute +(Odoo provides some built-in classes) to fine-tune the look. + +A simple example can be found +`here `__. + +.. exercise:: Add a custom form view. + + Define a form view for the ``estate.property`` model in the appropriate XML file. Check the + **Goal** of this section for the expected final design of the page. + +This might require some trial and error before you get to the expected result ;-) It is advised +that you add the fields and the tags one at a time to help understand how it works. + +In order to avoid relaunching the server every time you do a modification to the view, it can +be convenient to use the ``--dev xml`` parameter when launching the server: + +.. code-block:: console + + $ ./odoo-bin --addons-path=../custom,../enterprise/,addons -d rd-demo -u estate --dev xml + +This parameter allows you to just refresh the page to view your view modifications. + +Search +====== + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/views/search`. + +.. note:: + + **Goal**: at the end of this section, the search view should look like this: + + .. image:: 07_basicviews/media/search_01.png + :align: center + :alt: Search fields + + .. image:: 07_basicviews/media/search_02.png + :align: center + :alt: Filter + + .. image:: 07_basicviews/media/search_03.png + :align: center + :alt: Group By + +Search views are slightly different from the list and form views since they don't display +*content*. Although they apply to a specific model, they are used to filter +other views' content (generally aggregated views such as :ref:`reference/views/list`). +Beyond the difference in use case, they are defined the same way. + +Their root element is ````. The most basic version of this view simply +lists all the fields for which a shortcut is desired: + +.. code-block:: xml + + + + + + +The default search view generated by Odoo provides a shortcut to filter by ``name``. It is very +common to add the fields which the user is likely to filter on in a customized search view. + +.. exercise:: Add a custom search view. + + Define a search view for the ``estate.property`` model in the appropriate XML file. Check the + first image of this section's **Goal** for the list of fields. + +After restarting the server, it should be possible to filter on the given fields. + +Search views can also contain ```` elements, which act as toggles for +predefined searches. Filters must have one of the following attributes: + +- ``domain``: adds the given domain to the current search +- ``context``: adds some context to the current search; uses the key ``group_by`` to group + results on the given field name + +A simple example can be found +`here `__. + +Before going further in the exercise, it is necessary to introduce the 'domain' concept. + +Domains +------- + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/orm/domains`. + +In Odoo, a domain encodes conditions on +records: a domain is a list of criteria used to select a subset of a model's +records. Each criterion is a triplet with a *field name*, an *operator* and a *value*. +A record satisfies a criterion if the specified field meets the condition of the operator applied to the value. + +For instance, when used on the *Product* model the following domain selects +all *services* with a unit price greater than *1000*:: + + [('product_type', '=', 'service'), ('unit_price', '>', 1000)] + +By default criteria are combined with an implicit AND, meaning *every* criterion +needs to be satisfied for a record to match a domain. The logical operators +``&`` (AND), ``|`` (OR) and ``!`` (NOT) can be used to explicitly combine +criteria. They are used in prefix position (the operator is inserted before +its arguments rather than between). For instance, to select products 'which are +services *OR* have a unit price which is *NOT* between 1000 and 2000':: + + ['|', + ('product_type', '=', 'service'), + '!', '&', + ('unit_price', '>=', 1000), + ('unit_price', '<', 2000)] + +.. exercise:: Add filter and Group By. + + The following should be added to the previously created search view: + + - a filter which displays available properties, i.e. the state should be 'New' or + 'Offer Received'. + - the ability to group results by postcode. + +Looking good? At this point we are already able to create models and design a user interface which +makes sense business-wise. However, a key component is still missing: the +:ref:`link between models `. diff --git a/content/developer/howtos/rdtraining/07_basicviews/media/form.png b/content/developer/howtos/rdtraining/07_basicviews/media/form.png new file mode 100644 index 000000000..8d7fb5541 Binary files /dev/null and b/content/developer/howtos/rdtraining/07_basicviews/media/form.png differ diff --git a/content/developer/howtos/rdtraining/07_basicviews/media/list.png b/content/developer/howtos/rdtraining/07_basicviews/media/list.png new file mode 100644 index 000000000..aedde67fc Binary files /dev/null and b/content/developer/howtos/rdtraining/07_basicviews/media/list.png differ diff --git a/content/developer/howtos/rdtraining/07_basicviews/media/search_01.png b/content/developer/howtos/rdtraining/07_basicviews/media/search_01.png new file mode 100644 index 000000000..0a67c48a9 Binary files /dev/null and b/content/developer/howtos/rdtraining/07_basicviews/media/search_01.png differ diff --git a/content/developer/howtos/rdtraining/07_basicviews/media/search_02.png b/content/developer/howtos/rdtraining/07_basicviews/media/search_02.png new file mode 100644 index 000000000..47f2f9b57 Binary files /dev/null and b/content/developer/howtos/rdtraining/07_basicviews/media/search_02.png differ diff --git a/content/developer/howtos/rdtraining/07_basicviews/media/search_03.png b/content/developer/howtos/rdtraining/07_basicviews/media/search_03.png new file mode 100644 index 000000000..8b9840f59 Binary files /dev/null and b/content/developer/howtos/rdtraining/07_basicviews/media/search_03.png differ diff --git a/content/developer/howtos/rdtraining/08_relations.rst b/content/developer/howtos/rdtraining/08_relations.rst new file mode 100644 index 000000000..6070aeb56 --- /dev/null +++ b/content/developer/howtos/rdtraining/08_relations.rst @@ -0,0 +1,266 @@ +.. _howto/rdtraining/08_relations: + +=================================== +Chapter 8: Relations Between Models +=================================== + +The :ref:`previous chapter ` 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 +the customers and another one containing the list of users. You might need to refer to a customer +or a user on any existing business model. + +In our real estate module, we want the following information for a property: + +- the customer who bought the property +- the real restate agent who sold the property +- the property type: house, apartment, penthouse, castle... +- a list of tags characterizing the property: cozy, renovated... +- a list of the offers received + +Many2one +======== + +**Reference**: the documentation related to this topic can be found in +:class:`~odoo.fields.Many2one`. + +.. note:: + + **Goal**: at the end of this section: + + - a new ``estate.property.type`` model should be created with the corresponding menu, action and views. + + .. image:: 08_relations/media/property_type.png + :align: center + :alt: Property type + + - three Many2one fields should be added to the ``estate.property`` model: property type, buyer and seller. + + .. image:: 08_relations/media/property_many2one.png + :align: center + :alt: Property + +In our real estate module, we want to define the concept of property type. A property type +is, for example, a house or an apartment. It is a standard business need to categorize +properties according to their type, especially to refine filtering. + +A property can have **one** type, but the same type can be assigned to **many** properties. +This is supported by the **many2one** concept. + +A many2one is a simple link to another object. For example, in order to define a link to the +``res.partner`` in our test model, we can write:: + + partner_id = fields.Many2one("res.partner", string="Partner") + +By convention, many2one fields have the ``_id`` suffix. Accessing the data in the partner +can then be easily done with:: + + print(my_test_object.partner_id.name) + +.. seealso:: + + `foreign keys `_ + +In practice a many2one can be seen as a dropdown list in a form view. + +.. exercise:: Add the Real Estate Property Type table. + + - Create the ``estate.property.type`` model and add the following field: + + ========================= ========================= ========================= + Field Type Attributes + ========================= ========================= ========================= + name Char required + ========================= ========================= ========================= + + - Add the menus as displayed in this section's **Goal** + - Add the field ``property_type_id`` into your ``estate.property`` model and its form, tree + and search views + + This exercise is a good recap of the previous chapters: you need to create a + :ref:`model `, set the + :ref:`model `, add an + :ref:`action and a menu `, and + :ref:`create a view `. + + Tip: do not forget to import any new Python files in ``__init__.py``, add new data files in + ``__manifest.py__`` or add the access rights ;-) + +Once again, restart the server and refresh to see the results! + +In the real estate module, there are still two missing pieces of information we want on a property: +the buyer and the salesperson. The buyer can be any individual, but on the other hand the +salesperson must be an employee of the real estate agency (i.e. an Odoo user). + +In Odoo, there are two models which we commonly refer to: + +- ``res.partner``: a partner is a physical or legal entity. It can be a company, an individual or + even a contact address. +- ``res.users``: the users of the system. Users can be 'internal', i.e. they have + access to the Odoo backend. Or they can be 'portal', i.e. they cannot access the backend, only the + frontend (e.g. to access their previous orders in eCommerce). + +.. exercise:: Add the buyer and the salesperson. + + Add a buyer and a salesperson to the ``estate.property`` model using the two common models + mentioned above. They should be added in a new tab of the form view, as depicted in this section's **Goal**. + + The default value for the salesperson must be the current user. The buyer should not be copied. + + Tip: to get the default value, check the note below or look at an example + `here `__. + +.. note:: + + The object ``self.env`` gives access to request parameters and other useful + things: + + - ``self.env.cr`` or ``self._cr`` is the database *cursor* object; it is + used for querying the database + - ``self.env.uid`` or ``self._uid`` is the current user's database id + - ``self.env.user`` is the current user's record + - ``self.env.context`` or ``self._context`` is the context dictionary + - ``self.env.ref(xml_id)`` returns the record corresponding to an XML id + - ``self.env[model_name]`` returns an instance of the given model + +Now let's have a look at other types of links. + +Many2many +========= + +**Reference**: the documentation related to this topic can be found in +:class:`~odoo.fields.Many2many`. + +.. note:: + + **Goal**: at the end of this section: + + - a new ``estate.property.tag`` model should be created with the corresponding menu and action. + + .. image:: 08_relations/media/property_tag.png + :align: center + :alt: Property tag + + - tags should be added to the ``estate.property`` model: + + .. image:: 08_relations/media/property_many2many.png + :align: center + :alt: Property + +In our real estate module, we want to define the concept of property tags. A property tag +is, for example, a property which is 'cozy' or 'renovated'. + +A property can have **many** tags and a tag can be assigned to **many** properties. +This is supported by the **many2many** concept. + +A many2many is a bidirectional multiple relationship: any record on one side can be related to any +number of records on the other side. For example, in order to define a link to the +``account.tax`` model on our test model, we can write:: + + tax_ids = fields.Many2many("account.tax", string="Taxes") + +By convention, many2many fields have the ``_ids`` suffix. This means that several taxes can be +added to our test model. It behaves as a list of records, meaning that accessing the data must be +done in a loop:: + + for tax in my_test_object.tax_ids: + print(tax.name) + +A list of records is known as a *recordset*, i.e. an ordered collection of records. It supports +standard Python operations on collections, such as ``len()`` and ``iter()``, plus extra set +operations like ``recs1 | recs2``. + +.. exercise:: Add the Real Estate Property Tag table. + + - Create the ``estate.property.tag`` model and add the following field: + + ========================= ========================= ========================= + Field Type Attributes + ========================= ========================= ========================= + name Char required + ========================= ========================= ========================= + + - Add the menus as displayed in this section's **Goal** + - Add the field ``tag_ids`` to your ``estate.property`` model and in its form and tree views + + Tip: in the view, use the ``widget="many2many_tags"`` attribute as demonstrated + `here `__. + The ``widget`` attribute will be explained in detail in :ref:`a later chapter of the training `. + For now, you can try to adding and removing it and see the result ;-) + +One2many +======== + +**Reference**: the documentation related to this topic can be found in +:class:`~odoo.fields.One2many`. + +.. note:: + + **Goal**: at the end of this section: + + - a new ``estate.property.offer`` model should be created with the corresponding form and tree view. + - offers should be added to the ``estate.property`` model: + + .. image:: 08_relations/media/property_offer.png + :align: center + :alt: Property offers + +In our real estate module, we want to define the concept of property offers. A property offer +is an amount a potential buyer offers to the seller. The offer can be lower or higher than the +expected price. + +An offer applies to **one** property, but the same property can have **many** offers. +The concept of **many2one** appears once again. However, in this case we want to display the list +of offers for a given property so we will use the **one2many** concept. + +A one2many is the inverse of a many2one. For example, we defined +on our test model a link to the ``res.partner`` model thanks to the field ``partner_id``. +We can define the inverse relation, i.e. the list of test models linked to our partner:: + + test_ids = fields.One2many("test.model", "partner_id", string="Tests") + +The first parameter is called the ``comodel`` and the second parameter is the field we want to +inverse. + +By convention, one2many fields have the ``_ids`` suffix. They behave as a list of records, meaning +that accessing the data must be done in a loop:: + + for test in partner.test_ids: + print(test.name) + +.. danger:: + + Because a :class:`~odoo.fields.One2many` is a virtual relationship, + there *must* be a :class:`~odoo.fields.Many2one` field defined in the comodel. + +.. exercise:: Add the Real Estate Property Offer table. + + - Create the ``estate.property.offer`` model and add the following fields: + + ========================= ================================ ============= ================= + Field Type Attributes Values + ========================= ================================ ============= ================= + price Float + status Selection no copy Accepted, Refused + partner_id Many2one (``res.partner``) required + property_id Many2one (``estate.property``) required + ========================= ================================ ============= ================= + + - Create a tree view and a form view with the ``price``, ``partner_id`` and ``status`` fields. No + need to create an action or a menu. + - Add the field ``offer_ids`` to your ``estate.property`` model and in its form view as + depicted in this section's **Goal**. + +There are several important things to notice here. First, we don't need an action or a menu for all +models. Some models are intended to be accessed only through another model. This is the case in our +exercise: an offer is always accessed through a property. + +Second, despite the fact that the ``property_id`` field is required, we did not include it in the +views. How does Odoo know which property our offer is linked to? Well that's part of the +magic of using the Odoo framework: sometimes things are defined implicitly. When we create +a record through a one2many field, the corresponding many2one is populated automatically +for convenience. + +Still alive? This chapter is definitely not the easiest one. It introduced a couple of new concepts +while relying on everything that was introduced before. The +:ref:`next chapter ` will be lighter, don't worry ;-) diff --git a/content/developer/howtos/rdtraining/08_relations/media/property_many2many.png b/content/developer/howtos/rdtraining/08_relations/media/property_many2many.png new file mode 100644 index 000000000..189a38355 Binary files /dev/null and b/content/developer/howtos/rdtraining/08_relations/media/property_many2many.png differ diff --git a/content/developer/howtos/rdtraining/08_relations/media/property_many2one.png b/content/developer/howtos/rdtraining/08_relations/media/property_many2one.png new file mode 100644 index 000000000..b9b947e07 Binary files /dev/null and b/content/developer/howtos/rdtraining/08_relations/media/property_many2one.png differ diff --git a/content/developer/howtos/rdtraining/08_relations/media/property_offer.png b/content/developer/howtos/rdtraining/08_relations/media/property_offer.png new file mode 100644 index 000000000..dad5b29e3 Binary files /dev/null and b/content/developer/howtos/rdtraining/08_relations/media/property_offer.png differ diff --git a/content/developer/howtos/rdtraining/08_relations/media/property_tag.png b/content/developer/howtos/rdtraining/08_relations/media/property_tag.png new file mode 100644 index 000000000..dd128ae8b Binary files /dev/null and b/content/developer/howtos/rdtraining/08_relations/media/property_tag.png differ diff --git a/content/developer/howtos/rdtraining/08_relations/media/property_type.png b/content/developer/howtos/rdtraining/08_relations/media/property_type.png new file mode 100644 index 000000000..350128fc9 Binary files /dev/null and b/content/developer/howtos/rdtraining/08_relations/media/property_type.png differ diff --git a/content/developer/howtos/rdtraining/09_compute_onchange.rst b/content/developer/howtos/rdtraining/09_compute_onchange.rst new file mode 100644 index 000000000..5ae8cbe22 --- /dev/null +++ b/content/developer/howtos/rdtraining/09_compute_onchange.rst @@ -0,0 +1,305 @@ +.. _howto/rdtraining/09_compute_onchange: + +======================================== +Chapter 9: Computed Fields And Onchanges +======================================== + +The :ref:`relations between models ` 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 the values of other fields and other times we want to help the +user with data entry. + +These cases are supported by the concepts of computed fields and onchanges. Although this chapter is +not technically complex, the semantics of both concepts is very important. +This is also the first time we will write Python logic. Until now we haven't written anything +other than class definitions and field declarations. + +Computed Fields +=============== + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/fields/compute`. + +.. note:: + + **Goal**: at the end of this section: + + - In the property model, the total area and the best offer should be computed: + + .. image:: 09_compute_onchange/media/compute.gif + :align: center + :alt: Compute fields + + - In the property offer model, the validity date should be computed and can be updated: + + .. image:: 09_compute_onchange/media/compute_inverse.gif + :align: center + :alt: Compute field with inverse + +In our real estate module, we have defined the living area as well as the garden area. It is then +natural to define the total area as the sum of both fields. We will use the concept of a computed +field for this, i.e. the value of a given field will be computed from the value of other fields. + +So far fields have been stored directly in and retrieved directly from the +database. Fields can also be *computed*. In this case, the field's value is not +retrieved from the database but computed on-the-fly by calling a method of the +model. + +To create a computed field, create a field and set its attribute +:attr:`~odoo.fields.Field.compute` to the name of a method. The computation +method should set the value of the computed field for every record in +``self``. + +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:`howto/rdtraining/01_architecture`). Private methods have a name starting with an underscore ``_``. + +Dependencies +------------ + +The value of a computed field usually depends on the values of other fields in +the computed record. The ORM expects the developer to specify those dependencies +on the compute method with the decorator :func:`~odoo.api.depends`. +The given dependencies are used by the ORM to trigger the recomputation of the +field whenever some of its dependencies have been modified:: + + from odoo import api, fields, models + + class TestComputed(models.Model): + _name = "test.computed" + + total = fields.Float(compute="_compute_total") + amount = fields.Float() + + @api.depends("amount") + def _compute_total(self): + for record in self: + record.total = 2.0 * record.amount + +.. note:: ``self`` is a collection. + :class: aphorism + + The object ``self`` is a *recordset*, i.e. an ordered collection of + records. It supports the standard Python operations on collections, e.g. + ``len(self)`` and ``iter(self)``, plus extra set operations such as ``recs1 | + recs2``. + + Iterating over ``self`` gives the records one by one, where each record is + itself a collection of size 1. You can access/assign fields on single + records by using the dot notation, e.g. ``record.name``. + +Many examples of computed fields can be found in Odoo. +`Here `__ +is a simple one. + +.. exercise:: Compute the total area. + + - Add the ``total_area`` field to ``estate.property``. It is defined as the sum of the + ``living_area`` and the ``garden_area``. + + - Add the field in the form view as depicted on the first image of this section's **Goal**. + +For relational fields it's possible to use paths through a field as a dependency:: + + description = fields.Char(compute="_compute_description") + partner_id = fields.Many2one("res.partner") + + @api.depends("partner_id.name") + def _compute_description(self): + for record in self: + record.description = "Test for partner %s" % record.partner_id.name + +The example is given with a :class:`~odoo.fields.Many2one`, but it is valid for +:class:`~odoo.fields.Many2many` or a :class:`~odoo.fields.One2many`. An example can be found +`here `__. + +Let's try it in our module with the following exercise! + +.. exercise:: Compute the best offer. + + - Add the ``best_price`` field to ``estate.property``. It is defined as the highest (i.e. maximum) of the + offers' ``price``. + + - Add the field to the form view as depicted in the first image of this section's **Goal**. + + Tip: you might want to try using the :meth:`~odoo.models.BaseModel.mapped` method. See + `here `__ + for a simple example. + +Inverse Function +---------------- + +You might have noticed that computed fields are read-only by default. This is expected since the +user is not supposed to set a value. + +In some cases, it might be useful to still be able to set a value directly. In our real estate example, +we can define a validity duration for an offer and set a validity date. We would like to be able +to set either the duration or the date with one impacting the other. + +To support this Odoo provides the ability to use an ``inverse`` function:: + + from odoo import api, fields, models + + class TestComputed(models.Model): + _name = "test.computed" + + total = fields.Float(compute="_compute_total", inverse="_inverse_total") + amount = fields.Float() + + @api.depends("amount") + def _compute_total(self): + for record in self: + record.total = 2.0 * record.amount + + def _inverse_total(self): + for record in self: + record.amount = record.total / 2.0 + +An example can be found +`here `__. + +A compute method sets the field while an inverse method sets the field's +dependencies. + +Note that the ``inverse`` method is called when saving the record, while the +``compute`` method is called at each change of its dependencies. + +.. exercise:: Compute a validity date for offers. + + - Add the following fields to the ``estate.property.offer`` model: + + ========================= ========================= ========================= + Field Type Default + ========================= ========================= ========================= + validity Integer 7 + date_deadline Date + ========================= ========================= ========================= + + Where ``date_deadline`` is a computed field which is defined as the sum of two fields from + the offer: the ``create_date`` and the ``validity``. Define an appropriate inverse function + so that the user can set either the date or the validity. + + Tip: the ``create_date`` is only filled in when the record is created, therefore you will + need a fallback to prevent crashing at time of creation. + + - Add the fields in the form view and the list view as depicted on the second image of this section's **Goal**. + +Additional Information +---------------------- + +Computed fields are **not stored** in the database by default. Therefore it is **not +possible** to search on a computed field unless a ``search`` method is defined. This topic is beyond the scope +of this training, so we won't cover it. An example can be found +`here `__. + +Another solution is to store the field with the ``store=True`` attribute. While this is +usually convenient, pay attention to the potential computation load added to your model. Lets re-use +our example:: + + description = fields.Char(compute="_compute_description", store=True) + partner_id = fields.Many2one("res.partner") + + @api.depends("partner_id.name") + def _compute_description(self): + for record in self: + record.description = "Test for partner %s" % record.partner_id.name + +Every time the partner ``name`` is changed, the ``description`` is automatically recomputed for +**all the records** referring to it! This can quickly become prohibitive to recompute when +millions of records need recomputation. + +It is also worth noting that a computed field can depend on another computed field. The ORM is +smart enough to correctly recompute all the dependencies in the right order... but sometimes at the +cost of degraded performance. + +In general performance must always be kept in mind when defining computed fields. The more +complex is your field to compute (e.g. with a lot of dependencies or when a computed field +depends on other computed fields), the more time it will take to compute. Always take some time to +evaluate the cost of a computed field beforehand. Most of the time it is only when your code +reaches a production server that you realize it slows down a whole process. Not cool :-( + +Onchanges +========= + +**Reference**: the documentation related to this topic can be found in +:func:`~odoo.api.onchange`: + +.. note:: + + **Goal**: at the end of this section, enabling the garden will set a default area of 10 and + an orientation to North. + + .. image:: 09_compute_onchange/media/onchange.gif + :align: center + :alt: Onchange + +In our real estate module, we also want to help the user with data entry. When the 'garden' +field is set, we want to give a default value for the garden area as well as the orientation. +Additionally, when the 'garden' field is unset we want the garden area to reset to zero and the +orientation to be removed. In this case, the value of a given field modifies the value of +other fields. + +The 'onchange' mechanism provides a way for the client interface to update a +form without saving anything to the database whenever the user has filled in +a field value. To achieve this, we define a method where ``self`` represents +the record in the form view and decorate it with :func:`~odoo.api.onchange` +to specify which field it is triggered by. Any change you make on +``self`` will be reflected on the form:: + + from odoo import api, fields, models + + class TestOnchange(models.Model): + _name = "test.onchange" + + name = fields.Char(string="Name") + description = fields.Char(string="Description") + partner_id = fields.Many2one("res.partner", string="Partner") + + @api.onchange("partner_id") + def _onchange_partner_id(self): + self.name = "Document for %s" % (self.partner_id.name) + self.description = "Default description for %s" % (self.partner_id.name) + +In this example, changing the partner will also change the name and the description values. It is up to +the user whether or not to change the name and description values afterwards. Also note that we do not +loop on ``self``, this is because the method is only triggered in a form view, where ``self`` is always +a single record. + +.. exercise:: Set values for garden area and orientation. + + Create an ``onchange`` in the ``estate.property`` model in order to set values for the + garden area (10) and orientation (North) when garden is set to True. When unset, clear the fields. + +Additional Information +---------------------- + +Onchanges methods can also return a non-blocking warning message +(`example `__). + +How to use them? +================ + +There is no strict rule for the use of computed fields and onchanges. + +In many cases, both computed fields and onchanges may be used to achieve the same result. Always +prefer computed fields since they are also triggered outside of the context of a form view. Never +ever use an onchange to add business logic to your model. This is a **very bad** idea since +onchanges are not automatically triggered when creating a record programmatically; they are only +triggered in the form view. + +The usual pitfall of computed fields and onchanges is trying to be 'too smart' by adding too much +logic. This can have the opposite result of what was expected: the end user is confused from +all the automation. + +Computed fields tend to be easier to debug: such a field is set by a given method, so it's easy to +track when the value is set. Onchanges, on the other hand, may be confusing: it is very difficult to +know the extent of an onchange. Since several onchange methods may set the same fields, it +easily becomes difficult to track where a value is coming from. + +When using stored computed fields, pay close attention to the dependencies. When computed fields +depend on other computed fields, changing a value can trigger a large number of recomputations. +This leads to poor performance. + +In the :ref:`next chapter`, we'll see how we can trigger some business +logic when buttons are clicked. diff --git a/content/developer/howtos/rdtraining/09_compute_onchange/media/compute.gif b/content/developer/howtos/rdtraining/09_compute_onchange/media/compute.gif new file mode 100644 index 000000000..46cf10ecf Binary files /dev/null and b/content/developer/howtos/rdtraining/09_compute_onchange/media/compute.gif differ diff --git a/content/developer/howtos/rdtraining/09_compute_onchange/media/compute_inverse.gif b/content/developer/howtos/rdtraining/09_compute_onchange/media/compute_inverse.gif new file mode 100644 index 000000000..3c9071627 Binary files /dev/null and b/content/developer/howtos/rdtraining/09_compute_onchange/media/compute_inverse.gif differ diff --git a/content/developer/howtos/rdtraining/09_compute_onchange/media/onchange.gif b/content/developer/howtos/rdtraining/09_compute_onchange/media/onchange.gif new file mode 100644 index 000000000..eac5f8c12 Binary files /dev/null and b/content/developer/howtos/rdtraining/09_compute_onchange/media/onchange.gif differ diff --git a/content/developer/howtos/rdtraining/10_actions.rst b/content/developer/howtos/rdtraining/10_actions.rst new file mode 100644 index 000000000..7f64e9d1b --- /dev/null +++ b/content/developer/howtos/rdtraining/10_actions.rst @@ -0,0 +1,139 @@ +.. _howto/rdtraining/10_actions: + +================================== +Chapter 10: Ready For Some Action? +================================== + +So far we have mostly built our module by declaring fields and views. We just introduced business +logic in the :ref:`previous chapter ` thanks to computed fields +and onchanges. In any real business scenario, we would want to link some business logic to action buttons. +In our real estate example, we would like to be able to: + +- cancel or set a property as sold +- accept or refuse an offer + +One could argue that we can already do these things by changing the state manually, but +this is not really convenient. Moreover, we want to add some extra processing: when an offer is +accepted we want to set the selling price and the buyer for the property. + +Action Type +=========== + +**Reference**: the documentation related to this topic can be found in +:ref:`reference/actions` and :ref:`reference/exceptions`. + +.. note:: + + **Goal**: at the end of this section: + + - You should be able to cancel or set a property as sold: + + .. image:: 10_actions/media/property.gif + :align: center + :alt: Cancel and set to sold + + A canceled property cannot be sold and a sold property cannot be canceled. For the sake of + clarity, the ``state`` field has been added on the view. + + - You should be able to accept or refuse an offer: + + .. image:: 10_actions/media/offer_01.gif + :align: center + :alt: Accept or refuse an offer + + - Once an offer is accepted, the selling price and the buyer should be set: + + .. image:: 10_actions/media/offer_02.gif + :align: center + :alt: Accept an offer + +In our real estate module, we want to link business logic with some buttons. The most common way to +do this is to: + +- Add a button in the view, for example in the ``header`` of the view: + +.. code-block:: xml + +
+
+
+ + + +
+ +- and link this button to business logic: + +.. code-block:: python + + from odoo import fields, models + + class TestAction(models.Model): + _name = "test.action" + + name = fields.Char() + + def action_do_something(self): + for record in self: + record.name = "Something" + return True + +By assigning ``type="object"`` to our button, the Odoo framework will execute a Python method +with ``name="action_do_something"`` on the given model. + +The first important detail to note is that our method name isn't prefixed with an underscore +(``_``). This makes our method a **public** method, which can be called directly from the Odoo +interface (through an RPC call). Until now, all methods we created (compute, onchange) were called +internally, so we used **private** methods prefixed by an underscore. You should always define your +methods as private unless they need to be called from the user interface. + +Also note that we loop on ``self``. Always assume that a method can be called on multiple records; it's +better for reusability. + +Finally, a public method should always return something so that it can be called through XML-RPC. +When in doubt, just ``return True``. + +There are hundreds of examples in the Odoo source code. One example is this +`button in a view `__ +and its +`corresponding Python method `__ + +.. exercise:: Cancel and set a property as sold. + + - Add the buttons 'Cancel' and 'Sold' to the ``estate.property`` model. A canceled property + cannot be set as sold, and a sold property cannot be canceled. + + Refer to the first image of the **Goal** for the expected result. + + Tip: in order to raise an error, you can use the :ref:`UserError` + function. There are plenty of examples in the Odoo source code ;-) + + - Add the buttons 'Accept' and 'Refuse' to the ``estate.property.offer`` model. + + Refer to the second image of the **Goal** for the expected result. + + Tip: to use an icon as a button, have a look + `at this example `__. + + - When an offer is accepted, set the buyer and the selling price for the corresponding property. + + Refer to the third image of the **Goal** for the expected result. + + Pay attention: in real life only one offer can be accepted for a given property! + +Object Type +=========== + +In :ref:`howto/rdtraining/06_firstui`, we created an action that was linked to a menu. +You may be wondering if it is possible to link an action to a button. Good news, it is! One +way to do it is: + +.. code-block:: xml + +