OpenERP function fields, and the store= parameter

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.