Prepare migration to monorepo
This commit is contained in:
@@ -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
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
@@ -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/
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
@@ -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
|
||||
@@ -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 <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/
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
@@ -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>`.
|
||||
Reference in New Issue
Block a user