Merge lp:~openerp-dev/openobject-server/6.0-server-fix-stored-function-triggers into lp:openobject-server/6.0

Proposed by Olivier Dony (Odoo)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-server/6.0-server-fix-stored-function-triggers
Merge into: lp:openobject-server/6.0
Diff against target: 118 lines (+54/-38)
1 file modified
bin/osv/orm.py (+54/-38)
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/6.0-server-fix-stored-function-triggers
Reviewer Review Type Date Requested Status
OpenERP Publisher's Warranty Team Pending
Review via email: mp+53772@code.launchpad.net

Description of the change

Rewrite of ORM's _store_get_values() used to generate the list of stored function fields that need to be recomputed as a result of any create/write/unlink operation.

This rewrite attempts to make the code a bit more readable (had a headache just reading the previous version), but most importantly changes the logic to respect *all* priorities set on the function fields's triggers.
The previous version of this code only considered the priority of the first function field found for each model, hence causing hard-to-diagnose issues during updates of objects with complex inter-dependent fields.

This changes allows fixing bug 715470 as explained in the related merge proposal for addons:
https://code.launchpad.net/~openerp-dev/openobject-addons/trunk-bug-715470-ara/+merge/51288

Once merged in 6.0, this fix will be forward-ported to trunk.

To post a comment you must log in.
Revision history for this message
Olivier Dony (Odoo) (odo-openerp) wrote :

Note that the comments on top about the structure of _inherits and _inherit_fields are not really related to this issue, they're just provided for reference.

Unmerged revisions

3367. By Olivier Dony (Odoo)

[FIX] orm: rewrote and fixed _store_get_values to properly respect priority

_store_get_values is in charge of gathering the list of
function fields that should be recomputed as a result
of any write() operation, due to store flag triggers.
The previous version would group fields by model and
only respect the priority of the first trigger for a
given model, creating various problems when priority
of field computation is important.
The new version tries to be more readable and respect
all priorities.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/osv/orm.py'
2--- bin/osv/orm.py 2011-03-11 08:27:20 +0000
3+++ bin/osv/orm.py 2011-03-17 10:02:26 +0000
4@@ -396,7 +396,15 @@
5 _order = 'id'
6 _sequence = None
7 _description = None
8+
9+ # structure:
10+ # { 'parent_model': 'm2o_field', ... }
11 _inherits = {}
12+
13+ # structure:
14+ # { 'field_name': ('parent_model', 'm2o_field_to_reach_parent' , field_column_obj), ... }
15+ _inherit_fields = None
16+
17 _table = None
18 _invalids = set()
19 _log_create = False
20@@ -2803,7 +2811,7 @@
21 if f == order:
22 ok = False
23 if ok:
24- self.pool._store_function[object].append( (self._name, store_field, fnct, fields2, order, length))
25+ self.pool._store_function[object].append((self._name, store_field, fnct, tuple(fields2) if fields2 else None, order, length))
26 self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4]))
27
28 for (key, _, msg) in self._sql_constraints:
29@@ -3692,44 +3700,52 @@
30
31 :return: [(priority, model_name, [record_ids,], [function_fields,])]
32 """
33- # FIXME: rewrite, cleanup, use real variable names
34- # e.g.: http://pastie.org/1222060
35- result = {}
36- fncts = self.pool._store_function.get(self._name, [])
37- for fnct in range(len(fncts)):
38- if fncts[fnct][3]:
39- ok = False
40- if not fields:
41- ok = True
42- for f in (fields or []):
43- if f in fncts[fnct][3]:
44- ok = True
45- break
46- if not ok:
47- continue
48-
49- result.setdefault(fncts[fnct][0], {})
50-
51+ stored_functions = self.pool._store_function.get(self._name, [])
52+
53+ # use indexed names for the details of the stored_functions:
54+ model_name_, func_field_to_compute_, id_mapping_fnct_, trigger_fields_, priority_ = range(5)
55+
56+ # only keep functions that should be triggered for the ``fields``
57+ # being written to.
58+ if fields is None:
59+ # fields == None when unlink() is being called
60+ to_compute = stored_functions
61+ else:
62+ to_compute = [f for f in stored_functions \
63+ if ((not f[trigger_fields_]) or set(fields).intersection(f[trigger_fields_]))]
64+
65+ mapping = {}
66+ for function in to_compute:
67 # uid == 1 for accessing objects having rules defined on store fields
68- ids2 = fncts[fnct][2](self, cr, 1, ids, context)
69- for id in filter(None, ids2):
70- result[fncts[fnct][0]].setdefault(id, [])
71- result[fncts[fnct][0]][id].append(fnct)
72- dict = {}
73- for object in result:
74- k2 = {}
75- for id, fnct in result[object].items():
76- k2.setdefault(tuple(fnct), [])
77- k2[tuple(fnct)].append(id)
78- for fnct, id in k2.items():
79- dict.setdefault(fncts[fnct[0]][4], [])
80- dict[fncts[fnct[0]][4]].append((fncts[fnct[0]][4], object, id, map(lambda x: fncts[x][1], fnct)))
81- result2 = []
82- tmp = dict.keys()
83- tmp.sort()
84- for k in tmp:
85- result2 += dict[k]
86- return result2
87+ target_ids = [id for id in function[id_mapping_fnct_](self, cr, 1, ids, context) if id]
88+
89+ # the compound key must consider the priority and model name
90+ key = (function[priority_], function[model_name_])
91+ for target_id in target_ids:
92+ mapping.setdefault(key, {}).setdefault(target_id,set()).add(tuple(function))
93+
94+ # Here mapping looks like:
95+ # { (10, 'model_a') : { target_id1: [ (function_1_tuple, function_2_tuple) ], ... }
96+ # (20, 'model_a') : { target_id2: [ (function_3_tuple, function_4_tuple) ], ... }
97+ # (99, 'model_a') : { target_id1: [ (function_5_tuple, function_6_tuple) ], ... }
98+ # }
99+
100+ # Now we need to generate the batch function calls list
101+ # call_map =
102+ # { (10, 'model_a') : [(10, 'model_a', [record_ids,], [function_fields,])] }
103+ call_map = {}
104+ for ((priority,model), id_map) in mapping.iteritems():
105+ functions_ids_maps = {}
106+ # function_ids_maps =
107+ # { (function_1_tuple, function_2_tuple) : [target_id1, target_id2, ..] }
108+ for id, functions in id_map.iteritems():
109+ functions_ids_maps.setdefault(tuple(functions), []).append(id)
110+ for functions, ids in functions_ids_maps.iteritems():
111+ call_map.setdefault((priority,model),[]).append((priority, model, ids, [f[func_field_to_compute_] for f in functions]))
112+ ordered_keys = call_map.keys()
113+ ordered_keys.sort()
114+ result = reduce(operator.add, (call_map[k] for k in ordered_keys)) if call_map else []
115+ return result
116
117 def _store_set_values(self, cr, uid, ids, fields, context):
118 """Calls the fields.function's "implementation function" for all ``fields``, on records with ``ids`` (taking care of