-
Notifications
You must be signed in to change notification settings - Fork 30
5.0 step9
We just saw how wizards can make complex operations easier to perform by the user. We are now going to see a few other things about interfaces, as well as a guided tours of the main methods you can override on models.
You may have asked yourself about the reason for which we did not defined a
One2Many
field on library.editor
(or library.genre
) to view all
books that were linked to one of those.
There two main reasons for that:
- There is no "logic" in saying that an editor owns a list of books. There is a relation (no one will deny that), but it is not ownership
- That would be very long lists of books. For genres, we are talking potentially thousands of books. An "inlined" view in the editor / genre record would not be efficient, and easy to use (no filtering, etc.)
Also, there was no need to add this field to display the information, for there
are Relates. Open library.xml
and add the following (at the end):
<!-- ########### -->
<!-- # Relates # -->
<!-- ########### -->
<record model="ir.action.act_window" id="act_genre_book_relation">
<field name="name">Books</field>
<field name="res_model">library.book</field>
<field name="domain" eval="[('genre', '=', Eval('active_id'))]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_genre_book_relation_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="book_view_list"/>
<field name="act_window" ref="act_genre_book_relation"/>
</record>
<record model="ir.action.act_window.view" id="act_genre_book_relation_view2">
<field name="sequence" eval="1"/>
<field name="view" ref="book_view_form"/>
<field name="act_window" ref="act_genre_book_relation"/>
</record>
<record model="ir.action.keyword" id="act_open_genre_books_keyword">
<field name="keyword">form_relate</field>
<field name="model">library.genre,-1</field>
<field name="action" ref="act_genre_book_relation"/>
</record>
You should now know enough of your way around a tryton xml file to understand
most of what you are seeing. We create a new action of type act_window
,
which is linked to the default list
and form
views for
library.book
. We then add a keyword which triggers this action from the
library.genre
model.
The differences between what you already saw are:
- The value
form_relate
for thekeyword
field of their.action.keyword
instance. This is used to tell tryton that the action should appear under theRelate
menu in the toolbar. Relates should fall under this menu, and are usually defined as anything that opens a new tab to show related informations to the selected records - The
domain
field on their.action.act_window
model. Now you should be able to guess what it means: We want to force a constraint (domain) on the records that are displayed in the tab. The value is not passed as a string (like for instance in theres_model
field) but in theeval
parameter, with thepyson
attribute set to"1"
. Doing so will make tryton parse the value as a Pyson expression (which it is because of the use ofEval
), and stored it in a serialized way in the database (we cannot store theEval
class directly in the database). You should remember whatactive_id
is from the previous step: it contains theid
of the currently selected record in the client
So what we are doing here is adding a new entry point under the Relate
menu
for the library.genre
model. When activated, it will open a new tab, in
which will be displayed all the books which are related to this genre.
Relates are very interesting because:
- They allow you to display data related to a record even if the number of
elements in the list is huge. Also, it comes with all the filtering tools
of standard
ir.action.act_window
tabs - They are directly available in the "right-click" menu of a record
- They provide for a way to lighten your form views by showing big lists in separate tabs
Another thing you may want to do is to have separate tabs in a given tab. We
will dot this for books, in library.xml
, after the act_book_view_form
record:
<record model="ir.action.act_window.domain" id="act_book_domain_recent">
<field name="name">Recent</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('publishing_date', '>', Date(delta_years=-1))]" pyson="1"/>
<field name="act_window" ref="act_book"/>
</record>
<record model="ir.action.act_window.domain" id="act_book_domain_all">
<field name="name">All</field>
<field name="sequence" eval="20"/>
<field name="act_window" ref="act_book"/>
</record>
You can probably guess where this is going. The
ir.action.act_window.domain
records define domains on the
ir.action.act_window
model. Those will be converted to "sub-tabs" in the
"main tab" of the action. The contents of the tabs will be defined by the
domain
contents. Here, the first tab will be limited to books which were
published in the past year (Date(delta_years=-1)
means the current date
minus a year), while the second will have no limitation at all.
Open the view/book_form.xml
file and add the following after the closing
notebook
tag:
<button string="Create exemplaries" name="create_exemplaries" icon="tryton-new" colspan="4"/>
You can check it out in the form view. The string
will be displayed
alongside the icon
, and you can click on it (its a button). However you
will be greated by a cryptic error message about create_exemplaries
being a
method that you are not allowed to call on library.book
.
Buttons must be bound to methods defined in the model on which the button
is displayed. Also, not any method will do. Add the following in library.py
under the library.book
model:
@classmethod
@ModelView.button_action('library.act_create_exemplaries')
def create_exemplaries(cls, books):
pass
The prototype for "button" methods is a classmethod which accepts a list of
instances (cleverly named books
in our case).
@ModelView.button_action('library.act_create_exemplaries')
This line is used to explain to tryton what should happen when the button is
clicked. The ModelView.button_action
decorator is used to bind the method
to the action given as a parameter. What we are basically saying here is that
the create_exemplaries
button, when clicked, should open the
act_create_exemplaries
action, which is bound to the wizard we created in
the previous state. In this case, the body of the method will be left empty.
There are other button types:
- Pure
ModelView.button
will just run the code inside the method. The return value can be any of those defined here -
ModelView.button_change
is a little different. The method definition should in this case be an instance method. The available fields in the method will be those in the parameters of thebutton_change
decorator:
@ModelView.button_change('field1', 'field2')
def my_button(self):
if self.field1 == self.field2:
self.field1 += 1
You can see ``button_change`` buttons more or less as ``on_change`` methods
which are triggered by a click rather than a field modification.
Important: For button
and button_action
buttons, clicking the
button will force a save of the record. So if you modify a field and then
click on the button, the field modification will be saved before the button is
called, meaning that you cannot click the button if the record is not in a
valid state (required field filled up, and valid domains). For
button_change
there is no problem, however the method will be executed in a
readonly transaction (like on_change
methods are)
Okay, so now if you click your button, you will still have an error message.
That is because by default tryton forbid all but a few methods to be called by
a client, so we have to add our method to those exceptions. Add the following
in the __setup__
method of library.book
:
cls._buttons.update({
'create_exemplaries': {},
})
Tryton will look in a few places for allowed methods, and one of those is the
_buttons
attribute of the model. What we are doing here is declaring the
method create_exemplaries
as a button by adding it in _buttons
. The
value {}
can be used to add dynamicity on the button (though we do not need
it now), the same way that states
do for fields. This dictionary can have
either (or both) readonly
or invisible
keys, and pyson as value. So you
could (for instance) make a button readonly if the genre is Fake News
.
You can now click your button (again), and this time it will work as expected, opening our wizard.
We already talked a lot about __setup__
, and we will even more in the next
step. What must be remembered is that usually you will use it to set "model"
attributes that have a special meaning in tryton. We already saw
_error_messages
, _sql_constraints
and _buttons
, but there are
others:
-
_order
can be used to define the default sort order for the model. Ex:cls._order = [('field_1', 'ASC'), ('field_2', 'DESC')]
-
__rpc__
is used if you want to expose new methods to the client. This is usually only used if you write APIs for others to call, since the methods used by the tryton client are already in there. Ex:
from trytond.rpc import RPC
# ...
cls.__rpc__.update({
'my_method_1': RPC(instantiate=0), # instance method
'my_method_2': RPC(readonly=1), # readonly transaction
'my_method_3': RPC(check_access=0), # no automatice access right checks
'my_method_4': RPC(result=lambda x: -x), # the result will be inverted
})
There are a few other values that you will learn about by yourself if you ever need them.
Warning: The __setup__
method is called very early in the server
startup, so some things (like Pool
) are not available, and you should not
try to access the database unless you know what you are doing
The __register__
method is often used, but not when creating a module. The
main use case for overriding it is to manage automatic version upgrade of your
modules.
Internally, the __register__
method is responsible for creating / updating
the database table bound to a record (adding new columns, etc.), installing
translations, etc. It is called when the module is installed or upgraded.
A typical basic use case is to delete a column that is not needed anymore (tryton will automatically create new columns, but never delete old ones):
from trytond import backend
def __register__(cls, module_name):
super(MyClass, cls).__register__(module_name)
TableHandler = backend.get('TableHandler')
handler = TableHandler(cls)
if handler.column_exist('my_old_column'):
handler.drop_column('my_old_column')
We are here in internals of tryton, but you will probably use those if you develop with tryton for a long time.
Another use case is initializing the value of a new column:
from trytond import backend
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
handler = TableHandler(cls)
must_migrate = handler.column_exist('my_new_column')
super(MyClass, cls).__register__(module_name)
if must_migrate:
pool = Pool()
cursor = Transaction().connection.cursor()
my_table = cls.__table__()
cursor.execute(*my_table.update(
columns=[my_table.my_new_column],
values=[my_table.my_old_field + my_table.my_other_old_field]))
Note that the must_migrate
variable must be computed before calling
super
, because after the column will already has been added to the table so
you will not be able to detect it.
CRUD stands for Create, Read, Update, Delete. Those are the basic methods in tryton (though "update" turns to "write"), and even though you will not often modify them, it is good thing to know about them.
The create
method is called every time a new record is saved in the
database. Its prototype is:
@classmethod
def create(cls, vlist):
The data_dict
parameter contains a list of dictionaries that will each
create a new instance. For a library.book
, its contents will be something
like:
{
'edition_stopped': False,
'isbn': '',
'title': 'My Book',
'page_count': 256,
'exemplaries': [
['create', [
{
'identifier': '1234567890',
'acquisition_date': datetime.date(2017, 9, 22)}
]]],
}
Note the particular syntax for One2Many
fields contents.
Overriding the create
method can come in handy for setting calculated
values in any case (even when the creation is done server side, the client part
being handled by on_change
methods):
@classmethod
def create(cls, vlist):
for elem in vlist:
if 'field_1' in elem:
elem['field_calculated']) = elem['field_1'] * 2
return super(MyClass, cls).create(vlist)
Warning: Do not forget to return the super
value. The create
method returns a list of instances corresponding to each dictionary in the
vlist
parameter, and is expected to do so, so forgetting the return
will break things
The read
method will almost never be overriden, but it will often be used,
even indirectly:
@classmethod
def read(cls, ids, fields_names=None):
read
is the "low level" API to get the fields values for a record. It is
used directly by the client (once it know the fields in the displayed view, it
will call read
with the ids being displayed, and the list of fields), and
behind the scene almost every time you write instance.field_name
in your
code. It will query the database for real fields, and call the getter of
Function
fields.
You really should not have to modify it unless you are trying to work some magic with tryton.
When modifying an instance, and saving the modifications, the write
method
is called. You can also call it directly (as we did in our book merging
wizard). Same as the create
method, overriding it is not something that
will happen often, but it is better to know how it works:
@classmethod
def write(cls, records1, data1, records2, data2, ...):
As explained in the step 8 correction, there can be any
number of different modifications in a write
call. The exact prototype is
actually:
@classmethod
def write(cls, *args):
A use case would be for instance to update some sort of calculated field that
you want to store in database rather than use a Function
field for:
@classmethod
def write(cls, *args):
data = iter(args)
# Magic to "pair" arguments
for records, data in zip(data, data):
if 'field_1' in data:
data['field_calculated'] = data['field_1'] * 2
super(MyClass, cls).write(*args)
write
has no expected return value.
delete
will delete instances (...):
@classmethod
def delete(cls, instances):
Overriding it can be done for instance to perform additional checks which may possibly forbid the deletion:
@classmethod
def delete(cls, instances):
if any(x.cannot_delete() for x in instances):
cls.raise_user_error('cannot_delete')
super(MyClass, cls).delete(instances)
This method is maybe the one that you will most often override among the CRUDs, simply because this particular use case is more frequent than others.
Tryton has a built-in cache mechanism to avoid querying the database too often,
and you can use it to add your own caches if needed. We will not detail how
this is done (refer to the
documentation) for
this. Just know that when you manage cache, you will need to reset it if
needed, and create
, write
and delete
methods are often overriden
for this purpose.
save
is a shortcut for either creation or writing, depending on the state
of the record(s) it is called on. If it was already saved, modifications that
were made will be written, else it will be created.
It can be used either as an instance method (my_book.save()
) or as a class
method for optimizations (Book.save([book1, book2, book3, ...]
).
Internally, it will call create
or write
(or both if the list of
instances to save is mixed) depending on the status of the records.
Do not override it, because it not always called. The client never does for
instance, and nothing prevents a developer from calling create
or write
directly.
We already talked about this method in step 6. validate
is
called every time a record is modified, whether it is through create
or
write
. It will NOT be called if you directly update the database
through SQL (though you usually should not need to do that).
It is called after the modifications are made, and after the "technical" validation (domain integrity, selection field values, etc...) are checked.
You should not modify / save records in the validate
method. If you
need to change the behavior on saving a record, overwrite create
or
write
.
The view_attributes
method is used to add dynamicity to specific views
in the application. For instance, you may want to hide a whole group in a view
depending on the value of a field:
@classmethod
def view_attributes(cls):
return super(MyClass, cls).view_attributes() + [
("/form/group[@id='my_group_id']", 'states', {
'invisible': Bool(Eval('hide_my_group', False))})]
The view_attributes
method must return a list of 3-tuples built as follow:
(<xml/path/to/target/element, 'attribute_to_modify', value)
. The path to
the target element is defined using the
xpath syntax. The attribute you will
want to modify can be anything, but the typical use case is to set the
states
of a group to make it invisible depending on some pyson expression
(as done above).
Homework will be easy for this step:
- Add a relate to show the books of an author
- Add a relate to show the books of an editor
- Add a relate to show the exemplaries of a book, and remove the exemplaries tab in the book's form view
There is no need for a correction here, just compare your code with that of the
step9_homework
branch for differences.
You should now have all you need to create fully-fledged modules. We are now going to override those modules