There are 3 classes, sync.test.subject.a
which has many2many relation with sync.test.subject.b
which is inherited by sync.test.subject.c
.
sync.test.subject.b
's separated_chars
field is populated through a compute function called _compute_separated_chars
which is triggered by the change of sync.test.subject.b
's chars
field.
The role of sync.test.subject.c
is basically to set chars
by its own name
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 store sync.test.subject.a
ids because any changes that are not related to separated_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.
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:
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.