Merge lp:~openerp-dev/openobject-server/trunk-cleanup-ir-values into lp:openobject-server

Proposed by Olivier Dony (Odoo)
Status: Merged
Merged at revision: 3713
Proposed branch: lp:~openerp-dev/openobject-server/trunk-cleanup-ir-values
Merge into: lp:openobject-server
Diff against target: 921 lines (+498/-250)
5 files modified
openerp/addons/base/ir/ir.xml (+59/-82)
openerp/addons/base/ir/ir_values.py (+380/-162)
openerp/addons/base/security/base_security.xml (+7/-0)
openerp/addons/base/security/ir.model.access.csv (+1/-2)
openerp/addons/base/test/test_ir_values.yml (+51/-4)
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/trunk-cleanup-ir-values
Reviewer Review Type Date Requested Status
Naresh(OpenERP) Pending
OpenERP Core Team Pending
Review via email: mp+73809@code.launchpad.net

Description of the change

= Summary =

While looking for a fix for bug 817430, we figured it was time to start improving our ir.values internal model.

This internal model has always been a mess. Lately it is still used for two things: storing user-defined default values, and binding actions to certain 'action slots' in the UI (mostly wizards, reports, and related links visible in the sidebar for each model).
The model definition is totally opaque and cryptic (column names like key2, key, object, ..), and their API is not better, with two generic methods (get/set), just as opaque and cryptic.

This branch is a first step towards a clean refactoring, requiring minimal backend and API change while at the same time providing an implicit fix for 817430 and preparing for a better future

== Proposed fix ==

Ideally, we should have separate models with their own clean and specific API: one for storing user defaults, and one for storing action bindings (for example: ir.user.defaults and ir.actions.bindings)
This could be the next step.

Until then, this branch attempts to preserve the current API for perfect backwards-compatibility, does not require any migration, but introduces two separate APIs and underlying implementations for the two aspects: default vs. action bindings, and fixes bug 817430 in the process.

== Implementation notes ==

The main changes are:
- obsolete columns have been removed totally from ir.values (the meta* stuff, and the object column)
- existing columns have been documented, but preserved
- the set() and get() methods are deprecated, and internally delegate their tasks to the new set_action/get_actions and set_default/get_defaults.
- the API is now documented
- the administration UI for ir.values has been split in 2: defaults and action bindings now have their own menus and views

== Tests ==

- the existing YAML tests for ir.values have been extended to cover smoke-testing for both defaults and action bindings
- the tests do not use the newer API directly yet, but they do so indirectly because the old API relies on the new implementation now

== Next steps ==

1. The rest of the server code (and clients) should be adapted progressively to use the new API, but that does not need to happen immediately.
2. For next version we should split everything properly into separate models. There is already an rudimentary and unused "ir.defaults" model lying around, but it's not clean enough - should be removed/refactored in the process.

To post a comment you must log in.
Revision history for this message
xrg (xrg) wrote :

On Friday 02 September 2011, you wrote:
> Olivier Dony (OpenERP) has proposed merging
> lp:~openerp-dev/openobject-server/trunk-cleanup-ir-values into
> lp:openobject-server.
>
Right. This was mostly needed.

Note, that a shortcoming of the current implementation (with the "key" field)
is that we are allowed to base our defaults only on one other field of the ORM
model. This makes defaults unusable in a range of situations, meaning that we
need to write our own pythonic default_get() there.

You should consider a little more flexible design for the 'ir.default.values'
model to come.

--
Say NO to spam and viruses. Stop using Microsoft Windows!

Revision history for this message
Xavier (Open ERP) (xmo-deactivatedaccount) wrote :

I find it a bit strange that the `key2` column is defined as 128c wide, and the string put into it @449 of the diff is cut off at 200 characters. Not only do they mismatch, there's not even a warning. Would it not be better to have the same limit on both, and maybe to remove the limit altogether? (it won't bother postgres, though I'm not sure the ORM can do that).

I also think it would be better to raise some sort of error if the value tentatively set is wider than the field, instead of just removing everything which sticks out silently and potentially storing an invalid condition.

