changeset 0cd242b9bfc4 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset&node=0cd242b9bfc4
description:
        Add module tutorial

        review32711002
diffstat:

 CHANGELOG                               |    1 +
 doc/index.rst                           |    7 +-
 doc/ref/models.rst                      |    1 +
 doc/topics/modules/index.rst            |    6 +
 doc/topics/reports/index.rst            |    5 +-
 doc/tutorial/index.rst                  |   10 +
 doc/tutorial/module/anatomy.rst         |   23 +++
 doc/tutorial/module/default_values.rst  |   46 ++++++
 doc/tutorial/module/domains.rst         |   95 ++++++++++++++
 doc/tutorial/module/extend.rst          |  111 ++++++++++++++++
 doc/tutorial/module/function_fields.rst |   90 +++++++++++++
 doc/tutorial/module/index.rst           |   40 ++++++
 doc/tutorial/module/model.rst           |  123 ++++++++++++++++++
 doc/tutorial/module/on_change.rst       |   66 +++++++++
 doc/tutorial/module/report.rst          |  122 ++++++++++++++++++
 doc/tutorial/module/setup.rst           |   67 ++++++++++
 doc/tutorial/module/setup_database.rst  |   31 ++++
 doc/tutorial/module/states.rst          |   92 +++++++++++++
 doc/tutorial/module/table_query.rst     |  146 ++++++++++++++++++++++
 doc/tutorial/module/view.rst            |  212 ++++++++++++++++++++++++++++++++
 doc/tutorial/module/wizard.rst          |  171 +++++++++++++++++++++++++
 doc/tutorial/module/workflow.rst        |  166 +++++++++++++++++++++++++
 22 files changed, 1628 insertions(+), 3 deletions(-)

diffs (1785 lines):

diff -r aaecbbb8e595 -r 0cd242b9bfc4 CHANGELOG
--- a/CHANGELOG Tue Apr 12 10:47:17 2022 +0200
+++ b/CHANGELOG Tue Apr 12 13:09:01 2022 +0200
@@ -1,3 +1,4 @@
+* Add module tutorial
 * Test all XML view and SVG icon files are used
 * Add notification message
 * Add validate_fields to ModelStorage
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/index.rst
--- a/doc/index.rst     Tue Apr 12 10:47:17 2022 +0200
+++ b/doc/index.rst     Tue Apr 12 13:09:01 2022 +0200
@@ -13,6 +13,9 @@
   :ref:`Setup a database <topics-setup-database>` |
   :ref:`Start the server <topics-start-server>`
 
+    * **Tutorials:**
+      :ref:`Create a module <tutorial-module>`
+
 The model layer
 ===============
 
@@ -43,7 +46,8 @@
 =======================
 
 * **Modules**
-  :ref:`Module definition <topics-modules>`
+  :ref:`Module definition <topics-modules>` |
+  :ref:`Create a module <tutorial-module>`
 
 Contents
 ========
@@ -53,6 +57,7 @@
 
    topics/index
    ref/index
+   tutorial/index
 
 Indices, glossary and tables
 ============================
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/ref/models.rst
--- a/doc/ref/models.rst        Tue Apr 12 10:47:17 2022 +0200
+++ b/doc/ref/models.rst        Tue Apr 12 13:09:01 2022 +0200
@@ -689,6 +689,7 @@
 .. attribute:: Workflow._transition_state
 
    The name of the field that will be used to check state transition.
+   The default value is 'state'.
 
 .. attribute:: Workflow._transitions
 
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/topics/modules/index.rst
--- a/doc/topics/modules/index.rst      Tue Apr 12 10:47:17 2022 +0200
+++ b/doc/topics/modules/index.rst      Tue Apr 12 13:09:01 2022 +0200
@@ -20,6 +20,8 @@
 :file:`tryton.cfg`
    A Configuration file that describes the Tryton module.
 
+.. _topics-modules-init:
+
 :file:`__init__.py` file
 ------------------------
 
@@ -27,6 +29,7 @@
 It must contains a method named ``register()`` that must register to the pool
 all the objects of the module.
 
+.. _topics-modules-tryton-cfg:
 
 :file:`tryton.cfg` file
 -----------------------
@@ -69,6 +72,8 @@
 
 The Python files define the models for the modules.
 
+.. _topics-modules-xml-files:
+
 XML Files
 =========
 
@@ -219,3 +224,4 @@
 .. _time: http://docs.python.org/library/time.html
 .. _Decimal: https://docs.python.org/library/decimal.html
 .. _datetime: https://docs.python.org/library/datetime.html
+.. _RNG: https://en.wikipedia.org/wiki/RELAX_NG
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/topics/reports/index.rst
--- a/doc/topics/reports/index.rst      Tue Apr 12 10:47:17 2022 +0200
+++ b/doc/topics/reports/index.rst      Tue Apr 12 13:09:01 2022 +0200
@@ -118,7 +118,8 @@
 The directives that are supported by relatorio can be found here: `Quick
 Example`_ .
 
