Articles>Odoo — It depends

Odoo — It depends

A Use Case For Odoo Compute Functions and Performance

Written by
Holden Rehg
Posted on
June 4, 2016

When Odoo refactored their API and caused forums to be fill with “new api” “old api” talk, I feel that it really increased the ease of use for developers. Many of the powerful features and tools built into Odoo became even simpler to understand. It also became simpler to write poorly performing code.

It also became simpler to write poorly performing code.

In this article I’m going to break down an example specifically for Odoo’s compute functions that show how quickly a dependency chain can get out of hand and affect the entire system. Let’s take a look at defining a simple class.


# -*- coding: utf-8 -*-
import logging
from openerp import api, models, fields

_logger = logging.getLogger(__name__)


class PartnerGroup(models.Model):
    _name = 'res.partner.group'
    name = fields.Char('Group Name')
    partner_ids = fields.One2many('res.partner', 'partner_group_id', 'Partners')
        

This is a class for defining groups of partners. It has a name for the group and a reference to all the partners in the group. Simple enough. No performance issues yet.

Lets add a couple more classes.


# -*- coding: utf-8 -*-
import logging
from openerp import api, fields, models

_logger = logging.getLogger(__name__)


class Partner(models.Model):
    _inherit = 'res.partner'
    partner_group_id = fields.Many2one('res.partner.group')
    has_late_bills = fields.Boolean('Has Late Bills', compute='compute_has_late_bills')

    @api.multi
    @api.depends('invoice_ids')
    def compute_has_late_bills(self):
        """
        Computes if this partner has late bills based on
        if any of the invoices attached to this group are late.
        """
        for partner in self:
            _logger.info('DEPENDS_TEST: computing late bills for partner ' + str(partner.id))
            for invoice in partner.invoice_ids:
                if invoice.is_late:
                    partner.has_late_bills = True
                    return
            partner.has_late_bills = False
        

# -*- coding: utf-8 -*-
import logging
from datetime import datetime
from openerp import api, fields, models, tools

_logger = logging.getLogger(__name__)
LATE_LIMIT = 25


class AccountInvoice(models.Model):
    _inherit = 'account.invoice'
    is_late = fields.Boolean('Is Late', compute='compute_is_late')

    @api.multi
    @api.depends('date_invoice')
    def compute_is_late(self):
        """
        Computes if this bill is late. The bill is late if it has
        been the LATE_LIMIT days past the invoice date.
        """
        for invoice in self:
            _logger.info('DEPENDS_TEST: computing is late on invoice id ' + str(invoice.id))
            if invoice.date_invoice:
                date_invoice = datetime.strptime(invoice.date_invoice, tools.DEFAULT_SERVER_DATE_FORMAT)
                now = datetime.strptime(datetime.strftime(datetime.today(), tools.DEFAULT_SERVER_DATE_FORMAT), tools.DEFAULT_SERVER_DATE_FORMAT)
                invoice.is_late = (now - date_invoice).days > LATE_LIMIT
        

Now we’ve gotten a small system for defining late invoices and for checking if any given partner has a late invoice. There are two compute functions. One on each class and each has a single dependency. We can start mapping out what our dependency chain looks like.


res.partner:has_late_bills => res.partner:invoice_ids
account.invoice:is_late    => account.invoice:date_invoice
        

Every time an invoice is updated on a partner it computes the partner’s has_late_bills field and every time an invoice’s date_invoice field is updated it recomputes the invoice’s is_late field.

Again, this is pretty simple. Still no performance issues yet. Let’s update our partner group class to calculate if an entire group has any late bills.


# -*- coding: utf-8 -*-
import logging
from openerp import api, models, fields

_logger = logging.getLogger(__name__)


class PartnerGroup(models.Model):
    _name = 'res.partner.group'
    name = fields.Char('Group Name')
    partner_ids = fields.One2many('res.partner', 'partner_group_id', 'Partners')
    has_late_bills = fields.Boolean('Has Late Bills', compute='compute_has_late_bills')

    @api.multi
    @api.depends('partner_ids.has_late_bills', 'partner_ids.invoice_ids')
    def compute_has_late_bills(self):
        """
        Computes if this partner group has late bills based on
        if any of the partners in the group have late bills.
        """
        for group in self:
            _logger.info('DEPENDS_TEST: computing late bills for partner group ' + str(group.id))
            for partner in group.partner_ids:
                if partner.has_late_bills:
                    group.has_late_bills = True
                    return
            group.has_late_bills = False
        
Here’s where it gets interested

This includes a single mistake that can cause degraded performance and even unnecessary database queries (when using stored=True on compute functions) depending on how many concurrent users are accessing the system, the amount of invoices, the number of partners per partner group, etc.

The groups has_late_bills depends on both the partners has_late_bills field and the partners invoices ids which is not necessary. Our dependency chain get’s slightly more complicated.


res.partner:has_late_bills       => invoice_ids
account.invoice:is_late          => date_invoice
res.partner.group:has_late_bills => partner_ids.has_late_bills
res.partner.group:has_late_bills => partner_ids.invoice_ids
        

By creating a single invoice, it fires the following compute actions:

  • Create invoice
  • Compute account.invoice:is_late because date_invoice updated
  • Compute res.partner:has_late_bills because the partner has a new invoice
  • Compute res.partner.group:has_late_bills if one of the partners has a new has_late_bills value after recomputing it in the line above
  • Compute res.partner.group:has_late_bills because the one of the partners in the group has a new invoice

The last two actions are duplicates of each other. There is no reason why this needs to fire two times and can be resolved by updating our depends decorator.


@api.depends(‘partner_ids.has_late_bills’)
        

This may not be a huge problem in this specific example when updating a single invoice. It performs 4 operations instead of 3, but a 25% increase in computation starts to hurt at scale. Bulk editing 100 invoices turns into 400 operations instead of 300. 5 concurrent users bulk editing 100 invoices turns into 2000 operations instead of 1500.

In Conclusion

Think about this example within larger custom modules. This has 3 classes and 3 computed fields. In modules that have dozens or hundred of classes that use unnecessary dependencies on compute functions could severely hurt user experience.

Note: Watch out for stored, computed fields that cause both a computation and database write.

Luckily these are easy to track down! Log statements are your friend, and drawing simple dependency chain diagrams can allow you to cross out paths that already exists.

Best of luck Odoods!

Thanks For Reading

I appreciate you taking the time to read any of my articles. If you're looking for more ramblings, take a look at theentire catalog of articles I've written. Or feel free to give me a follow on Twitter, Instagram, or Github to see what else I've got going on. Or see my contact info below if you want to get in touch with me.

odoo
python
open source
web development
Share:

Holden Rehg, Author

Posted June 4, 2016

Other articles you might like