The `object` key seems completely unused (it's set to `False` in set_default and to `True` in set_action, and it does not seem used at all in the code, is there really a need to keep setting it in default values & al? Can't it just be made into a function field writing to nothing and reading from itself being a default or an action?

The `get_defaults` documentation talks about priority and links to `set_defaults`, which reads like `set_defaults` will talk further about priority setting, but that does not seem to be the case. It should also indicate that setting a default for a given company has a higher priority than for none. And the precedence between user_id and company_id priorities is not explained. From my reading, it's `Nothing < company_id < user_id < (user_id, company_id)`. Is that correct?

It's also unclear what the condition is supposed to be exactly: a domain? Or a Python expression? Or some sort of weird custom format? From the get_defaults code (diff line 482) it looks like it's just some sort of tag, with a strict identity match, is that it?

3565. By Olivier Dony (Odoo)

[FIX] ir.values: get_defaults() should cut the condition at 200 chars to match the conditions that were set

Revision history for this message
Olivier Dony (Odoo) (odo-openerp) wrote :
Download full text (3.3 KiB)

> You should consider a little more flexible design for the 'ir.default.values'
> model to come.

Yup, we could imagine a more flexible model, but I'm not sure how much more. Mostly when it comes to multi-valued triggers, this is best foreseen by the module itself via default_get or some on_change, isn't it? Any more specific ideas?

BTW, for those who don't know, the current system is very basic: it lets users save default values for each field, optionally triggered by a certain value of onanother field (like the zip code for an address, or the partner on an invoice).

> I find it a bit strange that the `key2` column is defined as 128c wide, and
> the string put into it @449 of the diff is cut off at 200 characters. Not only
> do they mismatch, there's not even a warning. Would it not be better to have
> the same limit on both, and maybe to remove the limit altogether? (it won't
> bother postgres, though I'm not sure the ORM can do that).

That's indeed one of the consequences of the current model. For default values key2 can hold a string of the form "field=value", which acts as an opaque qualifier for the key, as far as the server-side is concerned. Instead of making key2 a TEXT, the original implementation arbitrarily decided to cut off the conditions at 200 chars IN and OUT - effectively causing collisions for larger values - though I admit it does not occur often.
Reminds me the 200c limit needs to be enforced in get_defaults() too, fixed that!

Raising exceptions won't help unless we also fix the client side to make sure it handles these errors properly - and my point is not to waste time on this right now, we'll do it when we properly revamp the whole thing.

> The `object` key seems completely unused (it's set to `False` in set_default
> and to `True` in set_action, and it does not seem used at all in the code, is
> there really a need to keep setting it in default values & al? Can't it just
> be made into a function field writing to nothing and reading from itself being
> a default or an action?

Actually that's right, we don't actually need to care about it anymore, as the behavior is now fixed
in both cases. I'll drop it, this will further simplify the view and code.

> The `get_defaults` documentation talks about priority and links to
> `set_defaults`, which reads like `set_defaults` will talk further about
> priority setting, but that does not seem to be the case. It should also
> indicate that setting a default for a given company has a higher priority than
> for none. And the precedence between user_id and company_id priorities is not
> explained. From my reading, it's `Nothing < company_id < user_id < (user_id,
> company_id)`. Is that correct?

Correct, will fix the doc to mention it - this will probably stay true in the future API.

> It's also unclear what the condition is supposed to be exactly: a domain? Or a
> Python expression? Or some sort of weird custom format? From the get_defaults
> code (diff line 482) it looks like it's just some sort of tag, with a strict
> identity match, is that it?

It's actually left to the caller to use it as they wish, but in practice the clients use it to store a 'field=value' expres...

Read more...

Revision history for this message
Thibaut DIRLIK (Logica) (thibaut-dirlik) wrote :

I'm glad to see some improvements and documentation about this! I made a bug request some days ago about the possibility to add a domain on ir.values (or on "actions bindings" :) to show/hide reports or actions based on the object they will be called on.

For example, this would let developpers do some reports like "Order Sumarry" once the order is done. This is a stupid example, juste to show my point.

It could be the good time to make this !

Anyway, thanks for your work Olivier, nice to see things evolve in the good direction.

3566. By Olivier Dony (Odoo)

[MERGE] lastest trunk

3567. By Olivier Dony (Odoo)

[IMP] ir.values: improve security: users can only write to their personal defaults

Administrator access is required to set defaults for
everybody, as well as to alter the action bindings.

3568. By Olivier Dony (Odoo)

[IMP] ir.values: improved defaults doc + removed `object` column completely

The `object` column actually directly depended on the value of
the `key` column, so it can be totally removed with no side-effects.
Docstrings updated following review comments.

Revision history for this message
Olivier Dony (Odoo) (odo-openerp) wrote :

Thanks for the feedback so far.

I've updated the docstrings for {get,set}_default to explain the priorities, and removed the `object` column that was in fact useless now, as noted by Xavier.

I forgot to mention that this branch originally stems from bug 817430 - it's normally a low-risk fix for the bug, requiring minimal backend and API change, but preparing for a better future.

Revision history for this message
Naresh(OpenERP) (nch-openerp) wrote :

Hello Olivier,

The improvements are good but it does not fix the bug that led the improvement. It still returns the wrong set of records.

Thanks,

3569. By Olivier Dony (Odoo)

[MERGE] latest trunk

3570. By Olivier Dony (Odoo)

[FIX] ir.values.get_defaults: typo in SQL query - spotted by Naresh, thanks!

3571. By Vo Minh Thu

[MERGE] merged trunk.

3572. By Vo Minh Thu

[FIX] ir_values: missing trimming of the condition (as done elsewhere).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openerp/addons/base/ir/ir.xml'
2--- openerp/addons/base/ir/ir.xml 2011-10-02 15:44:57 +0000
3+++ openerp/addons/base/ir/ir.xml 2011-10-03 15:51:44 +0000
4@@ -7,44 +7,50 @@
5 <field name="name">ir.values.form.action</field>
6 <field name="model">ir.values</field>
7 <field name="type">form</field>
8- <field name="priority">20</field>
9 <field name="arch" type="xml">
10- <form string="Connect Events to Actions">
11- <field name="name" required="1"/>
12+ <form string="Action Bindings">
13+ <field name="name"/>
14 <newline/>
15 <group col="2" colspan="2">
16- <separator string="Action Source" colspan="2"/>
17+ <separator string="Action Binding" colspan="2"/>
18 <field name="model_id" on_change="onchange_object_id(model_id)"/>
19+ <field name="model"/>
20 <field name="res_id"/>
21- <field name="key2" required="1"/>
22+ <field name="key2"/>
23 </group>
24 <group col="2" colspan="2">
25- <separator string="Action To Launch" colspan="2"/>
26+ <separator string="Action" colspan="2"/>
27 <field name="action_id" on_change="onchange_action_id(action_id)"/>
28-
29- <field name="object" readonly="1"/>
30-
31- </group>
32- <group col="2" colspan="2">
33- <separator string="Values for Event Type" colspan="2"/>
34- <label string="client_action_multi, client_action_relate" colspan="2"/>
35- <label string="tree_but_open, client_print_multi" colspan="2"/>
36- </group>
37- <group col="2" colspan="2">
38- <separator colspan="2" string="Value"/>
39- <field name="value_unpickle" nolabel="1" colspan="4"/>
40- </group>
41- <group col="2" colspan="2">
42- <separator colspan="2" string="Metadata"/>
43- <field name="meta_unpickle" nolabel="1"/>
44- </group>
45- <group col="2" colspan="2">
46- <separator colspan="2" string=""/>
47- <field name="user_id"/>
48- <field name="company_id" groups="base.group_multi_company"/>
49- </group>
50- </form>
51-
52+ <field name="value_unpickle" colspan="4" string="Action Reference"/>
53+ </group>
54+ </form>
55+ </field>
56+ </record>
57+
58+ <record id="values_view_form_defaults" model="ir.ui.view">
59+ <field name="name">ir.values.form.defaults</field>
60+ <field name="model">ir.values</field>
61+ <field name="type">form</field>
62+ <field name="arch" type="xml">
63+ <form string="User-defined Defaults">
64+ <field name="name"/>
65+ <newline/>
66+ <group col="2" colspan="2">
67+ <separator string="Model" colspan="2"/>
68+ <field name="model_id" on_change="onchange_object_id(model_id)"/>
69+ <field name="model"/>
70+ <field name="key2" string="Condition"/>
71+ </group>
72+ <group col="2" colspan="2">
73+ <separator string="Default Value" colspan="2"/>
74+ <field name="value_unpickle" colspan="4" nolabel="1"/>
75+ </group>
76+ <group col="2" colspan="2">
77+ <separator colspan="2" string="Default Value Scope"/>
78+ <field name="user_id"/>
79+ <field name="company_id" groups="base.group_multi_company"/>
80+ </group>
81+ </form>
82 </field>
83 </record>
84
85@@ -53,10 +59,9 @@
86 <field name="model">ir.values</field>
87 <field name="type">tree</field>
88 <field name="arch" type="xml">
89- <tree string="Client Actions">
90+ <tree string="Action Bindings/Defaults">
91 <field name="name"/>
92 <field name="model"/>
93- <field name="action_id"/>
94 <field name="key2"/>
95 </tree>
96 </field>
97@@ -73,7 +78,7 @@
98 <field name="key2"/>
99 <newline/>
100 <group expand="0" string="Group By...">
101- <filter string="Object" icon="terp-stock_align_left_24" domain="[]" context="{'group_by':'model'}"/>
102+ <filter string="Model" icon="terp-stock_align_left_24" domain="[]" context="{'group_by':'model'}"/>
103 <filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'key2'}"/>
104 </group>
105 </search>
106@@ -81,23 +86,21 @@
107 </record>
108
109 <record id="act_values_form_action" model="ir.actions.act_window">
110- <field name="name">Client Events</field>
111+ <field name="name">Action Bindings</field>
112 <field name="type">ir.actions.act_window</field>
113 <field name="res_model">ir.values</field>
114 <field name="view_type">form</field>
115 <field name="view_mode">tree,form</field>
116 <field name="search_view_id" ref="values_view_search_action"/>
117 <field name="domain">[('key','=','action')]</field>
118- <field name="context">{'read':'default','default_object':1}</field>
119+ <field name="context">{'default_key':'action'}</field>
120 </record>
121-
122 <record model="ir.actions.act_window.view" id="action_values_tree_view">
123 <field name="sequence" eval="1"/>
124 <field name="view_mode">tree</field>
125 <field name="view_id" ref="values_view_tree_action"/>
126 <field name="act_window_id" ref="act_values_form_action"/>
127 </record>
128-
129 <record model="ir.actions.act_window.view" id="action_values_form_view">
130 <field name="sequence" eval="2"/>
131 <field name="view_mode">form</field>
132@@ -105,54 +108,27 @@
133 <field name="act_window_id" ref="act_values_form_action"/>
134 </record>
135
136-
137-
138-
139- <!-- Values -->
140-
141- <record id="values_view_form" model="ir.ui.view">
142- <field name="name">ir.values.form</field>
143- <field name="model">ir.values</field>
144- <field name="type">form</field>
145- <field name="arch" type="xml">
146- <form string="Values">
147- <field name="name" select="1"/>
148- <field name="model" select="1"/>
149- <field name="key" select="1"/>
150- <field name="key2" select="2"/>
151- <field name="object" select="2"/>
152- <field name="res_id"/>
153- <field name="user_id" select="2"/>
154- <field name="company_id" select="2"/>
155- <field name="value_unpickle"/>
156- <field name="meta_unpickle"/>
157- </form>
158- </field>
159- </record>
160-
161- <record id="values_view_tree" model="ir.ui.view">
162- <field name="name">ir.values.tree</field>
163- <field name="model">ir.values</field>
164- <field name="type">tree</field>
165- <field name="arch" type="xml">
166- <tree string="Values">
167- <field name="name"/>
168- <field name="model"/>
169- <field name="key"/>
170- <field name="key2"/>
171- <field name="user_id"/>
172- <field name="company_id"/>
173- </tree>
174- </field>
175- </record>
176-
177- <record id="act_values_form" model="ir.actions.act_window">
178- <field name="name">Client Actions Connections</field>
179+ <record id="act_values_form_defaults" model="ir.actions.act_window">
180+ <field name="name">User-defined Defaults</field>
181 <field name="type">ir.actions.act_window</field>
182 <field name="res_model">ir.values</field>
183 <field name="view_type">form</field>
184- <field name="view_id" ref="values_view_tree"/>
185- <field name="context">{'read':'default'}</field>
186+ <field name="view_mode">tree,form</field>
187+ <field name="search_view_id" ref="values_view_search_action"/>
188+ <field name="domain">[('key','=','default')]</field>
189+ <field name="context">{'default_key':'default','default_key2':''}</field>
190+ </record>
191+ <record model="ir.actions.act_window.view" id="action_values_defaults_tree_view">
192+ <field name="sequence" eval="1"/>
193+ <field name="view_mode">tree</field>
194+ <field name="view_id" ref="values_view_tree_action"/>
195+ <field name="act_window_id" ref="act_values_form_defaults"/>
196+ </record>
197+ <record model="ir.actions.act_window.view" id="action_values_defaults_form_view">
198+ <field name="sequence" eval="2"/>
199+ <field name="view_mode">form</field>
200+ <field name="view_id" ref="values_view_form_defaults"/>
201+ <field name="act_window_id" ref="act_values_form_defaults"/>
202 </record>
203
204 <!-- Sequences -->
205@@ -344,6 +320,7 @@
206 <menuitem id="next_id_6" name="Actions" parent="base.next_id_4" sequence="1"/>
207 <menuitem action="ir_sequence_actions" id="menu_ir_sequence_actions" parent="next_id_6"/>
208 <menuitem action="act_values_form_action" id="menu_values_form_action" parent="next_id_6"/>
209+ <menuitem action="act_values_form_defaults" id="menu_values_form_defaults" parent="next_id_6"/>
210
211 <!--Filters form view-->
212
213
214=== modified file 'openerp/addons/base/ir/ir_values.py'
215--- openerp/addons/base/ir/ir_values.py 2011-07-22 14:46:06 +0000
216+++ openerp/addons/base/ir/ir_values.py 2011-10-03 15:51:44 +0000
217@@ -28,19 +28,89 @@
218 'report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml',
219 'report_sxw_content_data', 'report_rml_content_data', 'search_view', ))
220
221+#: Possible slots to bind an action to with :meth:`~.set_action`
222+ACTION_SLOTS = [
223+ "client_action_multi", # sidebar wizard action
224+ "client_print_multi", # sidebar report printing button
225+ "client_action_relate", # sidebar related link
226+ "tree_but_open", # double-click on item in tree view
227+ "tree_but_action", # deprecated: same as tree_but_open
228+ ]
229+
230+
231 class ir_values(osv.osv):
232+ """Holds internal model-specific action bindings and user-defined default
233+ field values. definitions. This is a legacy internal model, mixing
234+ two different concepts, and will likely be updated or replaced in a
235+ future version by cleaner, separate models. You should not depend
236+ explicitly on it.
237+
238+ The purpose of each ``ir.values`` entry depends on its type, defined
239+ by the ``key`` column:
240+
241+ * 'default': user-defined default values, used when creating new
242+ records of this model:
243+ * 'action': binding of an action to a particular *action slot* of
244+ this model, making the action easily available in the user
245+ interface for this model.
246+
247+ The ``key2`` column acts as a qualifier, further refining the type
248+ of the entry. The possible values are:
249+
250+ * for 'default' entries: an optional condition restricting the
251+ cases where this particular default value will be applicable,
252+ or ``False`` for no condition
253+ * for 'action' entries: the ``key2`` qualifier is one of the available
254+ action slots, defining how this action can be invoked:
255+
256+ * ``'client_print_multi'`` for report printing actions that will
257+ be available on views displaying items from this model
258+ * ``'client_action_multi'`` for assistants (wizards) actions
259+ that will be available in views displaying objects of this model
260+ * ``'client_action_relate'`` for links towards related documents
261+ that should be available in views displaying objects of this model
262+ * ``'tree_but_open'`` for actions that will be triggered when
263+ double-clicking an item from this model in a hierarchical tree view
264+
265+ Each entry is specific to a model (``model`` column), and for ``'actions'``
266+ type, may even be made specific to a given record of that model when the
267+ ``res_id`` column contains a record ID (``False`` means it's global for
268+ all records).
269+
270+ The content of the entry is defined by the ``value`` column, which may either
271+ contain an arbitrary value, or a reference string defining the action that
272+ should be executed.
273+
274+ .. rubric:: Usage: default values
275+
276+ The ``'default'`` entries are usually defined manually by the
277+ users, and set by their UI clients calling :meth:`~.set_default`.
278+ These default values are then automatically used by the
279+ ORM every time a new record is about to be created, i.e. when
280+ :meth:`~openerp.osv.osv.osv.default_get`
281+ or :meth:`~openerp.osv.osv.osv.create` are called.
282+
283+ .. rubric:: Usage: action bindings
284+
285+ Business applications will usually bind their actions during
286+ installation, and OpenERP UI clients will apply them as defined,
287+ based on the list of actions included in the result of
288+ :meth:`~openerp.osv.osv.osv.fields_view_get`,
289+ or directly returned by explicit calls to :meth:`~.get_actions`.
290+ """
291 _name = 'ir.values'
292
293 def _value_unpickle(self, cursor, user, ids, name, arg, context=None):
294 res = {}
295- for report in self.browse(cursor, user, ids, context=context):
296- value = report[name[:-9]]
297- if not report.object and value:
298+ for record in self.browse(cursor, user, ids, context=context):
299+ value = record[name[:-9]]
300+ if record.key == 'default' and value:
301+ # default values are pickled on the fly
302 try:
303 value = str(pickle.loads(value))
304- except:
305+ except Exception:
306 pass
307- res[report.id] = value
308+ res[record.id] = value
309 return res
310
311 def _value_pickle(self, cursor, user, id, name, value, arg, context=None):
312@@ -49,18 +119,20 @@
313 ctx = context.copy()
314 if self.CONCURRENCY_CHECK_FIELD in ctx:
315 del ctx[self.CONCURRENCY_CHECK_FIELD]
316- if not self.browse(cursor, user, id, context=context).object:
317+ record = self.browse(cursor, user, id, context=context)
318+ if record.key == 'default':
319+ # default values are pickled on the fly
320 value = pickle.dumps(value)
321 self.write(cursor, user, id, {name[:-9]: value}, context=ctx)
322
323- def onchange_object_id(self, cr, uid, ids, object_id, context={}):
324+ def onchange_object_id(self, cr, uid, ids, object_id, context=None):
325 if not object_id: return {}
326 act = self.pool.get('ir.model').browse(cr, uid, object_id, context=context)
327 return {
328 'value': {'model': act.model}
329 }
330
331- def onchange_action_id(self, cr, uid, ids, action_id, context={}):
332+ def onchange_action_id(self, cr, uid, ids, action_id, context=None):
333 if not action_id: return {}
334 act = self.pool.get('ir.actions.actions').browse(cr, uid, action_id, context=context)
335 return {
336@@ -68,32 +140,47 @@
337 }
338
339 _columns = {
340- 'name': fields.char('Name', size=128),
341- 'model_id': fields.many2one('ir.model', 'Object', size=128,
342- help="This field is not used, it only helps you to select a good model."),
343- 'model': fields.char('Object Name', size=128, select=True),
344- 'action_id': fields.many2one('ir.actions.actions', 'Action',
345- help="This field is not used, it only helps you to select the right action."),
346- 'value': fields.text('Value'),
347+ 'name': fields.char('Name', size=128, required=True),
348+ 'model': fields.char('Model Name', size=128, select=True, required=True,
349+ help="Model to which this entry applies"),
350+
351+ # TODO: model_id and action_id should be read-write function fields
352+ 'model_id': fields.many2one('ir.model', 'Model (change only)', size=128,
353+ help="Model to which this entry applies - "
354+ "helper field for setting a model, will "
355+ "automatically set the correct model name"),
356+ 'action_id': fields.many2one('ir.actions.actions', 'Action (change only)',
357+ help="Action bound to this entry - "
358+ "helper field for binding an action, will "
359+ "automatically set the correct reference"),
360+
361+ 'value': fields.text('Value', help="Default value (pickled) or reference to an action"),
362 'value_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle,
363- method=True, type='text', string='Value'),
364- 'object': fields.boolean('Is Object'),
365- 'key': fields.selection([('action','Action'),('default','Default')], 'Type', size=128, select=True),
366- 'key2' : fields.char('Event Type', size=128, select=True, help="The kind of action or button on the client side "
367- "that will trigger the action. One of: "
368- "client_action_multi, client_action_relate, tree_but_open, "
369- "client_print_multi"),
370- 'meta': fields.text('Meta Datas'),
371- 'meta_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle,
372- method=True, type='text', string='Metadata'),
373- 'res_id': fields.integer('Object ID', help="Keep 0 if the action must appear on all resources.", select=True),
374- 'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True),
375- 'company_id': fields.many2one('res.company', 'Company', select=True)
376+ type='text',
377+ string='Default value or action reference'),
378+ 'key': fields.selection([('action','Action'),('default','Default')],
379+ 'Type', size=128, select=True, required=True,
380+ help="- Action: an action attached to one slot of the given model\n"
381+ "- Default: a default value for a model field"),
382+ 'key2' : fields.char('Qualifier', size=128, select=True,
383+ help="For actions, one of the possible action slots: \n"
384+ " - client_action_multi\n"
385+ " - client_print_multi\n"
386+ " - client_action_relate\n"
387+ " - tree_but_open\n"
388+ "For defaults, an optional condition"
389+ ,),
390+ 'res_id': fields.integer('Record ID', select=True,
391+ help="Database identifier of the record to which this applies. "
392+ "0 = for all records"),
393+ 'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True,
394+ help="If set, action binding only applies for this user."),
395+ 'company_id': fields.many2one('res.company', 'Company', ondelete='cascade', select=True,
396+ help="If set, action binding only applies for this company")
397 }
398 _defaults = {
399- 'key': lambda *a: 'action',
400- 'key2': lambda *a: 'tree_but_open',
401- 'company_id': lambda *a: False
402+ 'key': 'action',
403+ 'key2': 'tree_but_open',
404 }
405
406 def _auto_init(self, cr, context=None):
407@@ -102,140 +189,271 @@
408 if not cr.fetchone():
409 cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')
410
411- def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
412+ def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False):
413+ """Defines a default value for the given model and field_name. Any previous
414+ default for the same scope (model, field_name, value, for_all_users, company_id, condition)
415+ will be replaced and lost in the process.
416+
417+ Defaults can be later retrieved via :meth:`~.get_defaults`, which will return
418+ the highest priority default for any given field. Defaults that are more specific
419+ have a higher priority, in the following order (highest to lowest):
420+
421+ * specific to user and company
422+ * specific to user only
423+ * specific to company only
424+ * global to everyone
425+
426+ :param string model: model name
427+ :param string field_name: field name to which the default applies
428+ :param value: the default field value to set
429+ :type value: any serializable Python value
430+ :param bool for_all_users: whether the default should apply to everybody or only
431+ the user calling the method
432+ :param int company_id: optional ID of the company to which the default should
433+ apply. If omitted, the default will be global. If True
434+ is passed, the current user's company will be used.
435+ :param string condition: optional condition specification that can be used to
436+ restrict the applicability of the default values
437+ (e.g. based on another field's value). This is an
438+ opaque string as far as the API is concerned, but client
439+ stacks typically use single-field conditions in the
440+ form ``'key=stringified_value'``.
441+ (Currently, the condition is trimmed to 200 characters,
442+ so values that share the same first 200 characters always
443+ match)
444+ :return: id of the newly created ir.values entry
445+ """
446 if isinstance(value, unicode):
447 value = value.encode('utf8')
448- if not isobject:
449- value = pickle.dumps(value)
450- if meta:
451- meta = pickle.dumps(meta)
452- assert isinstance(models, (list, tuple)), models
453- ids_res = []
454- for model in models:
455+ if company_id is True:
456+ # should be company-specific, need to get company id
457+ user = self.pool.get('res.users').browse(cr, uid, uid)
458+ company_id = user.company_id.id
459+
460+ # remove existing defaults for the same scope
461+ search_criteria = [
462+ ('key', '=', 'default'),
463+ ('key2', '=', condition and condition[:200]),
464+ ('model', '=', model),
465+ ('name', '=', field_name),
466+ ('user_id', '=', False if for_all_users else uid),
467+ ('company_id','=', company_id)
468+ ]
469+ self.unlink(cr, uid, self.search(cr, uid, search_criteria))
470+
471+ return self.create(cr, uid, {
472+ 'name': field_name,
473+ 'value': pickle.dumps(value),
474+ 'model': model,
475+ 'key': 'default',
476+ 'key2': condition and condition[:200],
477+ 'user_id': False if for_all_users else uid,
478+ 'company_id': company_id,
479+ })
480+
481+ def get_defaults(self, cr, uid, model, condition=False):
482+ """Returns any default values that are defined for the current model and user,
483+ (and match ``condition``, if specified), previously registered via
484+ :meth:`~.set_default`.
485+
486+ Defaults are global to a model, not field-specific, but an optional
487+ ``condition`` can be provided to restrict matching default values
488+ to those that were defined for the same condition (usually based
489+ on another field's value).
490+
491+ Default values also have priorities depending on whom they apply
492+ to: only the highest priority value will be returned for any
493+ field. See :meth:`~.set_default` for more details.
494+
495+ :param string model: model name
496+ :param string condition: optional condition specification that can be used to
497+ restrict the applicability of the default values
498+ (e.g. based on another field's value). This is an
499+ opaque string as far as the API is concerned, but client
500+ stacks typically use single-field conditions in the
501+ form ``'key=stringified_value'``.
502+ (Currently, the condition is trimmed to 200 characters,
503+ so values that share the same first 200 characters always
504+ match)
505+ :return: list of default values tuples of the form ``(id, field_name, value)``
506+ (``id`` is the ID of the default entry, usually irrelevant)
507+ """
508+ # use a direct SQL query for performance reasons,
509+ # this is called very often
510+ query = """SELECT v.id, v.name, v.value FROM ir_values v
511+ LEFT JOIN res_users u ON (v.user_id = u.id)
512+ WHERE v.key = %%s AND v.model = %%s
513+ AND (v.user_id = %%s OR v.user_id IS NULL)
514+ AND (v.company_id IS NULL OR
515+ v.company_id =
516+ (SELECT company_id from res_users where id = %%s)
517+ )
518+ %s
519+ ORDER BY v.user_id, u.company_id"""
520+ query = query % ('AND v.key2 = %s' if condition else '')
521+ params = ('default', model, uid, uid)
522+ if condition:
523+ params += (condition[:200],)
524+ cr.execute(query, params)
525+
526+ # keep only the highest priority default for each field
527+ defaults = {}
528+ for row in cr.dictfetchall():
529+ defaults.setdefault(row['name'],
530+ (row['id'], row['name'], pickle.loads(row['value'].encode('utf-8'))))
531+ return defaults.values()
532+
533+ def set_action(self, cr, uid, name, action_slot, model, action, res_id=False):
534+ """Binds an the given action to the given model's action slot - for later
535+ retrieval via :meth:`~.get_actions`. Any existing binding of the same action
536+ to the same slot is first removed, allowing an update of the action's name.
537+ See the class description for more details about the various action
538+ slots: :class:`~ir_values`.
539+
540+ :param string name: action label, usually displayed by UI client
541+ :param string action_slot: the action slot to which the action should be
542+ bound to - one of ``client_action_multi``,
543+ ``client_print_multi``, ``client_action_relate``,
544+ ``tree_but_open``.
545+ :param string model: model name
546+ :param string action: action reference, in the form ``'model,id'``
547+ :param int res_id: optional record id - will bind the action only to a
548+ specific record of the model, not all records.
549+ :return: id of the newly created ir.values entry
550+ """
551+ assert isinstance(action, basestring) and ',' in action, \
552+ 'Action definition must be an action reference, e.g. "ir.actions.act_window,42"'
553+ assert action_slot in ACTION_SLOTS, \
554+ 'Action slot (%s) must be one of: %r' % (action_slot, ACTION_SLOTS)
555+
556+ # remove existing action definition of same slot and value
557+ search_criteria = [
558+ ('key', '=', 'action'),
559+ ('key2', '=', action_slot),
560+ ('model', '=', model),
561+ ('res_id', '=', res_id or 0), # int field -> NULL == 0
562+ ('value', '=', action),
563+ ]
564+ self.unlink(cr, uid, self.search(cr, uid, search_criteria))
565+
566+ return self.create(cr, uid, {
567+ 'key': 'action',
568+ 'key2': action_slot,
569+ 'model': model,
570+ 'res_id': res_id,
571+ 'name': name,
572+ 'value': action,
573+ })
574+
575+ def get_actions(self, cr, uid, action_slot, model, res_id=False, context=None):
576+ """Retrieves the list of actions bound to the given model's action slot.
577+ See the class description for more details about the various action
578+ slots: :class:`~.ir_values`.
579+
580+ :param string action_slot: the action slot to which the actions should be
581+ bound to - one of ``client_action_multi``,
582+ ``client_print_multi``, ``client_action_relate``,
583+ ``tree_but_open``.
584+ :param string model: model name
585+ :param int res_id: optional record id - will bind the action only to a
586+ specific record of the model, not all records.
587+ :return: list of action tuples of the form ``(id, name, action_def)``,
588+ where ``id`` is the ID of the default entry, ``name`` is the
589+ action label, and ``action_def`` is a dict containing the
590+ action definition as obtained by calling
591+ :meth:`~openerp.osv.osv.osv.read` on the action record.
592+ """
593+ assert action_slot in ACTION_SLOTS, 'Illegal action slot value: %s' % action_slot
594+ # use a direct SQL query for performance reasons,
595+ # this is called very often
596+ query = """SELECT v.id, v.name, v.value FROM ir_values v
597+ WHERE v.key = %s AND v.key2 = %s
598+ AND v.model = %s
599+ AND (v.res_id = %s
600+ OR v.res_id IS NULL
601+ OR v.res_id = 0)
602+ ORDER BY v.id"""
603+ cr.execute(query, ('action', action_slot, model, res_id or None))
604+ results = {}
605+ for action in cr.dictfetchall():
606+ action_model,id = action['value'].split(',')
607+ fields = [
608+ field
609+ for field in self.pool.get(action_model)._all_columns
610+ if field not in EXCLUDED_FIELDS]
611+ # FIXME: needs cleanup
612+ try:
613+ action_def = self.pool.get(action_model).read(cr, uid, int(id), fields, context)
614+ if action_def:
615+ if action_model in ('ir.actions.report.xml','ir.actions.act_window',
616+ 'ir.actions.wizard'):
617+ groups = action_def.get('groups_id')
618+ if groups:
619+ cr.execute('SELECT 1 FROM res_groups_users_rel WHERE gid IN %s AND uid=%s',
620+ (tuple(groups), uid))
621+ if not cr.fetchone():
622+ if action['name'] == 'Menuitem':
623+ raise osv.except_osv('Error !',
624+ 'You do not have the permission to perform this operation !!!')
625+ continue
626+ # keep only the first action registered for each action name
627+ results[action['name']] = (action['id'], action['name'], action_def)
628+ except except_orm, e:
629+ continue
630+ return results.values()
631+
632+ def _map_legacy_model_list(self, model_list, map_fn, merge_results=False):
633+ """Apply map_fn to the various models passed, according to
634+ legacy way to specify models/records.
635+ """
636+ assert isinstance(model_list, (list, tuple)), \
637+ "model_list should be in the form [model,..] or [(model,res_id), ..]"
638+ results = []
639+ for model in model_list:
640+ res_id = False
641 if isinstance(model, (list, tuple)):
642- model,res_id = model
643+ model, res_id = model
644+ result = map_fn(model, res_id)
645+ # some of the functions return one result at a time (tuple or id)
646+ # and some return a list of many of them - care for both
647+ if merge_results:
648+ results.extend(result)
649 else:
650- res_id = False
651- if replace:
652- search_criteria = [
653- ('key', '=', key),
654- ('key2', '=', key2),
655- ('model', '=', model),
656- ('res_id', '=', res_id),
657- ('user_id', '=', preserve_user and uid)
658- ]
659- if key in ('meta', 'default'):
660- search_criteria.append(('name', '=', name))
661- else:
662- search_criteria.append(('value', '=', value))
663-
664- self.unlink(cr, uid, self.search(cr, uid, search_criteria))
665- vals = {
666- 'name': name,
667- 'value': value,
668- 'model': model,
669- 'object': isobject,
670- 'key': key,
671- 'key2': key2 and key2[:200],
672- 'meta': meta,
673- 'user_id': preserve_user and uid,
674- }
675- if company:
676- cid = self.pool.get('res.users').browse(cr, uid, uid, context={}).company_id.id
677- vals['company_id']=cid
678- if res_id:
679- vals['res_id']= res_id
680- ids_res.append(self.create(cr, uid, vals))
681- return ids_res
682+ results.append(result)
683+ return results
684+
685+ # Backards-compatibility adapter layer to retrofit into split API
686+ def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
687+ """Deprecated legacy method to set default values and bind actions to models' action slots.
688+ Now dispatches to the newer API methods according to the value of ``key``: :meth:`~.set_default`
689+ (``key=='default'``) or :meth:`~.set_action` (``key == 'action'``).
690+
691+ :deprecated: As of v6.1, ``set_default()`` or ``set_action()`` should be used directly.
692+ """
693+ assert key in ['default', 'action'], "ir.values entry keys must be in ['default','action']"
694+ if key == 'default':
695+ def do_set(model,res_id):
696+ return self.set_default(cr, uid, model, field_name=name, value=value,
697+ for_all_users=(not preserve_user), company_id=company,
698+ condition=key2)
699+ elif key == 'action':
700+ def do_set(model,res_id):
701+ return self.set_action(cr, uid, name, action_slot=key2, model=model, action=value, res_id=res_id)
702+ return self._map_legacy_model_list(models, do_set)
703
704 def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
705- if context is None:
706- context = {}
707- result = []
708- assert isinstance(models, (list, tuple)), models
709-
710- for m in models:
711- if isinstance(m, (list, tuple)):
712- m, res_id = m
713- else:
714- res_id = False
715-
716- where = ['key=%s','model=%s']
717- params = [key, str(m)]
718- if key2:
719- where.append('key2=%s')
720- params.append(key2[:200])
721- elif key2_req and not meta:
722- where.append('key2 is null')
723- if res_id_req and (models[-1][0] == m):
724- if res_id:
725- where.append('res_id=%s')
726- params.append(res_id)
727- else:
728- where.append('(res_id is NULL)')
729- elif res_id:
730- if (models[-1][0]==m):
731- where.append('(res_id=%s or (res_id is null))')
732- params.append(res_id)
733- else:
734- where.append('res_id=%s')
735- params.append(res_id)
736- order = 'id'
737- if key == 'default':
738- # Make sure we get first the values for specific users, then
739- # the global values. The map/filter below will retain the first
740- # value for any given name. The 'order by' will put the null
741- # values last; this may be postgres specific (it is the
742- # behavior in postgres at least since 8.2).
743- order = 'user_id'
744- where.append('(user_id=%s or (user_id IS NULL)) order by '+ order)
745- params.append(uid)
746- clause = ' and '.join(where)
747- cr.execute('select id,name,value,object,meta, key from ir_values where ' + clause, params)
748- result = cr.fetchall()
749- if result:
750- break
751-
752- if not result:
753- return []
754-
755- def _result_get(x, keys):
756- if x[1] in keys:
757- return False
758- keys.append(x[1])
759- if x[3]:
760- model,id = x[2].split(',')
761- # FIXME: It might be a good idea to opt-in that kind of stuff
762- # FIXME: instead of arbitrarily removing random fields
763- fields = [
764- field
765- for field in self.pool.get(model).fields_get_keys(cr, uid)
766- if field not in EXCLUDED_FIELDS]
767-
768- try:
769- datas = self.pool.get(model).read(cr, uid, [int(id)], fields, context)
770- except except_orm, e:
771- return False
772- datas = datas and datas[0]
773- if not datas:
774- return False
775- else:
776- datas = pickle.loads(x[2].encode('utf-8'))
777- if meta:
778- return (x[0], x[1], datas, pickle.loads(x[4]))
779- return (x[0], x[1], datas)
780- keys = []
781- res = filter(None, map(lambda x: _result_get(x, keys), result))
782- res2 = res[:]
783- for r in res:
784- if isinstance(r[2], dict) and r[2].get('type') in ('ir.actions.report.xml','ir.actions.act_window','ir.actions.wizard'):
785- groups = r[2].get('groups_id')
786- if groups:
787- cr.execute('SELECT COUNT(1) FROM res_groups_users_rel WHERE gid IN %s AND uid=%s',(tuple(groups), uid))
788- cnt = cr.fetchone()[0]
789- if not cnt:
790- res2.remove(r)
791- if r[1] == 'Menuitem' and not res2:
792- raise osv.except_osv('Error !','You do not have the permission to perform this operation !!!')
793- return res2
794-ir_values()
795+ """Deprecated legacy method to get the list of default values or actions bound to models' action slots.
796+ Now dispatches to the newer API methods according to the value of ``key``: :meth:`~.get_defaults`
797+ (``key=='default'``) or :meth:`~.get_actions` (``key == 'action'``)
798+
799+ :deprecated: As of v6.1, ``get_defaults()`` or ``get_actions()`` should be used directly.
800+
801+ """
802+ assert key in ['default', 'action'], "ir.values entry keys must be in ['default','action']"
803+ if key == 'default':
804+ def do_get(model,res_id):
805+ return self.get_defaults(cr, uid, model, condition=key2)
806+ elif key == 'action':
807+ def do_get(model,res_id):
808+ return self.get_actions(cr, uid, action_slot=key2, model=model, res_id=res_id, context=context)
809+ return self._map_legacy_model_list(models, do_get, merge_results=True)
810
811=== modified file 'openerp/addons/base/security/base_security.xml'
812--- openerp/addons/base/security/base_security.xml 2011-09-19 11:47:17 +0000
813+++ openerp/addons/base/security/base_security.xml 2011-10-03 15:51:44 +0000
814@@ -69,6 +69,13 @@
815 <field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
816 </record>
817
818+ <record model="ir.rule" id="ir_values_default_rule">
819+ <field name="name">Defaults: alter personal values only</field>
820+ <field name="model_id" ref="model_ir_values"/>
821+ <field name="domain_force">[('key','=','default'),('user_id','=',user.id)]</field>
822+ <field name="perm_read" eval="False"/>
823+ </record>
824+
825 </data>
826 </openerp>
827
828
829=== modified file 'openerp/addons/base/security/ir.model.access.csv'
830--- openerp/addons/base/security/ir.model.access.csv 2011-09-19 21:01:21 +0000
831+++ openerp/addons/base/security/ir.model.access.csv 2011-10-03 15:51:44 +0000
832@@ -38,8 +38,7 @@
833 "access_ir_ui_view_custom_group_user","ir_ui_view_custom_group_user","model_ir_ui_view_custom",,1,0,0,0
834 "access_ir_ui_view_custom_group_system","ir_ui_view_custom_group_system","model_ir_ui_view_custom","group_system",1,1,1,1
835 "access_ir_ui_view_sc_group_user","ir_ui_view_sc group_user","model_ir_ui_view_sc",,1,1,1,1
836-"access_ir_values_group_erp_manager","ir_values group_erp_manager","model_ir_values","group_erp_manager",1,1,1,1
837-"access_ir_values_group_all","ir_values group_all","model_ir_values",,1,0,1,0
838+"access_ir_values_group_all","ir_values group_all","model_ir_values",,1,1,1,1
839 "access_res_company_group_erp_manager","res_company group_erp_manager","model_res_company","group_erp_manager",1,1,1,1
840 "access_res_company_group_user","res_company group_user","model_res_company",,1,0,0,0
841 "access_res_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0
842
843=== modified file 'openerp/addons/base/test/test_ir_values.yml'
844--- openerp/addons/base/test/test_ir_values.yml 2011-05-23 12:09:40 +0000
845+++ openerp/addons/base/test/test_ir_values.yml 2011-10-03 15:51:44 +0000
846@@ -2,24 +2,71 @@
847 Create some default value for some (non-existing) model, for all users.
848 -
849 !python {model: ir.values }: |
850- self.set(cr, uid, 'default', False, 'my_test_ir_value',['unexisting_model'], 'global value')
851+ self.set(cr, uid, 'default', False, 'my_test_field',['unexisting_model'], 'global value')
852 -
853 Retrieve it.
854 -
855 !python {model: ir.values }: |
856 # d is a list of triple (id, name, value)
857 d = self.get(cr, uid, 'default', False, ['unexisting_model'])
858- assert d[0][1] == u'my_test_ir_value', "Can't retrieve the created default value."
859+ assert d[0][1] == 'my_test_field', "Can't retrieve the created default value."
860 assert d[0][2] == 'global value', "Can't retrieve the created default value."
861 -
862 Do it again but for a specific user.
863 -
864 !python {model: ir.values }: |
865- self.set(cr, uid, 'default', False, 'my_test_ir_value',['unexisting_model'], 'specific value', preserve_user=True)
866+ self.set(cr, uid, 'default', False, 'my_test_field',['unexisting_model'], 'specific value', preserve_user=True)
867 -
868 Retrieve it and check it is the one for the current user.
869 -
870 !python {model: ir.values }: |
871 d = self.get(cr, uid, 'default', False, ['unexisting_model'])
872- assert d[0][1] == u'my_test_ir_value', "Can't retrieve the created default value."
873+ assert len(d) == 1, "Only one default must be returned per field"
874+ assert d[0][1] == 'my_test_field', "Can't retrieve the created default value."
875 assert d[0][2] == 'specific value', "Can't retrieve the created default value."
876+-
877+ Create some action bindings for a non-existing model
878+-
879+ !python {model: ir.values }: |
880+ self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True)
881+ self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action 2', ['unexisting_model'], 'ir.actions.act_window,11', isobject=True)
882+ self.set(cr, uid, 'action', 'client_action_multi', 'Side Wizard', ['unexisting_model'], 'ir.actions.act_window,12', isobject=True)
883+ self.set(cr, uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,2', isobject=True)
884+ self.set(cr, uid, 'action', 'client_action_relate', 'Related Stuff', ['unexisting_model'], 'ir.actions.act_window,14', isobject=True)
885+-
886+ Replace one action binding to set a new name
887+-
888+ !python {model: ir.values }: |
889+ self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action New', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True)
890+-
891+ Retrieve the action bindings and check they're correct
892+-
893+ !python {model: ir.values }: |
894+ actions = self.get(cr, uid, 'action', 'tree_but_open', ['unexisting_model'])
895+ assert len(actions) == 2, "Mismatching number of bound actions"
896+ #first action
897+ assert len(actions[0]) == 3, "Malformed action definition"
898+ assert actions[0][1] == 'OnDblClick Action 2', 'Bound action does not match definition'
899+ assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 11, 'Bound action does not match definition'
900+ #second action - this ones comes last because it was re-created with a different name
901+ assert len(actions[1]) == 3, "Malformed action definition"
902+ assert actions[1][1] == 'OnDblClick Action New', 'Re-Registering an action should replace it'
903+ assert isinstance(actions[1][2], dict) and actions[1][2]['id'] == 10, 'Bound action does not match definition'
904+
905+ actions = self.get(cr, uid, 'action', 'client_action_multi', ['unexisting_model'])
906+ assert len(actions) == 1, "Mismatching number of bound actions"
907+ assert len(actions[0]) == 3, "Malformed action definition"
908+ assert actions[0][1] == 'Side Wizard', 'Bound action does not match definition'
909+ assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 12, 'Bound action does not match definition'
910+
911+ actions = self.get(cr, uid, 'action', 'client_print_multi', ['unexisting_model'])
912+ assert len(actions) == 1, "Mismatching number of bound actions"
913+ assert len(actions[0]) == 3, "Malformed action definition"
914+ assert actions[0][1] == 'Nice Report', 'Bound action does not match definition'
915+ assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 2, 'Bound action does not match definition'
916+
917+ actions = self.get(cr, uid, 'action', 'client_action_relate', ['unexisting_model'])
918+ assert len(actions) == 1, "Mismatching number of bound actions"
919+ assert len(actions[0]) == 3, "Malformed action definition"
920+ assert actions[0][1] == 'Related Stuff', 'Bound action does not match definition'
921+ assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 14, 'Bound action does not match definition'