-See Genshi's documentation for more information: `Genshi XML Templates`_
+See Genshi's documentation for more information: `Genshi XML Template
+Language`_.
 
 Examples
 ^^^^^^^^
@@ -243,7 +244,7 @@
 
             return context
 
-.. _Genshi XML Templates: 
http://genshi.edgewall.org/wiki/Documentation/0.5.x/xml-templates.html
+.. _Genshi XML Template Language: 
https://genshi.edgewall.org/wiki/Documentation/xml-templates.html
 
 .. _Quick Example: https://relatorio.readthedocs.io/en/latest/quickexample.html
 
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/index.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/index.rst    Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,10 @@
+.. _tutorial-index:
+
+=========
+Tutorials
+=========
+
+.. toctree::
+   :maxdepth: 1
+
+   module/index
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/anatomy.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/anatomy.rst   Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,23 @@
+.. _tutorial-module-anatomy:
+
+Anatomy of a module
+===================
+
+A Tryton module is a `python module`_ thus it is a directory that contains a
+:ref:`__init__.py <topics-modules-init>` file.
+
+A Tryton module must also contain a :ref:`tryton.cfg
+<topics-modules-tryton-cfg>` file which is used to define the dependencies
+between modules and also lists the :ref:`XML files <topics-modules-xml-files>`
+that must be loaded by Tryton.
+
+Usually a module will define views used in the user interface, those views are
+described by XML files stored in the :file:`view` directory.
+
+Translations are handled with `po files`_ that sit in the :file:`locale`
+directory, one file per language.
+
+Let's continue with :ref:`creating the models <tutorial-module-model>`
+
+.. _`python module`: https://docs.python.org/tutorial/modules.html
+.. _`po files`: 
https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/default_values.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/default_values.rst    Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,46 @@
+.. _tutorial-module-default-values:
+
+Set default values
+==================
+
+Default values are useful to save time for users when entering data.
+On Tryton :ref:`default values <topics-fields_default_value>` are computed on
+server side and they will be set by the client when creating a new record if
+the field is shown on the view.
+If the field is not shown on the view, the server will set this values when
+storing the new records in the database.
+
+In order to define a default value for a field you should define a class method
+named ``default_<field_name>`` that returns the default value.
+For example to add today as the default date of our ``Opportunity`` model the
+following class method is added in :file:`opportunity.py` file:
+
+.. code-block:: python
+
+    from trytond.pool import Pool
+    ...
+    class Opportunity(ModelSQL, ModelView):
+        ...
+        @classmethod
+        def default_start_date(cls):
+            pool = Pool()
+            Date = pool.get('ir.date')
+            return Date.today()
+
+.. _tutorial-module-calling-other-classes:
+
+Call other model methods
+------------------------
+
+In the previous example we called the ``today`` method of the ``ir.date`` model
+from the :class:`~trytond.pool.Pool` instance.
+The :attr:`~trytond.model.Model.__name__` value is used to get the class.
+It is very important to get the class from the pool instead of using a normal
+Python import, because the pool ensures that all of the extensions are applied
+depending on the activated modules.
+For example, if we have the company module also activated the correct timezone
+for the user company will be used for computing the today value.
+
+Great, you have learned how to define default values, and how to call methods
+defined on other classes in the pool.
+Let's continue with :ref:`reacting on user input <tutorial-module-on-change>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/domains.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/domains.rst   Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,95 @@
+.. _tutorial-module-domains:
+
+Add domain restriction to fields
+================================
+
+One common requirement is to add restrictions to the possible value of a field.
+For example we can define the following restrictions:
+
+* The value of a numeric field must be greater that zero.
+* The value of another field must be greater than the value of other.
+* Related record must have fields with specific values.
+  For example allow to select only products of kind *service*.
+
+This is represented using a domain clause.
+The domain clause syntax is explained on :ref:`domain reference
+<topics-domain>`.
+
+A very interesting thing of the domain, is that the client evaluates them, so:
+
+* If we set a value that invalidate the domain of some fields, they are marked.
+  A notification is displayed before saving.
+* When searching for a related record, only the records that satisfy the domain
+  are available.
+  So it is not possible to select invalid records.
+* When creating a new related record, the client automatically enforces only
+  valid values.
+  Fields that can have only one value are filled and set read only.
+
+For example, it may be interesting to add the address of the party on our
+``Opportunity`` model.
+In this case we are interested on selecting only the addresses related to the
+party.
+
+Lets see how to do it:
+
+.. code-block:: python
+
+    from trytond.pyson import Eval
+    ...
+    class Opportunity(ModelSQL, ModelView):
+        ...
+        address = fields.Many2One(
+            'party.address', "Address",
+            domain=[
+                ('party', '=', Eval('party', -1)),
+                ])
+
+The domain uses the value of the party field with the
+:class:`~trytond.pyson.Eval` object.
+This defines a relation between party and address field.
+
+.. note::
+   It is up to you to add the new field to the views and update the database.
+
+Using conditional domains
+-------------------------
+
+Sometimes it is interesting to apply a domain only if another field is set.
+For example we want to ensure the start date is before the end date but both
+fields are optionals, so we don't want to apply any domain if they are empty.
+This can be solved by using a conditional domain.
+
+Lets see how we can achieve it:
+
+.. code-block:: python
+
+    from trytond.pyson import If, Bool, Eval
+    ...
+    class Opportunity(ModelSQL, ModelView):
+        ...
+        start_date = fields.Date(
+            "Start Date", required=True,
+            domain=[
+                If(Bool(Eval('end_date')),
+                    ('start_date', '<=', Eval('end_date')),
+                    ())])
+        end_date = fields.Date(
+            "End Date",
+            domain=[
+                If(Bool(Eval('end_date')),
+                    ('end_date', '>=', Eval('start_date')),
+                    ())])
+
+In this case we used the following statements:
+
+* :class:`~trytond.pyson.If` which expects three values:
+  (condition, true-statement, false-statement)
+  In this case we use to return a domain on when the condition is ``True`` and
+  return an empty domain on ``False``.
+* :class:`~trytond.pyson.Bool` used to convert the field value into boolean.
+
+All of the domains are :ref:`PSYON <ref-pyson>` statements.
+
+Great, you have learned to add constraint on the fields value.
+Let's continue with :ref:`adding a workflow <tutorial-module-workflow>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/extend.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/extend.rst    Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,111 @@
+.. _tutorial-module-extend:
+
+Extend model
+============
+
+Sometimes we want to extend an existing :class:`~trytond.model.Model` to add
+:class:`~trytond.model.fields.Field` or methods.
+This can be done using the extension mechanism of Tryton which can combine
+classes with the same ``__name__`` that are registered in the
+:class:`~trytond.pool.Pool`.
+
+Extend the Party model
+----------------------
+
+Let's add an ``opportunities`` field on the ``party.party`` model.
+The model in :file:`party.py` file of our module looks like this:
+
+.. code-block:: python
+
+    from trytond.model import fields
+    from trytond.pool import PoolMeta
+
+    class Party(metaclass=PoolMeta):
+        __name__ = 'party.party'
+        opportunities = fields.One2Many(
+            'training.opportunity', 'party', "Opportunities")
+
+This new class must be register in the :class:`~trytond.pool.Pool`.
+So in :file:`__init__.py` we add:
+
+.. code-block:: python
+
+    from . import party
+
+    def register():
+        Pool.register(
+            ...,
+            party.Party,
+            module='opportunity', type_='model')
+
+Extend the Party view
+---------------------
+
+Now that we added a new field to the ``party.party``
+:class:`~trytond.model.Model`, we can also add it the form view.
+This is done by adding a ``ir.ui.view`` record that inherit the party form view
+of the ``party`` module.
+Here is the content of the :file:`party.xml` file:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         <record model="ir.ui.view" id="party_view_form">
+            <field name="model">party.party</field>
+            <field name="inherit" ref="party.party_view_form"/>
+            <field name="name">party_form</field>
+         </record>
+      </data>
+   </tryton>
+
+The ``type`` is replaced by:
+
+``inherit``
+   A reference to the XML id of the view extended prefixed by the name of the
+   module where the view is declared.
+
+The content of the inheriting view must contain an XPath_ expression to define
+the position from which to include the partial view XML.
+Here is the content of the form view in :file:`view/party_form.xml`:
+
+.. code-block:: xml
+
+   <data>
+      <xpath expr="/form/notebook/page[@name='identifiers']" position="after">
+         <page name="opportunities" col="1">
+            <field name="opportunities"/>
+         </page>
+      </xpath>
+   </data>
+
+.. _XPath: https://en.wikipedia.org/wiki/XPath
+
+And finally we must declare the new XML data in the :file:`tryton.cfg` file:
+
+.. code-block:: ini
+
+   [tryton]
+   ...
+   xml:
+      ...
+      party.xml
+
+Update database
+---------------
+
+As we have defined new field and XML record, we need to update the database
+with:
+
+.. code-block:: console
+
+   $ trytond-admin -d test --all
+
+And restart the server and reconnect with the client to see the new field on
+the party:
+
+.. code-block:: console
+
+   $ trytond
+
+Let's use a :ref:`wizard to convert the opportunity <tutorial-module-wizard>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/function_fields.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/function_fields.rst   Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,90 @@
+.. _tutorial-module-function-fields:
+
+Add computed fields
+===================
+
+Computed fields can also be defined to avoid storing duplicated data in the
+database.
+For example, as we have the start date and the end date of our opportunity we
+can always compute the duration the opportunity lasts.
+This is done with a :class:`~trytond.model.fields.Function` field, which can be
+used to represent any kind of field.
+
+Lets see how this can be done in :file:`opportunity.py` file:
+
+.. code-block:: python
+
+    class Opportunity(ModelSQL, ModelView):
+        ...
+        duration = fields.Function(
+            fields.TimeDelta("Duration"), 'compute_duration')
+        ...
+        def compute_duration(self, name):
+            if self.start_date and self.end_date:
+                return self.end_date - self.start_date
+            return None
+
+The first parameter of the Function field is another
+:class:`~trytond.model.fields.Field` instance which defined the type of the
+field to mimic and on the second parameter, the
+:attr:`~trytond.model.fields.Function.getter`, we must specify the name of the
+method used to compute the value.
+
+:class:`~trytond.model.fields.Function` fields are read-only be default, but we
+can make them writable by defining a
+:attr:`~trytond.model.fields.Function.setter` attribute, which is a method to
+call to store the value.
+Similarly we can also provide a method to search or order on them.
+All the Function fields possibilities are explained on
+:class:`~trytond.model.fields.Function` fields reference.
+
+.. warning::
+   If you change the start date or the end date of the opportunity, you will
+   notice that the days value is not updated until the record is saved. That's
+   because function fields are computed only on server side.
+
+.. note::
+   We let you add the new field to the views.
+
+.. _tutorial-module-on-change-with:
+
+Combine Function fields and on_change_with
+------------------------------------------
+
+On previous steps we learned how :ref:`on_change <tutorial-module-on-change>`
+and :class:`~trytond.model.fields.Function` fields work.
+One interesting feature is to combine them to compute and update the value.
+So we can have a computed field that changes every time the user modifies one
+of the values of the form.
+
+It's a common pattern to use an ``on_change_with`` method as
+:attr:`~trytond.model.fields.Function.getter` of a
+:class:`~trytond.model.fields.Function` field, so the value is correctly
+computed on client side and then it reacts to the user input.
+
+In order to achieve it the following changes must be done in
+:file:`opportunity.py` file:
+
+.. code-block:: python
+
+    class Opportunity(ModelSQL, ModelView):
+        ...
+        duration = fields.Function(
+            fields.TimeDelta("Duration"), 'on_change_with_duration')
+        ...
+        @fields.depends('start_date', 'end_date')
+        def on_change_with_duration(self, name=None):
+            if self.start_date and self.end_date:
+                return self.end_date - self.start_date
+            return None
+
+The important facts are the following:
+
+    * Add :meth:`~trytond.model.fields.depends` decorator to react on user 
input
+    * Change the name of the method to ``on_change_with_<field_name>``
+    * Add a default None value for the name argument as it won't be supplied
+      when the client updates the values reacting to user input.
+
+Great, you designed a Function fields which reacts to the user input.
+Let's go to the next step to :ref:`add domain restrictions
+<tutorial-module-domains>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/index.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/index.rst     Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,40 @@
+.. _tutorial-module:
+
+===============
+Module Tutorial
+===============
+
+A step by step tutorial to create a first module.
+In this tutorial we will create a simple Tryton module to manage business
+opportunities.
+
+We will use Tryton and SQLite_ as database, so the installation should be 
pretty
+straightforward.
+
+.. TODO: rename module into training
+
+We will call our module ``opportunity``. This simple module will do the
+following things:
+
+.. toctree::
+   :maxdepth: 1
+
+   setup
+   setup_database
+   anatomy
+   model
+   view
+   default_values
+   on_change
+   function_fields
+   domains
+   workflow
+   states
+   extend
+   wizard
+   report
+   table_query
+
+Let's start with :ref:`installing Tryton for developers 
<tutorial-module-setup>`.
+
+.. _SQLite: https://sqlite.org/
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/model.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/model.rst     Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,123 @@
+.. _tutorial-module-model:
+
+Define model
+============
+
+The :ref:`models <ref-models>` are the base objects of a module to store and
+display data.
+The :class:`~trytond.model.ModelSQL` is the base class that implements the
+persistence in the SQL database.
+The :class:`~trytond.model.ModelView` is the base class that implements the
+view layer.
+And of course, a model would be useless without its :ref:`fields
+<ref-models-fields>`.
+
+Let's start with a simple model to store the opportunities with a description,
+a start and end date, a link to a party and an optional comment.
+Our model in :file:`opportunity.py` file currently looks like this:
+
+.. code-block:: python
+
+    from trytond.model import ModelSQL, fields
+
+    class Opportunity(ModelSQL):
+        "Opportunity"
+        __name__ = 'training.opportunity'
+        _rec_name = 'description'
+
+        description = fields.Char("Description", required=True)
+        start_date = fields.Date("Start Date", required=True)
+        end_date = fields.Date("End Date")
+        party = fields.Many2One('party.party', "Party", required=True)
+        comment = fields.Text("Comment")
+
+As you can see a Model must have a :attr:`~trytond.model.Model.__name__`
+attribute.
+This name is used to make reference to this object.
+It is also used to build the name of the SQL table to store the opportunity
+records in the database.
+
+The :attr:`~trytond.model.Model._rec_name` attribute defines the field that
+will be used to compute the name of the record.
+The name of the record is its textual representation.
+
+The ``party`` field is a relation field (Many2One_) to another Model of Tryton
+named ``party.party``.
+This model is defined by the ``party`` module.
+
+.. _Many2One: https://en.wikipedia.org/wiki/Many-to-one
+
+Register the model in the Pool
+------------------------------
+
+Once a Tryton model is defined, you need to register it in the
+:class:`~trytond.pool.Pool`.
+This is done in the :file:`__init__.py` file of your module with the following
+code:
+
+.. code-block:: python
+
+    from trytond.pool import Pool
+    from . import opportunity
+
+    def register():
+        Pool.register(
+            opportunity.Opportunity,
+            module='opportunity', type_='model')
+
+Models in the pool are inspected by Tryton when activating or updating a module
+in order to create or update the schema of the table in the database.
+
+Activate the opportunity module
+-------------------------------
+
+Now that we have a basic module, we will use it to create the related table
+into the :ref:`database created <tutorial-module-setup-database>`.
+
+First we must edit the :file:`tryton.cfg` file to specify that this module
+depends on the ``party`` and ``ir`` module.
+We need to do this because the ``Opportunity`` model contains the ``party``
+field which refers to the ``Party`` model.
+And we always need the ``ir`` module which is always included in Tryton server.
+
+Here is the content of our :file:`tryton.cfg` file:
+
+.. code-block:: ini
+
+   [tryton]
+   version=x.y.0
+   depends:
+      ir
+      party
+
+As we defined a new dependency, we must refresh the installation with:
+
+.. code-block:: console
+
+   $ python -m pip install --editable opportunity
+
+Now we can activate the ``opportunity`` module and its dependencies:
+
+.. code-block:: console
+
+    $ trytond-admin -d test -u opportunity --activate-dependencies
+
+This step has created the tables into your database.
+You can check it with the :command:`sqlite3` command line:
+
+.. code-block:: console
+
+   $ sqlite3 ~/db/test.sqlite '.schema training_opportunity'
+   CREATE TABLE "training_opportunity" (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      "comment" TEXT,
+      "create_uid" INTEGER,
+      "create_date" TIMESTAMP,
+      "description" VARCHAR,
+      "end_date" DATE,
+      "start_date" DATE,
+      "write_date" TIMESTAMP,
+      "party" INTEGER,
+      "write_uid" INTEGER);
+
+The next step will be :ref:`displaying record <tutorial-module-view>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/on_change.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/on_change.rst Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,66 @@
+.. _tutorial-module-on-change:
+
+React to user input
+===================
+
+Tryton provides a way to :ref:`change the value of a field
+<topics-fields_on_change>` depending on other fields.
+This computation is done on the server and the values are sent back to the
+client.
+The value is not stored on the server until the user saves the record.
+This is a great way to react to user inputs.
+
+For example, in order to set the end date of our opportunity depending on
+the start date, we can add the following instance method to ``Opportunity``
+class in :file:`opportunity.py` file:
+
+.. code-block:: python
+
+    import datetime as dt
+    ...
+    class Opportunity(ModelSQL, ModelView):
+        ...
+        @fields.depends('start_date')
+        def on_change_with_end_date(self):
+            if self.start_date:
+                return self.start_date + dt.timedelta(days=3)
+
+In this case the :meth:`~trytond.model.fields.depends` decorator indicates the
+names of the fields which will trigger the computation when their values are
+changed.
+You should take care to set all the fields used to make the computation because
+the server will have only access to those fields.
+This ensures that the client reacts to each field the computation depends on.
+
+We can also compute the values of other fields when some field change.
+In this case we use the ``on_change_<field_name>`` function instead of
+``on_change_with_<field_name>``.
+The :meth:`~trytond.model.fields.depends` decorator indicates the fields that
+will be available to compute the new values.
+In order to set the other fields value, we must assign them to the instance and
+the changes will be propagated to the client.
+
+So for example we can compute the description and the comment of our
+opportunity model depending on the party by adding this method to the
+``Opportunity`` class in :file:`opportunity.py` file:
+
+.. code-block:: python
+
+    class Opportunity(ModelSQL, ModelView):
+        ...
+        @fields.depends('party', 'description', 'comment')
+        def on_change_party(self):
+            if self.party:
+                if not self.description:
+                    self.description = self.party.rec_name
+                if not self.comment:
+                    lines = []
+                    if self.party.phone:
+                        lines.append("Tel: %s" % self.party.phone)
+                    if self.party.email:
+                        lines.append("Mail: %s" % self.party.email)
+                    self.comment = '\n'.join(lines)
+
+Great, you have learned how to compute values depending on other fields values.
+Let's continue with :ref:`adding computed fields
+<tutorial-module-function-fields>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/report.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/report.rst    Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,122 @@
+.. _tutorial-module-report:
+
+Create report
+=============
+
+A frequent requirement is to generate a printable document for a record.
+For that we use ``trytond.report.Report`` which provides the tooling to
+render OpenDocument_ based on relatorio_ template.
+
+First we create a ``trytond.report.Report`` class in :file:`opportunity.py`:
+
+.. code-block:: python
+
+    from trytond.report import Report
+    ...
+    class OpportunityReport(Report):
+        __name__ = 'training.opportunity.report'
+
+And we register it in the :class:`~trytond.pool.Pool` as type ``report`` in
+:file:`__init__.py`:
+
+.. code-block:: python
+
+    def register():
+        ...
+        Pool.register(
+            opportunity.OpportunityReport,
+            module='opportunity', type_='report')
+
+
+Now we have to create a ``ir.action.report`` and ``ir.action.keyword`` in
+:file:`opportunity.xml`:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         ...
+         <record model="ir.action.report" id="report_opportunity">
+            <field name="name">Opportunity</field>
+            <field name="report_name">training.opportunity.report</field>
+            <field name="model">training.opportunity</field>
+            <field name="report">opportunity/opportunity.fodt</field>
+            <field name="template_extension">odt</field>
+         </record>
+         <record model="ir.action.keyword" id="report_opportunity_keyword">
+            <field name="keyword">form_print</field>
+            <field name="model">training.opportunity,-1</field>
+            <field name="action" ref="report_opportunity"/>
+         </record>
+      </data>
+   </tryton>
+
+The ``ir.action.report`` links the ``trytond.report.Report`` with the
+:class:`~trytond.model.Model`.
+
+``name``
+   The string that is shown on the menu.
+``report_name``
+   The name of the ``trytond.report.Report``.
+``model``
+   The name of the :class:`~trytond.model.Model`.
+``report``
+   The path to the template file starting with the module directory.
+``template_extension``
+   The template format.
+
+And like for the :ref:`wizard <tutorial-module-wizard>`, the
+``ir.action.keyword`` makes the ``trytond.report.Report`` available as action
+to any ``training.opportunity``.
+
+Finally we create the OpenDocument_ template as :file:`opportunity.fodt` using
+LibreOffice_.
+We use the `Genshi XML Template Language`_ implemented by relatorio_ using
+``Placeholder Text``.
+The rendering context contains the variable ``records`` which is a list of
+selected record instances.
+
+Here is an example of the directives to insert in the document:
+
+.. code-block::
+
+   <for each="opportunity in records">
+   Opportunity: <opportunity.rec_name>
+   Party: <opportunity.party.rec_name>
+   Start Date: <format_date(opportunity.start_date) if opportunity.start_date 
else ''>
+   End Date: <format_date(opportunity.end_date) if opportunity.end_date else 
''>
+
+   Comment:
+   <for each="line in (opportunity.comment or '').splitlines()">
+   <line>
+   </for>
+   </for>
+
+.. note::
+   We must render text field line by line because OpenDocument does not
+   consider simple breakline.
+
+Update database
+---------------
+
+As we have registered new report and XML records, we need to update the
+database with:
+
+.. code-block:: console
+
+   $ trytond-admin -d test --all
+
+And restart the server and reconnect with the client to test rendering the
+report:
+
+.. code-block:: console
+
+   $ trytond
+
+Next we create a :ref:`a reporting model using SQL query
+<tutorial-module-table-query>`.
+
+.. _OpenDocument: https://en.wikipedia.org/wiki/OpenDocument
+.. _relatorio: https://relatorio.tryton.org/
+.. _LibreOffice: https://www.libreoffice.org/
+.. _Genshi XML Template Language: 
https://genshi.edgewall.org/wiki/Documentation/xml-templates.html
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/setup.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/setup.rst     Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,67 @@
+.. _tutorial-module-setup:
+
+Setup
+=====
+
+Create virtual environment
+--------------------------
+
+This step will cover the installation of tryton from a developer perspective.
+We will assume that you are already fluent with venv_ and pip_.
+
+Let's create a virtual environment inside your working directory:
+
+.. code-block:: console
+
+   $ python -m venv .venv
+   $ source .venv/bin/activate
+
+Install cookiecutter and mercurial
+----------------------------------
+
+To bootstrap the module, we provide a cookiecutter_ template.
+First we install cookiecutter_ and mercurial_ with:
+
+.. code-block:: console
+
+   $ python -m pip install cookiecutter mercurial
+
+Setup module
+------------
+
+The Tryton template can be rendered into a module with:
+
+.. code-block:: console
+
+   $ cookiecutter hg+https://hg.tryton.org/cookiecutter
+   module_name [my_module]: opportunity
+   prefix []: tuto
+   package_name [tuto_opportunity]:
+   version []: x.y.0
+   description []:
+   author [Tryton]: John Doe
+   author_email [[email protected]]: [email protected]
+   fullname []: John Doe
+   url [http://www.tryton.org/]: http://www.example.com/
+   keywords []:
+   test_with_scenario []:
+
+.. note::
+   The version number must use the same two first numbers as the Tryton series
+   wanted.
+
+Install module
+--------------
+
+Now we can install the new module in editable mode:
+
+.. code-block:: console
+
+   $ python -m pip install --editable opportunity
+
+Continue with :ref:`initializing the database <tutorial-module-setup-database>`
+
+.. _pip: https://pip.pypa.io/
+.. _venv: https://docs.python.org/library/venv.html
+.. _cookiecutter: https://pypi.org/project/cookiecutter/
+.. _mercurial: https://www.mercurial-scm.org/
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/setup_database.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/setup_database.rst    Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,31 @@
+.. _tutorial-module-setup-database:
+
+Initialize the database
+=======================
+
+By default Tryton, use an SQLite database stored in the folder :file:`db` of
+your home directory.
+This can be changed in the ``database`` section of the `configuration
+<topics-configuration>`.
+
+Now creating a Tryton database is only a matter of executing the following
+commands:
+
+.. code-block:: console
+
+   $ mkdir ~/db
+   $ touch ~/db/test.sqlite
+   $ trytond-admin -d test --all
+
+You will be prompted to set the administrator email and password.
+
+Once the database is initialized you can run the Tryton server:
+
+.. code-block:: console
+
+    $ trytond
+
+Connecting to the database using a Tryton client you will be greeted by the
+module configuration wizard.
+
+We will continue with :ref:`the anatomy of the module 
<tutorial-module-anatomy>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/states.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/states.rst    Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,92 @@
+.. _tutorial-module-states:
+
+Add dynamic state to fields
+===========================
+
+Sometimes you want to make fields read-only, invisible or required under
+certain conditions.
+This can be achieved using the :attr:`~trytond.model.fields.Field.states`
+attribute of the :class:`~trytond.model.fields.Field`.
+It is a dictionary with the keys ``readonly``, ``invisible`` and ``required``.
+The values are :class:`~trytond.pyson.PYSON` statements that are evaluated with
+the values of the record.
+
+In our example we make some fields read-only when the record is not in the
+state ``opportunity``, the "End Date" required for the ``converted`` and
+``lost`` state and make the comment invisible if empty:
+
+.. code-block:: python
+
+    class Opportunity(...):
+        ...
+        description = fields.Char(
+            "Description", required=True,
+            states={
+                'readonly': Eval('state') != 'draft',
+                })
+        start_date = fields.Date(
+            "Start Date", required=True,
+            states={
+                'readonly': Eval('state') != 'draft',
+                })
+        end_date = fields.Date(
+            "End Date",
+            states={
+                'readonly': Eval('state') != 'draft',
+                'required': Eval('state').in_(['converted', 'lost']),
+                })
+        party = fields.Many2One(
+            'party.party', "Party", required=True,
+            states={
+                'readonly': Eval('state') != 'draft',
+                })
+        address = fields.Many2One(
+            'party.address', "Address",
+            domain=[
+                ('party', '=', Eval('party')),
+                ],
+            states={
+                'readonly': Eval('state') != 'draft',
+                })
+        comment = fields.Text(
+            "Comment",
+            states={
+                'readonly': Eval('state') != 'draft',
+                'invisible': (
+                    (Eval('state') != 'draft') & ~Eval('comment')),
+                })
+
+It is also possible to set the ``readonly``, ``invisible`` and ``icon`` states
+on the :attr:`~trytond.model.ModelView._buttons`.
+So we can make invisible each button for the state in which the transition is
+not available:
+
+.. code-block:: python
+
+    class Opportunity(ModelSQL, ModelView):
+        ...
+        @classmethod
+        def __setup__(cls):
+            ...
+            cls._buttons.update({
+                    'convert': {
+                        'invisible': Eval('state') != 'draft',
+                        'depends': ['state'],
+                        },
+                    'lost': {
+                        'invisible': Eval('state') != 'draft',
+                        'depends': ['state'],
+                        },
+                    })
+
+.. note::
+   The fields in :class:`~trytond.pyson.Eval` statement must be added to the
+   ``depends`` attribute to register the field on which the states depend.
+
+Exercise
+--------
+
+As exercise we let you define the state for the button that reset to ``draft``
+state.
+
+Let's :ref:`extend the party model <tutorial-module-extend>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/table_query.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/table_query.rst       Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,146 @@
+.. _tutorial-module-table-query:
+
+Define aggregated model
+=======================
+
+Aggregated data are useful to analyze business.
+Tryton can provide such data using :class:`~trytond.model.ModelSQL` class which
+are not based on an existing table in the database but using a SQL query.
+This is done by defining a :meth:`~trytond.model.ModelSQL.table_query` method
+which returns a SQL ``FromItem``.
+
+Let's create a :class:`~trytond.model.ModelSQL` which aggregate the number of
+opportunity converted or lost per month.
+
+First we create a :class:`~trytond.model.ModelSQL` class which defines a
+:meth:`~trytond.model.ModelSQL.table_query` in :file:`opportunity.py`:
+
+.. code-block:: python
+
+    from sql import Literal
+    from sql.aggregate import Count, Min
+    from sql.functions import CurrentTimestamp, DateTrunc
+    ...
+    class OpportunityMonthly(ModelSQL, ModelView):
+        "Opportunity Monthly"
+        __name__ = 'training.opportunity.monthly'
+
+        month = fields.Date("Month")
+        converted = fields.Integer("Converted")
+        lost = fields.Integer("Lost")
+
+        @classmethod
+        def table_query(cls):
+            pool = Pool()
+            Opportunity = pool.get('training.opportunity')
+            opportunity = Opportunity.__table__()
+
+            month = cls.month.sql_cast(
+                DateTrunc('month', opportunity.end_date))
+            query = opportunity.select(
+                Literal(0).as_('create_uid'),
+                CurrentTimestamp().as_('create_date'),
+                Literal(None).as_('write_uid'),
+                Literal(None).as_('write_date'),
+                Min(opportunity.id).as_('id'),
+                month.as_('month'),
+                Count(
+                    Literal('*'),
+                    filter_=opportunity.state == 'converted').as_('converted'),
+                Count(
+                    Literal('*'),
+                    filter_=opportunity.state == 'lost').as_('lost'),
+                where=opportunity.state.in_(['converted', 'lost']),
+                group_by=[month])
+            return query
+
+.. note::
+   The table query must return a value for all the fields of the model but also
+   a unique ``id`` and a value for the create and write fields.
+
+.. note::
+   We get the SQL table from the :meth:`~trytond.model.ModelSQL.__table__`
+   method.
+
+.. note::
+   We use :meth:`~trytond.model.fields.Field.sql_cast` to convert the timestamp
+   returned by ``date_trunc`` into a :py:class:`~datetime.date`.
+
+Then as usual we register the :class:`~trytond.model.ModelSQL` class in the in
+the :class:`~trytond.pool.Pool` as type ``model`` in :file:`__init__.py`:
+
+.. code-block:: python
+
+    def register():
+        ...
+        Pool.register(
+            ...
+            opportunity.OpportunityMonthly,
+            module='opportunity', type_='model')
+
+And to display we create a list view and the menu entry in
+:file:`opportunity.xml`:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         ...
+         <record model="ir.ui.view" id="opportunity_monthly_view_list">
+            <field name="model">training.opportunity.monthly</field>
+            <field name="type">tree</field>
+            <field name="name">opportunity_monthly_list</field>
+         </record>
+
+         <record model="ir.action.act_window" 
id="act_opportunity_monthly_form">
+            <field name="name">Monthly Opportunities</field>
+            <field name="res_model">training.opportunity.monthly</field>
+         </record>
+         <record model="ir.action.act_window.view" 
id="act_opportunity_monthly_form_view">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="opportunity_monthly_view_list"/>
+            <field name="act_window" ref="act_opportunity_monthly_form"/>
+         </record>
+
+         <menuitem
+            parent="menu_opportunity"
+            action="act_opportunity_monthly_form"
+            sequence="50"
+            id="menu_opportunity_monthly_form"/>
+      </data>
+   </tryton>
+
+And now the view in :file:`view/opportunity_monthly_list.xml`:
+
+.. code-block:: xml
+
+   <tree>
+      <field name="month"/>
+      <field name="converted"/>
+      <field name="lost"/>
+   </tree>
+
+Update database
+---------------
+
+As we have registered new model and XML records, we need to update the database
+with:
+
+.. code-block:: console
+
+   $ trytond-admin -d test --all
+
+And restart the server and reconnect with the client to test computing
+aggregate:
+
+.. code-block:: console
+
+   $ trytond
+
+.. note::
+   As you can see the model behaves like the other models, except that you can
+   not create, delete nor write on them.
+
+This is all for your first module.
+If you want to learn more about Tryton, you can continue on :ref:`specific
+topics <topics-index>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/view.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/view.rst      Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,212 @@
+.. _tutorial-module-view:
+
+Display records
+===============
+
+Having records in the database is nice but we want the user to manage this
+records through the user interface.
+
+In order to denote that a model can be displayed in the interface, you have to
+inherit from :class:`~trytond.model.ModelView`:
+
+.. code-block:: python
+
+    from trytond.model import ModelSQL, ModelView
+    ...
+
+    class Opportunity(ModelSQL, ModelView):
+        ...
+
+When you inherit from :class:`~trytond.model.ModelView`, your model gains the
+methods required to display the data on Tryton clients.
+Those methods allow to retrieve the fields and the definition of the views used
+by a model, to apply attributes on view elements and they also provide all the
+machinery for :ref:`on_change <tutorial-module-on-change>` and
+:ref:`on_change_with <tutorial-module-on-change-with>`.
+
+Tryton Views
+------------
+
+In Tryton data can be displayed using different kind of views.
+The available view types and it's attributes are listed on the :ref:`Views
+<topics-views>` topic.
+
+Tryton views are usual Tryton records that are persisted into the database.
+This design choice means that views are extendable and that you can use the
+traditional Tryton concepts when interacting with them.
+
+
+Define views
+------------
+
+Views are defined in XML_ files and they contain one XML tag for each element
+displayed in the view.
+The root tag of the view defines the view type.
+An example view for our opportunity module will be as follows:
+
+Here is the content of the form view of opportunity in
+:file:`view/opportunity_form.xml`:
+
+.. code-block:: xml
+
+   <form>
+      <label name="party"/>
+      <field name="party"/>
+      <label name="description"/>
+      <field name="description"/>
+      <label name="start_date"/>
+      <field name="start_date"/>
+      <label name="end_date"/>
+      <field name="end_date"/>
+      <separator name="comment" colspan="4"/>
+      <field name="comment" colspan="4"/>
+   </form>
+
+And here is the content of the list view in :file:`view/opportunity_list.xml`:
+
+.. code-block:: xml
+
+   <tree>
+      <field name="party"/>
+      <field name="description"/>
+      <field name="start_date"/>
+      <field name="end_date"/>
+   </tree>
+
+The value of the ``name`` attribute for ``field`` and ``label`` tags is the
+name of the field attribute of the model.
+Each XML tag can contain different attributes to customize how the widgets
+are displayed in the views.
+The full reference can be found on the :ref:`Views <topics-views>` section.
+
+Once a views is defined it must be registered on the Tryton database in order
+to make the server know about them.
+In order to do so with should register it on a :ref:`XML file
+<topics-modules-xml-files>` specifying the following information:
+
+``model``
+   The name of the model of the view
+``type``
+   Possible values are: tree, form, calendar, graph, board
+``name``
+   The name of the XML file (without extension) in the :file:`view` folder
+   which contains the view definition
+
+Here is the content of the :file:`opportunity.xml` file:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         <record model="ir.ui.view" id="opportunity_view_form">
+            <field name="model">training.opportunity</field>
+            <field name="type">form</field>
+            <field name="name">opportunity_form</field>
+         </record>
+
+         <record model="ir.ui.view" id="opportunity_view_list">
+            <field name="model">training.opportunity</field>
+            <field name="type">tree</field>
+            <field name="name">opportunity_list</field>
+         </record>
+      </data>
+   </tryton>
+
+Now we have to declare the XML data file in the :file:`tryton.cfg` file:
+
+.. code-block:: ini
+
+   [tryton]
+   ...
+   xml:
+      opportunity.xml
+
+.. _XML: https://en.wikipedia.org/wiki/XML
+
+Create menu entry
+-----------------
+
+In order to show our models on the user menu we need an
+``ir.action.act_window`` and a menu entry.
+
+An action window is used to relate one or more views, usually a list and a form
+view.
+
+Here is the definition of the opportunities action to append into
+:file:`opportunity.xml`:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         ...
+         <record model="ir.action.act_window" id="act_opportunity_form">
+            <field name="name">Opportunities</field>
+            <field name="res_model">training.opportunity</field>
+         </record>
+         <record model="ir.action.act_window.view" 
id="act_opportunity_form_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="opportunity_view_list"/>
+            <field name="act_window" ref="act_opportunity_form"/>
+         </record>
+         <record model="ir.action.act_window.view" 
id="act_opportunity_form_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="opportunity_view_form"/>
+            <field name="act_window" ref="act_opportunity_form"/>
+         </record>
+      </data>
+   </tryton>
+
+A menu entry is created using the special ``menuitem`` XML tag which accepts
+the following values:
+
+``id``
+   Required XML identifier to refer this menu_item from other records.
+``sequence``
+   Used to define the order of the menus.
+``action``
+   The action to execute when clicking the menu.
+``name``
+   The string that will be shown on the menu.
+   If no name is entered and an action is set, the action name will be used.
+``parent``
+   The parent menu when creating a sub-menu.
+
+Lets add a menu entry to the :file:`opportunity.xml` file with:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         ...
+         <menuitem
+            name="Opportunities"
+            sequence="50"
+            id="menu_opportunity"/>
+         <menuitem
+            parent="menu_opportunity"
+            action="act_opportunity_form"
+            sequence="10"
+            id="menu_opportunity_form"/>
+      </data>
+   </tryton>
+
+
+Update database
+---------------
+
+As we have defined new XML records, we need to update the database with:
+
+.. code-block:: console
+
+   $ trytond-admin -d test --all
+
+And restart the server and reconnect with the client to see the new menu
+entries:
+
+.. code-block:: console
+
+   $ trytond
+
+Let's continue with :ref:`setting default values
+<tutorial-module-default-values>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/wizard.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/wizard.rst    Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,171 @@
+.. _tutorial-module-wizard:
+
+Create wizard
+=============
+
+Sometime you want to add functionalities to a model that do not suite the use
+of a button.
+For this kind of use case the :ref:`wizard <topics-wizard>` is the preferred
+solution.
+A wizard is a kind of `state machine`_ where states can be a form view, an
+action or transition.
+
+.. _state machine: https://en.wikipedia.org/wiki/Finite-state_machine
+
+Let's create a wizard that converts the opportunities by asking for the end 
date.
+
+First we define a :class:`~trytond.model.ModelView` class in
+:file:`opportunity.py`:
+
+.. code-block:: python
+
+    class ConvertStart(ModelView):
+        "Convert Opportunities"
+        __name__ = 'training.opportunity.convert.start'
+
+        end_date = fields.Date("End Date", required=True)
+
+And we register it in the :class:`~trytond.pool.Pool` in :file:`__init__.py`:
+
+.. code-block:: python
+
+    def register():
+        Pool.register(
+            ...,
+            opportunity.ConvertStart,
+            module='opportunity', type_='model')
+
+Then the form view record in :file:`opportunity.xml`:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         ...
+
+         <record model="ir.ui.view" id="opportunity_convert_start_view_form">
+            <field name="model">training.opportunity.convert.start</field>
+            <field name="type">form</field>
+            <field name="name">opportunity_convert_start_form</field>
+         </record>
+      </data>
+   </tryton>
+
+And the view in :file:`view/opportunity_convert_start_form.xml`:
+
+.. code-block:: xml
+
+   <form col="2">
+      <label string="Convert Opportunities?" id="convert_opportunities" 
colspan="2" xalign="0"/>
+      <label name="end_date"/>
+      <field name="end_date"/>
+   </form>
+
+Now we can define the :class:`~trytond.wizard.Wizard` with a ``start``
+:class:`~trytond.wizard.StateView` for the form and a ``convert``
+:class:`~trytond.wizard.StateTransition` in :file:`opportunity.py`:
+
+.. code-block:: python
+
+    from trytond.wizard import Wizard, StateView, StateTransition, Button
+    ...
+    class Opportunity(...):
+        ...
+       @classmethod
+       @Workflow.transition('converted')
+       def convert(cls, opportunities, end_date=None):
+           pool = Pool()
+           Date = pool.get('ir.date')
+           cls.write(opportunities, {
+               'end_date': end_date or Date.today(),
+               })
+    ...
+    class Convert(Wizard):
+        "Convert Opportunities"
+        __name__ = 'training.opportunity.convert'
+
+        start = StateView(
+            'training.opportunity.convert.start',
+            'opportunity.opportunity_convert_start_view_form', [
+                Button("Cancel", 'end', 'tryton-cancel'),
+                Button("Convert", 'convert', 'tryton-ok', default=True),
+                ])
+        convert = StateTransition()
+
+        def transition_convert(self):
+            self.model.convert(self.records, self.start.end_date)
+            return 'end'
+
+.. note::
+   We added an optional ``end_date`` to the convert method.
+
+And we register it in the :class:`~trytond.pool.Pool` as type ``wizard`` in
+:file:`__init__.py`:
+
+.. code-block:: python
+
+    def register():
+        ...
+        Pool.register(
+            opportunity.Convert,
+            module='opportunity', type_='wizard')
+
+Finally we just need to create a ``ir.action.wizard`` and ``ir.action.keyword``
+in :file:`opportunity.xml`:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         ...
+         <record model="ir.action.wizard" id="act_convert_opportunities">
+            <field name="name">Convert Opportunities</field>
+            <field name="wiz_name">training.opportunity.convert</field>
+            <field name="model">training.opportunity</field>
+         </record>
+         <record model="ir.action.keyword" 
id="act_convert_opportunities_keyword">
+            <field name="keyword">form_action</field>
+            <field name="model">training.opportunity,-1</field>
+            <field name="action" ref="act_convert_opportunities"/>
+         </record>
+      </data>
+   </tryton>
+
+The ``ir.action.wizard`` links the :class:`~trytond.wizard.Wizard` with the
+:class:`~trytond.model.Model`.
+
+``name``
+   The string that is shown on the menu.
+``wiz_name``
+   The name of the :class:`~trytond.wizard.Wizard`.
+``model``
+   The name of the :class:`~trytond.model.Model`.
+
+And the ``ir.action.keyword`` makes the :class:`~trytond.wizard.Wizard`
+available as action to any ``training.opportunity``.
+
+``keyword``
+   The type of `keyword <topics-actions>`.
+``model``
+   The model or record for which the action must be displayed.
+   Use ``-1`` as id for any record.
+``action``
+   The link to the action.
+
+Update database
+---------------
+
+As we have defined new fields and XML records, we need to update the database
+with:
+
+.. code-block:: console
+
+   $ trytond-admin -d test --all
+
+And restart the server and reconnect with the client to test the wizard:
+
+.. code-block:: console
+
+   $ trytond
+
+Let's create a :ref:`a report to print opportunities <tutorial-module-report>`.
diff -r aaecbbb8e595 -r 0cd242b9bfc4 doc/tutorial/module/workflow.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial/module/workflow.rst  Tue Apr 12 13:09:01 2022 +0200
@@ -0,0 +1,166 @@
+.. _tutorial-module-workflow:
+
+Define workflow
+===============
+
+Often records follow a workflow to change their state.
+For example the opportunity can be converted or lost.
+Tryton has a :class:`~trytond.model.Workflow` class that provides the tooling
+to follow a workflow based on the field defined in
+:attr:`~trytond.model.Workflow._transition_state` which is by default
+``state``.
+
+First we need to inherit from :class:`~trytond.model.Workflow` and add a
+:class:`~trytond.model.fields.Selection` field to store the state of the
+record:
+
+.. code-block:: python
+
+    from trytond.model import Workflow
+    ...
+    class Opportunity(Workflow, ModelSQL, ModelView):
+       ...
+       state = fields.Selection([
+                 ('draft', "Draft"),
+                 ('converted', "Converted"),
+                 ('lost', "Lost"),
+                 ], "State",
+           required=True, readonly=True, sort=False)
+
+       @classmethod
+       def default_state(cls):
+           return 'draft'
+
+We must define the allowed transitions between states by filling the
+:attr:`~trytond.model.Workflow._transitions` set with tuples using the
+:meth:`~trytond.model.Model.__setup__` method:
+
+.. code-block:: python
+
+    class Opportunity(Workflow, ModelSQL, ModelView):
+       ...
+       @classmethod
+       def __setup__(cls):
+          super().__setup__()
+          cls._transitions.update({
+                    ('draft', 'converted'),
+                    ('draft', 'lost'),
+                    })
+
+For each target state, we must define a
+:meth:`~trytond.model.Workflow.transition` method.
+For example when the opportunity is converted we fill the ``end_date`` field
+with today:
+
+.. code-block:: python
+
+    class Opportunity(Workflow, ModelSQL, ModelView):
+       ...
+       @classmethod
+       @Workflow.transition('converted')
+       def convert(cls, opportunities):
+           pool = Pool()
+           Date = pool.get('ir.date')
+           cls.write(opportunities, {
+               'end_date': Date.today(),
+               })
+
+.. note::
+   We let you define the transition method for lost.
+
+Now we need to add a button for each transition so the user can trigger them.
+
+We must declare the button in the :attr:`~trytond.model.ModelView._buttons`
+dictionary and decorate the transition method with the
+:meth:`~trytond.model.ModelView.button` to be callable from the client:
+
+.. code-block:: python
+
+    class Opportunity(Workflow, ModelSQL, ModelView):
+        ...
+        @classmethod
+        def __setup__(cls):
+            ...
+            cls._buttons.update({
+                    'convert': {},
+                    'lost': {},
+                    })
+
+        @classmethod
+        @ModelView.button
+        @Workflow.transition('converted')
+        def convert(cls, opportunities):
+            ...
+
+        @classmethod
+        @ModelView.button
+        @Workflow.transition('lost')
+        def lost(cls, opportunities):
+            ...
+
+Every button must also be recorded in ``ir.model.button`` to define its label
+(and also the :ref:`access right <topics-access_rights>`).
+We must add to the ``opportunity.xml`` file:
+
+.. code-block:: xml
+
+   <tryton>
+      <data>
+         ...
+         <record model="ir.model.button" id="opportunity_convert_button">
+            <field name="name">convert</field>
+            <field name="string">Convert</field>
+            <field name="model" search="[('model', '=', 
'training.opportunity')]"/>
+         </record>
+
+         <record model="ir.model.button" id="opportunity_lost_button">
+            <field name="name">lost</field>
+            <field name="string">Lost</field>
+            <field name="model" search="[('model', '=', 
'training.opportunity')]"/>
+         </record>
+      </data>
+   </tryton>
+
+Now we can add the ``state`` field and the buttons in the form view.
+The buttons can be grouped under a ``group`` tag.
+This is how the ``view/opportunity_form.xml`` must be adapted:
+
+.. code-block:: xml
+
+   <form>
+      ...
+      <label name="state"/>
+      <field name="state"/>
+      <group col="2" colspan="2" id="button">
+         <button name="lost" icon="tryton-cancel"/>
+         <button name="convert" icon="tryton-forward"/>
+      </group>
+   </form>
+
+.. note::
+   We let you add the ``state`` field on the list view.
+
+Update database
+---------------
+
+As we have defined new fields and XML records, we need to update the database
+with:
+
+.. code-block:: shell
+
+   $ trytond-admin -d test --all
+
+And restart the server and reconnect with the client to test the workflow:
+
+.. code-block:: shell
+
+   $ trytond
+
+Exercise
+---------
+
+As exercise we let you add a transition between ``lost`` and ``draft`` which
+will clear the ``end_date``.
+
+Let's continue with :ref:`adding more reaction with dynamic state
+<tutorial-module-states>`.

Reply via email to