Prepare migration to monorepo

This commit is contained in:
Cédric Krier
2022-12-16 14:43:04 +01:00
parent 2755687c07
commit cc8faa6699
551 changed files with 58 additions and 0 deletions
+23
View File
@@ -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>`.
+95
View File
@@ -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>`.
+111
View File
@@ -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>`.
+40
View File
@@ -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/
+123
View File
@@ -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>`.
+66
View File
@@ -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>`.
+122
View File
@@ -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
+67
View File
@@ -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>`.
+92
View File
@@ -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>`.
+146
View File
@@ -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>`.
+212
View File
@@ -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>`.
+171
View File
@@ -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>`.
+166
View File
@@ -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>`.