How do I update other fields or another models from inside compute function?
Because the question is interesting and deal with the behavior of the new Odoo API I took the time to play a little bit with the compute
methods. What you say in your question is not totally wrong although there are several premature statements.
To demonstrate the Odoo's behavior I created the simple Books application with the following design.
There are two models - 'books.book' and 'books.author'. Each of them has a Many2many
relation with the other - that's mode than normal as every book may be written by one or more authors and every author is supposed to have written one or more books.
Here is the place to say that is a little bit weired to deal with Many2many
related objects from such a compute
method as you want. That's because the Many2many
records exist and have their one life independently each of the other. With One2many
relation it's much different.
But any way, to reproduce the behavior you show us in your example I made the author.books
field computed - it's value is computed by the _get_books()
method oh the author
class.
Just to show that different computed fields work well and independently, I created another computed field - name
, which is computed be the method _get_full_name()
of the author
class.
Now some words about the _get_books()
method. Based on the books_list
Text field, this method generates one book per line of the books_list
.
When creating the book the method first verify if a book with this name already exists. If this is the case, this book is linked to the author. Else, a new book is created and linked to the author.
And now the question that mostly interests you - before the creation of the new books the existing books related to this author are deleted. To do that the method uses a low level SQL queries. This way we deal with the problem that we don't have the list of related objects inside the compute
method.
What you must have in mind when dealing with computed fields depending from another field is the following:
- They are computed when the field they depend on is changed (thats the good news)
- The need to recompute them is evaluated every time when you try to access their value. So some care is needed to avoid endless recursion.
About changing the values of another fields inside the compute method. Read the following part of the documentation:
Note
onchange methods work on virtual records assignment on these records is not written to the database, just used to know which value to send back to the client
Thats valid for the compute
methods too. What that means? It means that if you assign a value to another field of the model, this value won't be written in the database. But the value will be returned to the user interface and written to the database while saving the form.
Before pasting my sample code, I suggest you again to change the design of your application and not to deal in this way with the many2many relations from inside the compute method. Creation of new objects works well but deletion and modification of existing ones is tricky and not pleasant at all.
Here is the books.py
file:
from openerp import models, fields, api
class book(models.Model):
_name = 'books.book'
_description = 'Some book'
name = fields.Char('Name')
authors = fields.Many2many('books.author', string='Author',
relation='books_to_authors_relation',
column1='book_id', column2='author_id')
book()
class author(models.Model):
_name = 'books.author'
_description = 'Author'
first_name = fields.Char('First Name')
second_name = fields.Char('Second Name')
name = fields.Char('Name', compute='_get_full_name', store=True)
books_list = fields.Text('List of books')
notes = fields.Text('Notes')
books = fields.Many2many('books.book', string='Books',
relation='books_to_authors_relation',
column1='author_id', column2='book_id',
compute='_get_books', store=True)
@api.one
@api.depends('first_name', 'second_name')
def _get_full_name(self):
import pdb; pdb.set_trace()
if not self.first_name or not self.second_name:
return
self.name = self.first_name + ' ' + self.second_name
@api.depends('books_list')
def _get_books(self):
if not self.books_list:
return
books = self.books_list.split('\n')
# Update another field of this object
# Please note that in this step we update just the
# fiedl in the web form. The real field of the object
# will be updated when saving the form
self.notes = self.books_list
# Empty the many2many relation
self.books = None
# And delete the related records
if isinstance(self.id, int):
sql = """
DELETE FROM books_to_authors_relation
WHERE author_id = %s
"""
self.env.cr.execute(sql, (self.id, ))
sql = """
DELETE FROM books_book
WHERE
name not in %s
AND id NOT in (
SELECT id from books_book as book
INNER JOIN books_to_authors_relation
as relation
ON book.id = relation.book_id
WHERE relation.author_id != %s)
"""
self.env.cr.execute(sql, (tuple(books), self.id, ))
### As per the documentation, we have to invalidate the caches after
### low level sql changes to the database
##self.env.invalidate_all()
# Create book records dinamically according to
# the Text field content
book_repository = self.env['books.book']
for book_name in books:
book = book_repository.search([('name', '=', book_name)])
if book:
self.books += book
else:
self.books += book_repository.create({'name': book_name, })
return
author()
And the user interface:
<openerp>
<data>
<menuitem id="books" name="Books App" sequence="0" />
<menuitem id="books.library" name="Library"
parent="books" sequence="0" />
<record model="ir.ui.view" id="books.book_form">
<field name="name">books.book.form</field>
<field name="model">books.book</field>
<field name="type">form</field>
<field name="arch" type="xml">
<group col="2">
<field name="name" />
</group>
<field name="authors" string="Authors" />
</field>
</record>
<record model="ir.ui.view" id="books.book_tree">
<field name="name">books.book.tree</field>
<field name="model">books.book</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<field name="name" />
<field name="authors" string="Authors" />
</field>
</record>
<record id="books.book_action" model="ir.actions.act_window">
<field name="name">Books</field>
<field name="res_model">books.book</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="books.books_menu" name="Books"
parent="books.library" sequence="10"
action="books.book_action"/>
<record model="ir.ui.view" id="books.author_tree">
<field name="name">books.author.tree</field>
<field name="model">books.author</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<field name="name" />
<field name="books_list" />
<field name="notes" />
<field name="books" string="Books" />
</field>
</record>
<record model="ir.ui.view" id="books.author_form">
<field name="name">books.author.form</field>
<field name="model">books.author</field>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="name" />
<group col="4">
<field name="first_name" />
<field name="second_name" />
</group>
<group col="6">
<field name="books_list" />
<field name="notes" string="Notes"/>
<field name="books" string="Books" />
</group>
</field>
</record>
<record id="books.author_action" model="ir.actions.act_window">
<field name="name">Authors</field>
<field name="res_model">books.author</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="books.authors" name="Authors"
parent="books.library" sequence="5"
action="books.author_action"/>
</data>
EDIT
If you want to subclass the author class for example, than remove the relation
, column1
and column2
attributes from the Many2many field definition . his will leave the default relation table names.
Now you can define in each subclass a method like this:
def _get_relation_table(self):
return 'books_author_books_book_rel'
and use this method in the SQL query construction when you want to delete records from this relation table.
William Wino
Years of experience in technology led me to an entrepreneurial life that is colored with the passion to learn the most advanced technology that is in the market. I have a knack for developing a framework which can cut down the cost of developing a product. I'm optimistic that, with the right mindset, the future of technology will bring prosperity to all of us. Currently I'm building up a software development company which integrates ReactJS, NextJS and Golang to create a rich application development ecosystem that speeds up the development cycles. We already have our own Enterprise Resource Planning application which integrates Sales, Purchasing, Accounting, Finance and Inventory Management into a single system. And we're planning to expand our business to reach out potential partners that can work with us to develop niche products that will bring ease to our everyday life.
Updated on June 17, 2022Comments
-
William Wino almost 2 years
There are 3 classes,
sync.test.subject.a
which has many2many relation withsync.test.subject.b
which is inherited bysync.test.subject.c
.sync.test.subject.b
'sseparated_chars
field is populated through a compute function called_compute_separated_chars
which is triggered by the change ofsync.test.subject.b
'schars
field.The role of
sync.test.subject.c
is basically to setchars
by its ownname
so that_compute_separated_chars
is triggered.The problem is I can't delete leftover records that are related to a Many2many field (namely
sync.test.subject.a
leftover records) from inside the compute function because BEFORE the function is executed the field is already emptied by the system so I can't get the ids. I can't even use temporary field to storesync.test.subject.a
ids because any changes that are not related toseparated_chars
won't be committed by the system from inside the compute function (By any changes, I mean really ANY changes either to other fields from the same model or other changes to other models won't be committed). How do I solve this?Models:
from openerp import models, fields, api, _ class sync_test_subject_a(models.Model): _name = "sync.test.subject.a" name = fields.Char('Name') sync_test_subject_a() class sync_test_subject_b(models.Model): _name = "sync.test.subject.b" chars = fields.Char('Characters') separated_chars = fields.Many2many('sync.test.subject.a',string='Separated Name', store=True, compute='_compute_separated_chars') @api.one @api.depends('chars') def _compute_separated_chars(self): a_model = self.env['sync.test.subject.a'] if not self.chars: return self.separated_chars.unlink() #DELETE LEFTOVER RECORDS FROM a_model for character in self.chars: self.separated_chars += a_model.create({'name': character}) sync_test_subject_b() class sync_test_subject_c(models.Model): _name = "sync.test.subject.c" _inherit = "sync.test.subject.b" name = fields.Char('Name') @api.one def action_set_char(self): self.chars = self.name sync_test_subject_c()
Views:
<?xml version="1.0" encoding="UTF-8"?> <openerp> <data> <!-- Top menu item --> <menuitem name="Testing Module" id="testing_module_menu" sequence="1"/> <menuitem id="sync_test_menu" name="Synchronization Test" parent="testing_module_menu" sequence="1"/> <!--Expense Preset View--> <record model="ir.ui.view" id="sync_test_subject_c_form_view"> <field name="name">sync.test.subject.c.form.view</field> <field name="model">sync.test.subject.c</field> <field name="type">form</field> <field name="arch" type="xml"> <form string="Sync Test" version="7.0"> <header> <div class="header_bar"> <button name="action_set_char" string="Set Name To Chars" type="object" class="oe_highlight"/> </div> </header> <sheet> <group> <field string="Name" name="name" class="oe_inline"/> <field string="Chars" name="chars" class="oe_inline"/> <field string="Separated Chars" name="separated_chars" class="oe_inline"/> </group> </sheet> </form> </field> </record> <record model="ir.ui.view" id="sync_test_subject_c_tree_view"> <field name="name">sync.test.subject.c.tree.view</field> <field name="model">sync.test.subject.c</field> <field name="type">tree</field> <field name="arch" type="xml"> <tree string="Class"> <field string="Name" name="name"/> </tree> </field> </record> <record model="ir.ui.view" id="sync_test_subject_c_search"> <field name="name">sync.test.subject.c.search</field> <field name="model">sync.test.subject.c</field> <field name="type">search</field> <field name="arch" type="xml"> <search string="Sync Test Search"> <field string="Name" name="name"/> </search> </field> </record> <record id="sync_test_subject_c_action" model="ir.actions.act_window"> <field name="name">Sync Test</field> <field name="res_model">sync.test.subject.c</field> <field name="view_type">form</field> <field name="domain">[]</field> <field name="context">{}</field> <field name="view_id" eval="sync_test_subject_c_tree_view"/> <field name="search_view_id" ref="sync_test_subject_c_search"/> <field name="target">current</field> <field name="help">Synchronization Test</field> </record> <menuitem action="sync_test_subject_c_action" icon="STOCK_JUSTIFY_FILL" sequence="1" id="sync_test_subject_c_action_menu" parent="testing_module.sync_test_menu" /> </data> </openerp>
I think this behavior is caused by a lazy implementation by Odoo to handle chain computed field triggers instead of handling the triggers correctly (sequentially based on the dependencies) they just update EVERY computed fields EVERYTIME there are changes to EVERY OTHER FIELD. And because of that they restrict any update to any other field from inside the compute function. Because if they don't it will blow up with recursive compute function calling.
-
William Wino over 8 yearsAndrei, you're the man! Haha. I've never thought of using low level SQL queries. The reason why I use Many2many relation is because of this problem: stackoverflow.com/questions/29962101/…. Odoo inheritance is unlike Java inheritance, I don't know how to code a superclass with one2many field that can be inherited by the subclasses. Thus, I decided to use many2many relation instead which works well, until I got into this problem.
-
William Wino over 8 yearsBut I still need to modify your solution as the relation table changes every time I extend the superclass!
-
Andrei Boyanov over 8 yearsSo you problem is completely elsewhere :) I'll answer the other question. In Odoo inheritance is by delegation - don't use the Python class inheritance, use the Odoo _inherit = '....' instead.
-
William Wino over 8 yearsHey another problem came up..sorry if I keep bothering you with these questions of mine. I am honestly tired of these problems with computed fields myself. Check it out: stackoverflow.com/questions/32722324/…
-
William Wino over 8 yearsAbout the relation table name I reconstruct the table name from this:
model_name = self.__class__.__name__.replace(".", "_")
this code enables dynamic model name plus you don't need to redefine_get_relation_table
every time you create a new subclass. I've included the sample code in my new question.