diff --git a/CHANGELOG b/CHANGELOG index c949fca95a..d5ade835f5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 --git a/doc/index.rst b/doc/index.rst index ff426595f9..d66b7c151c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -13,6 +13,9 @@ First steps :ref:`Setup a database ` | :ref:`Start the server ` + * **Tutorials:** + :ref:`Create a module ` + The model layer =============== @@ -43,7 +46,8 @@ The development process ======================= * **Modules** - :ref:`Module definition ` + :ref:`Module definition ` | + :ref:`Create a module ` Contents ======== @@ -53,6 +57,7 @@ Contents topics/index ref/index + tutorial/index Indices, glossary and tables ============================ diff --git a/doc/ref/models.rst b/doc/ref/models.rst index 8052efd333..99b90cf720 100644 --- a/doc/ref/models.rst +++ b/doc/ref/models.rst @@ -689,6 +689,7 @@ Class attribute: .. 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 --git a/doc/topics/modules/index.rst b/doc/topics/modules/index.rst index e15300adbc..3dc1cb9b72 100644 --- a/doc/topics/modules/index.rst +++ b/doc/topics/modules/index.rst @@ -20,6 +20,8 @@ files: :file:`tryton.cfg` A Configuration file that describes the Tryton module. +.. _topics-modules-init: + :file:`__init__.py` file ------------------------ @@ -27,6 +29,7 @@ It is the Python :file:`__init__.py` to define a module. 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 @@ Python Files The Python files define the models for the modules. +.. _topics-modules-xml-files: + XML Files ========= @@ -219,3 +224,4 @@ Here is the list of the tags: .. _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 --git a/doc/topics/reports/index.rst b/doc/topics/reports/index.rst index fc91f8f579..bb84d011da 100644 --- a/doc/topics/reports/index.rst +++ b/doc/topics/reports/index.rst @@ -118,7 +118,8 @@ Also note that relatorio only supports a subset of Genshi. 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 @@ Now the invoice report will be able to access the employee record. 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 --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst new file mode 100644 index 0000000000..62d6088aad --- /dev/null +++ b/doc/tutorial/index.rst @@ -0,0 +1,10 @@ +.. _tutorial-index: + +========= +Tutorials +========= + +.. toctree:: + :maxdepth: 1 + + module/index diff --git a/doc/tutorial/module/anatomy.rst b/doc/tutorial/module/anatomy.rst new file mode 100644 index 0000000000..4a971d7dac --- /dev/null +++ b/doc/tutorial/module/anatomy.rst @@ -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 ` file. + +A Tryton module must also contain a :ref:`tryton.cfg +` file which is used to define the dependencies +between modules and also lists the :ref:`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 ` + +.. _`python module`: https://docs.python.org/tutorial/modules.html +.. _`po files`: https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html diff --git a/doc/tutorial/module/default_values.rst b/doc/tutorial/module/default_values.rst new file mode 100644 index 0000000000..ce9b76859c --- /dev/null +++ b/doc/tutorial/module/default_values.rst @@ -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 ` 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_`` 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 `. diff --git a/doc/tutorial/module/domains.rst b/doc/tutorial/module/domains.rst new file mode 100644 index 0000000000..fc5b0670f9 --- /dev/null +++ b/doc/tutorial/module/domains.rst @@ -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 +`. + +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 ` statements. + +Great, you have learned to add constraint on the fields value. +Let's continue with :ref:`adding a workflow `. diff --git a/doc/tutorial/module/extend.rst b/doc/tutorial/module/extend.rst new file mode 100644 index 0000000000..c293b8ce51 --- /dev/null +++ b/doc/tutorial/module/extend.rst @@ -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 + + + + + party.party + + party_form + + + + +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 + + + + + + + + + +.. _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 `. diff --git a/doc/tutorial/module/function_fields.rst b/doc/tutorial/module/function_fields.rst new file mode 100644 index 0000000000..ab2637a916 --- /dev/null +++ b/doc/tutorial/module/function_fields.rst @@ -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 ` +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_`` + * 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 +`. diff --git a/doc/tutorial/module/index.rst b/doc/tutorial/module/index.rst new file mode 100644 index 0000000000..a5c6fb89dd --- /dev/null +++ b/doc/tutorial/module/index.rst @@ -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 `. + +.. _SQLite: https://sqlite.org/ diff --git a/doc/tutorial/module/model.rst b/doc/tutorial/module/model.rst new file mode 100644 index 0000000000..cf61ac4126 --- /dev/null +++ b/doc/tutorial/module/model.rst @@ -0,0 +1,123 @@ +.. _tutorial-module-model: + +Define model +============ + +The :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 +`. + +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 `. + +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 `. diff --git a/doc/tutorial/module/on_change.rst b/doc/tutorial/module/on_change.rst new file mode 100644 index 0000000000..a560864351 --- /dev/null +++ b/doc/tutorial/module/on_change.rst @@ -0,0 +1,66 @@ +.. _tutorial-module-on-change: + +React to user input +=================== + +Tryton provides a way to :ref:`change the value of a field +` 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_`` function instead of +``on_change_with_``. +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 +`. diff --git a/doc/tutorial/module/report.rst b/doc/tutorial/module/report.rst new file mode 100644 index 0000000000..ef4e94c146 --- /dev/null +++ b/doc/tutorial/module/report.rst @@ -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 + + + + ... + + Opportunity + training.opportunity.report + training.opportunity + opportunity/opportunity.fodt + odt + + + form_print + training.opportunity,-1 + + + + + +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 `, 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:: + + + Opportunity: + Party: + Start Date: + End Date: + + Comment: + + + + + +.. 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 +`. + +.. _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 --git a/doc/tutorial/module/setup.rst b/doc/tutorial/module/setup.rst new file mode 100644 index 0000000000..b35ac6e271 --- /dev/null +++ b/doc/tutorial/module/setup.rst @@ -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 [bugs@tryton.org]: john@example.com + 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 ` + +.. _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 --git a/doc/tutorial/module/setup_database.rst b/doc/tutorial/module/setup_database.rst new file mode 100644 index 0000000000..e593826af2 --- /dev/null +++ b/doc/tutorial/module/setup_database.rst @@ -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 +`. + +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 `. diff --git a/doc/tutorial/module/states.rst b/doc/tutorial/module/states.rst new file mode 100644 index 0000000000..f84761c62a --- /dev/null +++ b/doc/tutorial/module/states.rst @@ -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 `. diff --git a/doc/tutorial/module/table_query.rst b/doc/tutorial/module/table_query.rst new file mode 100644 index 0000000000..213bc3c18c --- /dev/null +++ b/doc/tutorial/module/table_query.rst @@ -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 + + + + ... + + training.opportunity.monthly + tree + opportunity_monthly_list + + + + Monthly Opportunities + training.opportunity.monthly + + + + + + + + + + + +And now the view in :file:`view/opportunity_monthly_list.xml`: + +.. code-block:: xml + + + + + + + +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 `. diff --git a/doc/tutorial/module/view.rst b/doc/tutorial/module/view.rst new file mode 100644 index 0000000000..4ede37ea05 --- /dev/null +++ b/doc/tutorial/module/view.rst @@ -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 ` and +:ref:`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 +` 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 + +
+