How do I update other fields or another models from inside compute function?

William Wino picture William Wino · Sep 14, 2015 · Viewed 10.6k times · Source

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.

Answer

Andrei Boyanov picture Andrei Boyanov · Sep 17, 2015

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.