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

11,421

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.

Share:
11,421
William Wino
Author by

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, 2022

Comments

  • William Wino
    William Wino almost 2 years

    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.

  • William Wino
    William Wino over 8 years
    Andrei, 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
    William Wino over 8 years
    But I still need to modify your solution as the relation table changes every time I extend the superclass!
  • Andrei Boyanov
    Andrei Boyanov over 8 years
    So 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
    William Wino over 8 years
    Hey 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
    William Wino over 8 years
    About 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.