Apr 052013
 

This is another one of those brain-RAM-to-blog-NVRAM dumps for my own future reference as much as anyone else’s benefit.  One thing I could never quite get my head around was the store= parameter in OpenERP.

OpenERP explains it like this:

store Parameter

It will calculate the field and store the result in the table. The field will be recalculated when certain fields are changed on other objects. It uses the following syntax:

store = {
    'object_name': (
            function_name,
            ['field_name1', 'field_name2'],
            priority)
}

It will call function function_name when any changes are written to fields in the list [‘field1’,’field2’] on object ‘object_name’. The function should have the following signature:

def function_name(self, cr, uid, ids, context=None):

Where ids will be the ids of records in the other object’s table that have changed values in the watched fields. The function should return a list of ids of records in its own table that should have the field recalculated. That list will be sent as a parameter for the main function of the field.

Now note the parameter self. Quite often, these are defined as methods of the object that owns the given function field. self, in the Python vernacular, is similar to the this keyword in C++; it is a reference to the object that owns the method. And here, the use of the name self is misleading.

I had occasion to derive a few timesheet objects so that I could rename a few fields. I didn’t want to change the implementation, just the name. And I wanted to do it in one place, not go do a search and replace on each and every view for the timesheet. The model seemed the most appropriate place for it. Unfortunately, the only way you change a field name in this way, is to copy and paste the definition.

So I tried adding this to my derived model (note, we were able to leave _progress_rate alone, since I had in fact overridden that method in this same object to fix a bug in the original, otherwise I’d use the same lambda trick here):

        'planned_hours': fields.function(
            _progress_rate,
            multi="progress", string='Total Planned Time',
            help=   "Sum of hours planned by the project manager for all "  \
                    "tasks related to this project and its child projects",
            store = {
                'project.project': (
                    lambda self, cr, uid, ids, context=None :               \
                        self._get_project_and_parents(                      \
                            cr, uid, ids, context),
                    ['tasks', 'parent_id', 'child_ids'], 10),
                'project.task': (
                    lambda self, cr, uid, ids, context=None :               \
                        self._get_projects_from_tasks(                      \
                            cr, uid, ids, context),
                    ['planned_hours', 'remaining_hours',
                    'work_ids', 'state'], 20),
            }),

The lambda functions are just a quick way of picking up the functions that would later be inherited by the base class (handled by the osv.osv object).

Imagine my surprise when I get told that there is no attribute called _get_projects_from_tasks. … Hang on, I’m sure that’s what it’s called! I check again, yes, I spelt it correctly. I look closer at the backtrace:

AttributeError: 'project.task' object has no attribute '_get_projects_from_tasks'

I’ve underlined the significant bit I had missed earlier. Despite the fact that this _get_project_from_tasks is in fact, defined as a method in project.project, the argument that’s passed in as self is not a project.project instance, but a project.task.

self is not in fact, self, but another object entirely.

So from now on, I shall no longer call this parameter self, as this will trip regular Python programmers up — it should be called what it is. My field definition now looks like this:

        'planned_hours': fields.function(
            _progress_rate,
            multi="progress", string='Total Planned Time',
            help=   "Sum of hours planned by the project manager for all "  \
                    "tasks related to this project and its child projects",
            store={
                'project.project': (
                    lambda project_obj, cr, uid, ids, context=None: \
                        project_obj._get_project_and_parents(cr, uid, ids, context),
                    ['tasks', 'parent_id', 'child_ids'],
                    10
                ),
                'project.task': (
                    lambda task_obj, cr, uid, ids, context=None:    \
                        task_obj.pool.get('project.project'         \
                            )._get_projects_from_tasks(cr, uid,     \
                                ids, context),
                    ['planned_hours', 'remaining_hours', 'work_ids', 'state'],
                    20
                ),
            }),

Hopefully that should be clear next time I, or anyone else, comes across it.

  5 Responses to “OpenERP function fields, and the store= parameter”

  1. Thanks, I thought store was just a boolean! True or False..
    Does a “related” field have the same option? I’ve had problems with related fields not updating when the other object changes, but maybe I should be using a function rather.

    • I think the related field is basically a function field internally, they’ve just provided the functions that let the magic happen.

      OpenERP’s docs (or rather, Odoo’s docs, since that’s what they call themselves now) are not great, and sometimes I find the store= argument doesn’t always work as expected either.

      • Thanks, I checked in the modules and found that it is used in related fields. I’m trying to add a “date_done” field on a sale_order when the sock picking is delivered, but it doesnt get updated. This is what my code looks like now:
        (in inherited version of sale.order:)

        'date_shipped': fields.related('picking_ids','date_done', type="date", relation='stock.picking', string="Date Delivered", store={
        'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['date_done'], 10),
        }),

        I’m not sure what the ids param should be though – how does it know that it should update if any of the picking_ids change??

        Thanks for your help!

        • One thing I find confusing, is that “self” normally refers to the object that owns the method being called. In the case of your code, ‘self’ will be the sale.order object.

          In this instance I’d find it helpful to call it ‘sale_order_obj’ to make it abundantly clear.

          ‘ids’ though, are the IDs of the model being modified (stock.picking?). It might be helpful once again, to rename it appropriately. The lambda should return the IDs of the ‘sale.order’ records that need changing.

          One way I’ve done this, is to do a search and return that:

          i.e.

          lambda sale_order, cr, uid, stock_picking_ids, context=None : sale_order.search(cr, uid, [('sale_order_id_field','in',stock_picking_ids)], context=context)

          Note that’s written on the fly and not tested, so your mileage may vary. 😉

      • Not to worry.. I made it a normal date field and then just updated it when the related picking changed.. much easier than figuring out the magic behind the scenes 😉