[FIX]base, ir_actions: ir_values with client_multi_actions and a server action with...
[odoo/odoo.git] / openerp / addons / base / ir / ir_actions.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2011 OpenERP S.A. <http://www.openerp.com>
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from functools import partial
23 import logging
24 import operator
25 import os
26 import time
27
28 import openerp
29 from openerp import SUPERUSER_ID
30 from openerp import tools
31 from openerp import workflow
32 from openerp.osv import fields, osv
33 from openerp.osv.orm import browse_record
34 import openerp.report.interface
35 from openerp.report.report_sxw import report_sxw, report_rml
36 from openerp.tools.safe_eval import safe_eval as eval
37 from openerp.tools.translate import _
38 import openerp.workflow
39
40 _logger = logging.getLogger(__name__)
41
42 class actions(osv.osv):
43     _name = 'ir.actions.actions'
44     _table = 'ir_actions'
45     _order = 'name'
46     _columns = {
47         'name': fields.char('Name', size=64, required=True),
48         'type': fields.char('Action Type', required=True, size=32),
49         'usage': fields.char('Action Usage', size=32),
50         'help': fields.text('Action description',
51             help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
52             translate=True),
53     }
54     _defaults = {
55         'usage': lambda *a: False,
56     }
57 actions()
58
59
60 class report_xml(osv.osv):
61
62     def _report_content(self, cursor, user, ids, name, arg, context=None):
63         res = {}
64         for report in self.browse(cursor, user, ids, context=context):
65             data = report[name + '_data']
66             if not data and report[name[:-8]]:
67                 fp = None
68                 try:
69                     fp = tools.file_open(report[name[:-8]], mode='rb')
70                     data = fp.read()
71                 except:
72                     data = False
73                 finally:
74                     if fp:
75                         fp.close()
76             res[report.id] = data
77         return res
78
79     def _report_content_inv(self, cursor, user, id, name, value, arg, context=None):
80         self.write(cursor, user, id, {name+'_data': value}, context=context)
81
82     def _report_sxw(self, cursor, user, ids, name, arg, context=None):
83         res = {}
84         for report in self.browse(cursor, user, ids, context=context):
85             if report.report_rml:
86                 res[report.id] = report.report_rml.replace('.rml', '.sxw')
87             else:
88                 res[report.id] = False
89         return res
90
91     def _lookup_report(self, cr, name):
92         """
93         Look up a report definition.
94         """
95         opj = os.path.join
96
97         # First lookup in the deprecated place, because if the report definition
98         # has not been updated, it is more likely the correct definition is there.
99         # Only reports with custom parser sepcified in Python are still there.
100         if 'report.' + name in openerp.report.interface.report_int._reports:
101             new_report = openerp.report.interface.report_int._reports['report.' + name]
102         else:
103             cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
104             r = cr.dictfetchone()
105             if r:
106                 if r['report_rml'] or r['report_rml_content_data']:
107                     if r['parser']:
108                         kwargs = { 'parser': operator.attrgetter(r['parser'])(openerp.addons) }
109                     else:
110                         kwargs = {}
111                     new_report = report_sxw('report.'+r['report_name'], r['model'],
112                             opj('addons',r['report_rml'] or '/'), header=r['header'], register=False, **kwargs)
113                 elif r['report_xsl'] and r['report_xml']:
114                     new_report = report_rml('report.'+r['report_name'], r['model'],
115                             opj('addons',r['report_xml']),
116                             r['report_xsl'] and opj('addons',r['report_xsl']), register=False)
117                 else:
118                     raise Exception, "Unhandled report type: %s" % r
119             else:
120                 raise Exception, "Required report does not exist: %s" % r
121
122         return new_report
123
124     def render_report(self, cr, uid, res_ids, name, data, context=None):
125         """
126         Look up a report definition and render the report for the provided IDs.
127         """
128         new_report = self._lookup_report(cr, name)
129         return new_report.create(cr, uid, res_ids, data, context)
130
131     _name = 'ir.actions.report.xml'
132     _inherit = 'ir.actions.actions'
133     _table = 'ir_act_report_xml'
134     _sequence = 'ir_actions_id_seq'
135     _order = 'name'
136     _columns = {
137         'name': fields.char('Name', size=64, required=True, translate=True),
138         'model': fields.char('Object', size=64, required=True),
139         'type': fields.char('Action Type', size=32, required=True),
140         'report_name': fields.char('Service Name', size=64, required=True),
141         'usage': fields.char('Action Usage', size=32),
142         'report_type': fields.char('Report Type', size=32, required=True, help="Report Type, e.g. pdf, html, raw, sxw, odt, html2html, mako2html, ..."),
143         'groups_id': fields.many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', 'Groups'),
144         'multi': fields.boolean('On Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view."),
145         'attachment': fields.char('Save as Attachment Prefix', size=128, help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.'),
146         'attachment_use': fields.boolean('Reload from Attachment', help='If you check this, then the second time the user prints with same attachment name, it returns the previous report.'),
147         'auto': fields.boolean('Custom Python Parser'),
148
149         'header': fields.boolean('Add RML Header', help="Add or not the corporate RML header"),
150
151         'report_xsl': fields.char('XSL Path', size=256),
152         'report_xml': fields.char('XML Path', size=256, help=''),
153
154         # Pending deprecation... to be replaced by report_file as this object will become the default report object (not so specific to RML anymore)
155         'report_rml': fields.char('Main Report File Path', size=256, help="The path to the main report file (depending on Report Type) or NULL if the content is in another data field"),
156         # temporary related field as report_rml is pending deprecation - this field will replace report_rml after v6.0
157         'report_file': fields.related('report_rml', type="char", size=256, required=False, readonly=False, string='Report File', help="The path to the main report file (depending on Report Type) or NULL if the content is in another field", store=True),
158
159         'report_sxw': fields.function(_report_sxw, type='char', string='SXW Path'),
160         'report_sxw_content_data': fields.binary('SXW Content'),
161         'report_rml_content_data': fields.binary('RML Content'),
162         'report_sxw_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='SXW Content',),
163         'report_rml_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='RML Content'),
164
165         'parser': fields.char('Parser Class'),
166     }
167     _defaults = {
168         'type': 'ir.actions.report.xml',
169         'multi': False,
170         'auto': True,
171         'header': True,
172         'report_sxw_content': False,
173         'report_type': 'pdf',
174         'attachment': False,
175     }
176
177 report_xml()
178
179 class act_window(osv.osv):
180     _name = 'ir.actions.act_window'
181     _table = 'ir_act_window'
182     _inherit = 'ir.actions.actions'
183     _sequence = 'ir_actions_id_seq'
184     _order = 'name'
185
186     def _check_model(self, cr, uid, ids, context=None):
187         for action in self.browse(cr, uid, ids, context):
188             if action.res_model not in self.pool:
189                 return False
190             if action.src_model and action.src_model not in self.pool:
191                 return False
192         return True
193
194     def _invalid_model_msg(self, cr, uid, ids, context=None):
195         return _('Invalid model name in the action definition.')
196
197     _constraints = [
198         (_check_model, _invalid_model_msg, ['res_model','src_model'])
199     ]
200
201     def _views_get_fnc(self, cr, uid, ids, name, arg, context=None):
202         """Returns an ordered list of the specific view modes that should be
203            enabled when displaying the result of this action, along with the
204            ID of the specific view to use for each mode, if any were required.
205
206            This function hides the logic of determining the precedence between
207            the view_modes string, the view_ids o2m, and the view_id m2o that can
208            be set on the action.
209
210            :rtype: dict in the form { action_id: list of pairs (tuples) }
211            :return: { action_id: [(view_id, view_mode), ...], ... }, where view_mode
212                     is one of the possible values for ir.ui.view.type and view_id
213                     is the ID of a specific view to use for this mode, or False for
214                     the default one.
215         """
216         res = {}
217         for act in self.browse(cr, uid, ids):
218             res[act.id] = [(view.view_id.id, view.view_mode) for view in act.view_ids]
219             view_ids_modes = [view.view_mode for view in act.view_ids]
220             modes = act.view_mode.split(',')
221             missing_modes = [mode for mode in modes if mode not in view_ids_modes]
222             if missing_modes:
223                 if act.view_id and act.view_id.type in missing_modes:
224                     # reorder missing modes to put view_id first if present
225                     missing_modes.remove(act.view_id.type)
226                     res[act.id].append((act.view_id.id, act.view_id.type))
227                 res[act.id].extend([(False, mode) for mode in missing_modes])
228         return res
229
230     def _search_view(self, cr, uid, ids, name, arg, context=None):
231         res = {}
232         for act in self.browse(cr, uid, ids, context=context):
233             field_get = self.pool[act.res_model].fields_view_get(cr, uid,
234                 act.search_view_id and act.search_view_id.id or False,
235                 'search', context=context)
236             res[act.id] = str(field_get)
237         return res
238
239     _columns = {
240         'name': fields.char('Action Name', size=64, translate=True),
241         'type': fields.char('Action Type', size=32, required=True),
242         'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='cascade'),
243         'domain': fields.char('Domain Value',
244             help="Optional domain filtering of the destination data, as a Python expression"),
245         'context': fields.char('Context Value', required=True,
246             help="Context dictionary as Python expression, empty by default (Default: {})"),
247         'res_id': fields.integer('Record ID', help="Database ID of record to open in form view, when ``view_mode`` is set to 'form' only"),
248         'res_model': fields.char('Destination Model', size=64, required=True,
249             help="Model name of the object to open in the view window"),
250         'src_model': fields.char('Source Model', size=64,
251             help="Optional model name of the objects on which this action should be visible"),
252         'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline Edit'),('inlineview','Inline View')], 'Target Window'),
253         'view_mode': fields.char('View Mode', size=250, required=True,
254             help="Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)"),
255         'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
256             help="View type: Tree type to use for the tree view, set to 'tree' for a hierarchical tree view, or 'form' for a regular list view"),
257         'usage': fields.char('Action Usage', size=32,
258             help="Used to filter menu and home actions from the user form."),
259         'view_ids': fields.one2many('ir.actions.act_window.view', 'act_window_id', 'Views'),
260         'views': fields.function(_views_get_fnc, type='binary', string='Views',
261                help="This function field computes the ordered list of views that should be enabled " \
262                     "when displaying the result of an action, federating view mode, views and " \
263                     "reference view. The result is returned as an ordered list of pairs (view_id,view_mode)."),
264         'limit': fields.integer('Limit', help='Default limit for the list view'),
265         'auto_refresh': fields.integer('Auto-Refresh',
266             help='Add an auto-refresh on the view'),
267         'groups_id': fields.many2many('res.groups', 'ir_act_window_group_rel',
268             'act_id', 'gid', 'Groups'),
269         'search_view_id': fields.many2one('ir.ui.view', 'Search View Ref.'),
270         'filter': fields.boolean('Filter'),
271         'auto_search':fields.boolean('Auto Search'),
272         'search_view' : fields.function(_search_view, type='text', string='Search View'),
273         'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view"),
274     }
275
276     _defaults = {
277         'type': 'ir.actions.act_window',
278         'view_type': 'form',
279         'view_mode': 'tree,form',
280         'context': '{}',
281         'limit': 80,
282         'target': 'current',
283         'auto_refresh': 0,
284         'auto_search':True,
285         'multi': False,
286     }
287
288     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
289         """ call the method get_empty_list_help of the model and set the window action help message
290         """
291         ids_int = isinstance(ids, (int, long))
292         if ids_int:
293             ids = [ids]
294         results = super(act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load)
295
296         if not fields or 'help' in fields:
297             context = dict(context or {})
298             eval_dict = {
299                 'active_model': context.get('active_model'),
300                 'active_id': context.get('active_id'),
301                 'active_ids': context.get('active_ids'),
302                 'uid': uid,
303             }
304             for res in results:
305                 model = res.get('res_model')
306                 if model and self.pool.get(model):
307                     try:
308                         with tools.mute_logger("openerp.tools.safe_eval"):
309                             eval_context = eval(res['context'] or "{}", eval_dict) or {}
310                     except Exception:
311                         continue
312                     custom_context = dict(context, **eval_context)
313                     res['help'] = self.pool.get(model).get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context)
314         if ids_int:
315             return results[0]
316         return results
317
318     def for_xml_id(self, cr, uid, module, xml_id, context=None):
319         """ Returns the act_window object created for the provided xml_id
320
321         :param module: the module the act_window originates in
322         :param xml_id: the namespace-less id of the action (the @id
323                        attribute from the XML file)
324         :return: A read() view of the ir.actions.act_window
325         """
326         dataobj = self.pool.get('ir.model.data')
327         data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
328         res_id = dataobj.browse(cr, uid, data_id, context).res_id
329         return self.read(cr, uid, res_id, [], context)
330
331 act_window()
332
333 VIEW_TYPES = [
334     ('tree', 'Tree'),
335     ('form', 'Form'),
336     ('graph', 'Graph'),
337     ('calendar', 'Calendar'),
338     ('gantt', 'Gantt'),
339     ('kanban', 'Kanban')]
340 class act_window_view(osv.osv):
341     _name = 'ir.actions.act_window.view'
342     _table = 'ir_act_window_view'
343     _rec_name = 'view_id'
344     _order = 'sequence'
345     _columns = {
346         'sequence': fields.integer('Sequence'),
347         'view_id': fields.many2one('ir.ui.view', 'View'),
348         'view_mode': fields.selection(VIEW_TYPES, string='View Type', required=True),
349         'act_window_id': fields.many2one('ir.actions.act_window', 'Action', ondelete='cascade'),
350         'multi': fields.boolean('On Multiple Doc.',
351             help="If set to true, the action will not be displayed on the right toolbar of a form view."),
352     }
353     _defaults = {
354         'multi': False,
355     }
356     def _auto_init(self, cr, context=None):
357         super(act_window_view, self)._auto_init(cr, context)
358         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
359         if not cr.fetchone():
360             cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
361 act_window_view()
362
363 class act_wizard(osv.osv):
364     _name = 'ir.actions.wizard'
365     _inherit = 'ir.actions.actions'
366     _table = 'ir_act_wizard'
367     _sequence = 'ir_actions_id_seq'
368     _order = 'name'
369     _columns = {
370         'name': fields.char('Wizard Info', size=64, required=True, translate=True),
371         'type': fields.char('Action Type', size=32, required=True),
372         'wiz_name': fields.char('Wizard Name', size=64, required=True),
373         'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the wizard will not be displayed on the right toolbar of a form view."),
374         'groups_id': fields.many2many('res.groups', 'res_groups_wizard_rel', 'uid', 'gid', 'Groups'),
375         'model': fields.char('Object', size=64),
376     }
377     _defaults = {
378         'type': 'ir.actions.wizard',
379         'multi': False,
380     }
381 act_wizard()
382
383 class act_url(osv.osv):
384     _name = 'ir.actions.act_url'
385     _table = 'ir_act_url'
386     _inherit = 'ir.actions.actions'
387     _sequence = 'ir_actions_id_seq'
388     _order = 'name'
389     _columns = {
390         'name': fields.char('Action Name', size=64, translate=True),
391         'type': fields.char('Action Type', size=32, required=True),
392         'url': fields.text('Action URL',required=True),
393         'target': fields.selection((
394             ('new', 'New Window'),
395             ('self', 'This Window')),
396             'Action Target', required=True
397         )
398     }
399     _defaults = {
400         'type': 'ir.actions.act_url',
401         'target': 'new'
402     }
403 act_url()
404
405 def model_get(self, cr, uid, context=None):
406     wkf_pool = self.pool.get('workflow')
407     ids = wkf_pool.search(cr, uid, [])
408     osvs = wkf_pool.read(cr, uid, ids, ['osv'])
409
410     res = []
411     mpool = self.pool.get('ir.model')
412     for osv in osvs:
413         model = osv.get('osv')
414         id = mpool.search(cr, uid, [('model','=',model)])
415         name = mpool.read(cr, uid, id)[0]['name']
416         res.append((model, name))
417
418     return res
419
420 class ir_model_fields(osv.osv):
421     _inherit = 'ir.model.fields'
422     _rec_name = 'field_description'
423     _columns = {
424         'complete_name': fields.char('Complete Name', size=64, select=1),
425     }
426 ir_model_fields()
427
428 class server_object_lines(osv.osv):
429     _name = 'ir.server.object.lines'
430     _sequence = 'ir_actions_id_seq'
431     _columns = {
432         'server_id': fields.many2one('ir.actions.server', 'Related Server Action'),
433         'col1': fields.many2one('ir.model.fields', 'Field', required=True),
434         'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
435                                                           "When Formula type is selected, this field may be a Python expression "
436                                                           " that can use the same values as for the condition field on the server action.\n"
437                                                           "If Value type is selected, the value will be used directly without evaluation."),
438         'type': fields.selection([
439             ('value', 'Value'),
440             ('equation', 'Python expression')
441         ], 'Evaluation Type', required=True, change_default=True),
442     }
443     _defaults = {
444         'type': 'value',
445     }
446
447
448 ##
449 # Actions that are run on the server side
450 #
451 class actions_server(osv.osv):
452     """ Server actions model. Server action work on a base model and offer various
453     type of actions that can be executed automatically, for example using base
454     action rules, of manually, by adding the action in the 'More' contextual
455     menu.
456
457     Since OpenERP 8.0 a button 'Create Menu Action' button is available on the
458     action form view. It creates an entry in the More menu of the base model.
459     This allows to create server actions and run them in mass mode easily through
460     the interface.
461
462     The available actions are :
463
464     - 'Execute Python Code': a block of python code that will be executed
465     - 'Trigger a Workflow Signal': send a signal to a workflow
466     - 'Run a Client Action': choose a client action to launch
467     - 'Create or Copy a new Record': create a new record with new values, or
468       copy an existing record in your database
469     - 'Write on a Record': update the values of a record
470     - 'Execute several actions': define an action that triggers several other
471       server actions
472     """
473     _name = 'ir.actions.server'
474     _table = 'ir_act_server'
475     _inherit = 'ir.actions.actions'
476     _sequence = 'ir_actions_id_seq'
477     _order = 'sequence,name'
478
479     def _select_objects(self, cr, uid, context=None):
480         model_pool = self.pool.get('ir.model')
481         ids = model_pool.search(cr, uid, [('name', 'not ilike', '.')])
482         res = model_pool.read(cr, uid, ids, ['model', 'name'])
483         return [(r['model'], r['name']) for r in res] + [('', '')]
484
485     def _get_states(self, cr, uid, context=None):
486         """ Override me in order to add new states in the server action. Please
487         note that the added key length should not be higher than already-existing
488         ones. """
489         return [('code', 'Execute Python Code'),
490                 ('trigger', 'Trigger a Workflow Signal'),
491                 ('client_action', 'Run a Client Action'),
492                 ('object_create', 'Create or Copy a new Record'),
493                 ('object_write', 'Write on a Record'),
494                 ('multi', 'Execute several actions')]
495
496     def _get_states_wrapper(self, cr, uid, context=None):
497         return self._get_states(cr, uid, context)
498
499     _columns = {
500         'name': fields.char('Action Name', required=True, size=64, translate=True),
501         'condition': fields.char('Condition',
502                                  help="Condition verified before executing the server action. If it "
503                                  "is not verified, the action will not be executed. The condition is "
504                                  "a Python expression, like 'object.list_price > 5000'. A void "
505                                  "condition is considered as always True. Help about python expression "
506                                  "is given in the help tab."),
507         'state': fields.selection(_get_states_wrapper, 'Action To Do', required=True,
508                                   help="Type of server action. The following values are available:\n"
509                                   "- 'Execute Python Code': a block of python code that will be executed\n"
510                                   "- 'Trigger a Workflow Signal': send a signal to a workflow\n"
511                                   "- 'Run a Client Action': choose a client action to launch\n"
512                                   "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
513                                   "- 'Write on a Record': update the values of a record\n"
514                                   "- 'Execute several actions': define an action that triggers several other server actions\n"
515                                   "- 'Send Email': automatically send an email (available in email_template)"),
516         'usage': fields.char('Action Usage', size=32),
517         'type': fields.char('Action Type', size=32, required=True),
518         # Generic
519         'sequence': fields.integer('Sequence',
520                                    help="When dealing with multiple actions, the execution order is "
521                                    "based on the sequence. Low number means high priority."),
522         'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade',
523                                     help="Base model on which the server action runs."),
524         'menu_ir_values_id': fields.many2one('ir.values', 'More Menu entry', readonly=True,
525                                              help='More menu entry.'),
526         # Client Action
527         'action_id': fields.many2one('ir.actions.actions', 'Client Action',
528                                      help="Select the client action that has to be executed."),
529         # Python code
530         'code': fields.text('Python Code',
531                             help="Write Python code that the action will execute. Some variables are "
532                             "available for use; help about pyhon expression is given in the help tab."),
533         # Workflow signal
534         'use_relational_model': fields.selection([('base', 'Use the base model of the action'),
535                                                   ('relational', 'Use a relation field on the base model')],
536                                                  string='Target Model', required=True),
537         'wkf_transition_id': fields.many2one('workflow.transition', string='Signal to Trigger',
538                                              help="Select the workflow signal to trigger."),
539         'wkf_model_id': fields.many2one('ir.model', 'Target Model',
540                                         help="The model that will receive the workflow signal. Note that it should have a workflow associated with it."),
541         'wkf_model_name': fields.related('wkf_model_id', 'model', type='char', string='Target Model Name', store=True, readonly=True),
542         'wkf_field_id': fields.many2one('ir.model.fields', string='Relation Field',
543                                         oldname='trigger_obj_id',
544                                         help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)"),
545         # Multi
546         'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions',
547                                       'server_id', 'action_id',
548                                       string='Child Actions',
549                                       help='Child server actions that will be executed. Note that the last return returned action value will be used as global return value.'),
550         # Create/Copy/Write
551         'use_create': fields.selection([('new', 'Create a new record in the Base Model'),
552                                         ('new_other', 'Create a new record in another model'),
553                                         ('copy_current', 'Copy the current record'),
554                                         ('copy_other', 'Choose and copy a record in the database')],
555                                        string="Creation Policy", required=True,
556                                        help=""),
557         'crud_model_id': fields.many2one('ir.model', 'Target Model',
558                                          oldname='srcmodel_id',
559                                          help="Model for record creation / update. Set this field only to specify a different model than the base model."),
560         'crud_model_name': fields.related('crud_model_id', 'model', type='char',
561                                           string='Create/Write Target Model Name',
562                                           store=True, readonly=True),
563         'ref_object': fields.reference('Reference record', selection=_select_objects, size=128,
564                                        oldname='copy_object'),
565         'link_new_record': fields.boolean('Attach the new record',
566                                           help="Check this if you want to link the newly-created record "
567                                           "to the current record on which the server action runs."),
568         'link_field_id': fields.many2one('ir.model.fields', 'Link using field',
569                                          oldname='record_id',
570                                          help="Provide the field where the record id is stored after the operations."),
571         'use_write': fields.selection([('current', 'Update the current record'),
572                                        ('expression', 'Update a record linked to the current record using python'),
573                                        ('other', 'Choose and Update a record in the database')],
574                                       string='Update Policy', required=True,
575                                       help=""),
576         'write_expression': fields.char('Expression',
577                                         oldname='write_id',
578                                         help="Provide an expression that, applied on the current record, gives the field to update."),
579         'fields_lines': fields.one2many('ir.server.object.lines', 'server_id',
580                                         string='Value Mapping',
581                                         help=""),
582
583         # Fake fields used to implement the placeholder assistant
584         'model_object_field': fields.many2one('ir.model.fields', string="Field",
585                                               help="Select target field from the related document model.\n"
586                                                    "If it is a relationship field you will be able to select "
587                                                    "a target field at the destination of the relationship."),
588         'sub_object': fields.many2one('ir.model', 'Sub-model', readonly=True,
589                                       help="When a relationship field is selected as first field, "
590                                            "this field shows the document model the relationship goes to."),
591         'sub_model_object_field': fields.many2one('ir.model.fields', 'Sub-field',
592                                                   help="When a relationship field is selected as first field, "
593                                                        "this field lets you select the target field within the "
594                                                        "destination document model (sub-model)."),
595         'copyvalue': fields.char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field."),
596         # Fake fields used to implement the ID finding assistant
597         'id_object': fields.reference('Record', selection=_select_objects, size=128),
598         'id_value': fields.char('Record ID'),
599     }
600
601     _defaults = {
602         'state': 'code',
603         'condition': 'True',
604         'type': 'ir.actions.server',
605         'sequence': 5,
606         'code': """# You can use the following variables:
607 #  - self: ORM model of the record on which the action is triggered
608 #  - object: browse_record of the record on which the action is triggered if there is one, otherwise None
609 #  - pool: ORM model pool (i.e. self.pool)
610 #  - cr: database cursor
611 #  - uid: current user id
612 #  - context: current context
613 #  - time: Python time module
614 # If you plan to return an action, assign: action = {...}""",
615         'use_relational_model': 'base',
616         'use_create': 'new',
617         'use_write': 'current',
618     }
619
620     def _check_expression(self, cr, uid, expression, model_id, context):
621         """ Check python expression (condition, write_expression). Each step of
622         the path must be a valid many2one field, or an integer field for the last
623         step.
624
625         :param str expression: a python expression, beginning by 'obj' or 'object'
626         :param int model_id: the base model of the server action
627         :returns tuple: (is_valid, target_model_name, error_msg)
628         """
629         if not model_id:
630             return (False, None, 'Your expression cannot be validated because the Base Model is not set.')
631         # fetch current model
632         current_model_name = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
633         # transform expression into a path that should look like 'object.many2onefield.many2onefield'
634         path = expression.split('.')
635         initial = path.pop(0)
636         if initial not in ['obj', 'object']:
637             return (False, None, 'Your expression should begin with obj or object.\nAn expression builder is available in the help tab.')
638         # analyze path
639         while path:
640             step = path.pop(0)
641             column_info = self.pool[current_model_name]._all_columns.get(step)
642             if not column_info:
643                 return (False, None, 'Part of the expression (%s) is not recognized as a column in the model %s.' % (step, current_model_name))
644             column_type = column_info.column._type
645             if column_type not in ['many2one', 'int']:
646                 return (False, None, 'Part of the expression (%s) is not a valid column type (is %s, should be a many2one or an int)' % (step, column_type))
647             if column_type == 'int' and path:
648                 return (False, None, 'Part of the expression (%s) is an integer field that is only allowed at the end of an expression' % (step))
649             if column_type == 'many2one':
650                 current_model_name = column_info.column._obj
651         return (True, current_model_name, None)
652
653     def _check_write_expression(self, cr, uid, ids, context=None):
654         for record in self.browse(cr, uid, ids, context=context):
655             if record.write_expression and record.model_id:
656                 correct, model_name, message = self._check_expression(cr, uid, record.write_expression, record.model_id.id, context=context)
657                 if not correct:
658                     _logger.warning('Invalid expression: %s' % message)
659                     return False
660         return True
661
662     _constraints = [
663         (_check_write_expression,
664             'Incorrect Write Record Expression',
665             ['write_expression']),
666         (partial(osv.Model._check_m2m_recursion, field_name='child_ids'),
667             'Recursion found in child server actions',
668             ['child_ids']),
669     ]
670
671     def on_change_model_id(self, cr, uid, ids, model_id, wkf_model_id, crud_model_id, context=None):
672         """ When changing the action base model, reset workflow and crud config
673         to ease value coherence. """
674         values = {
675             'use_create': 'new',
676             'use_write': 'current',
677             'use_relational_model': 'base',
678             'wkf_model_id': model_id,
679             'wkf_field_id': False,
680             'crud_model_id': model_id,
681         }
682         return {'value': values}
683
684     def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
685         """ Update workflow type configuration
686
687          - update the workflow model (for base (model_id) /relational (field.relation))
688          - update wkf_transition_id to False if workflow model changes, to force
689            the user to choose a new one
690         """
691         values = {}
692         if use_relational_model == 'relational' and wkf_field_id:
693             field = self.pool['ir.model.fields'].browse(cr, uid, wkf_field_id, context=context)
694             new_wkf_model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', field.relation)], context=context)[0]
695             values['wkf_model_id'] = new_wkf_model_id
696         else:
697             values['wkf_model_id'] = model_id
698         return {'value': values}
699
700     def on_change_wkf_model_id(self, cr, uid, ids, wkf_model_id, context=None):
701         """ When changing the workflow model, update its stored name also """
702         wkf_model_name = False
703         if wkf_model_id:
704             wkf_model_name = self.pool.get('ir.model').browse(cr, uid, wkf_model_id, context).model
705         values = {'wkf_transition_id': False, 'wkf_model_name': wkf_model_name}
706         return {'value': values}
707
708     def on_change_crud_config(self, cr, uid, ids, state, use_create, use_write, ref_object, crud_model_id, model_id, context=None):
709         """ Wrapper on CRUD-type (create or write) on_change """
710         if state == 'object_create':
711             return self.on_change_create_config(cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=context)
712         elif state == 'object_write':
713             return self.on_change_write_config(cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=context)
714         else:
715             return {}
716
717     def on_change_create_config(self, cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=None):
718         """ When changing the object_create type configuration:
719
720          - `new` and `copy_current`: crud_model_id is the same as base model
721          - `new_other`: user choose crud_model_id
722          - `copy_other`: disassemble the reference object to have its model
723          - if the target model has changed, then reset the link field that is
724            probably not correct anymore
725         """
726         values = {}
727         if use_create == 'new':
728             values['crud_model_id'] = model_id
729         elif use_create == 'new_other':
730             pass
731         elif use_create == 'copy_current':
732             values['crud_model_id'] = model_id
733         elif use_create == 'copy_other' and ref_object:
734             ref_model, ref_id = ref_object.split(',')
735             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
736             values['crud_model_id'] = ref_model_id
737
738         if values.get('crud_model_id') != crud_model_id:
739             values['link_field_id'] = False
740         return {'value': values}
741
742     def on_change_write_config(self, cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=None):
743         """ When changing the object_write type configuration:
744
745          - `current`: crud_model_id is the same as base model
746          - `other`: disassemble the reference object to have its model
747          - `expression`: has its own on_change, nothing special here
748         """
749         values = {}
750         if use_write == 'current':
751             values['crud_model_id'] = model_id
752         elif use_write == 'other' and ref_object:
753             ref_model, ref_id = ref_object.split(',')
754             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
755             values['crud_model_id'] = ref_model_id
756         elif use_write == 'expression':
757             pass
758
759         if values.get('crud_model_id') != crud_model_id:
760             values['link_field_id'] = False
761         return {'value': values}
762
763     def on_change_write_expression(self, cr, uid, ids, write_expression, model_id, context=None):
764         """ Check the write_expression and update crud_model_id accordingly """
765         values = {}
766         valid, model_name, message = self._check_expression(cr, uid, write_expression, model_id, context=context)
767         if valid:
768             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', model_name)], context=context)[0]
769             values['crud_model_id'] = ref_model_id
770             return {'value': values}
771         if not message:
772             message = 'Invalid expression'
773         return {
774             'warning': {
775                 'title': 'Incorrect expression',
776                 'message': message,
777             }
778         }
779
780     def on_change_crud_model_id(self, cr, uid, ids, crud_model_id, context=None):
781         """ When changing the CRUD model, update its stored name also """
782         crud_model_name = False
783         if crud_model_id:
784             crud_model_name = self.pool.get('ir.model').browse(cr, uid, crud_model_id, context).model
785         values = {'link_field_id': False, 'crud_model_name': crud_model_name}
786         return {'value': values}
787
788     def _build_expression(self, field_name, sub_field_name):
789         """ Returns a placeholder expression for use in a template field,
790         based on the values provided in the placeholder assistant.
791
792         :param field_name: main field name
793         :param sub_field_name: sub field name (M2O)
794         :return: final placeholder expression
795         """
796         expression = ''
797         if field_name:
798             expression = "object." + field_name
799             if sub_field_name:
800                 expression += "." + sub_field_name
801         return expression
802
803     def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, context=None):
804         result = {
805             'sub_object': False,
806             'copyvalue': False,
807             'sub_model_object_field': False,
808         }
809         if model_object_field:
810             fields_obj = self.pool.get('ir.model.fields')
811             field_value = fields_obj.browse(cr, uid, model_object_field, context)
812             if field_value.ttype in ['many2one', 'one2many', 'many2many']:
813                 res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
814                 sub_field_value = False
815                 if sub_model_object_field:
816                     sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
817                 if res_ids:
818                     result.update({
819                         'sub_object': res_ids[0],
820                         'copyvalue': self._build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
821                         'sub_model_object_field': sub_model_object_field or False,
822                     })
823             else:
824                 result.update({
825                     'copyvalue': self._build_expression(field_value.name, False),
826                 })
827         return {'value': result}
828
829     def onchange_id_object(self, cr, uid, ids, id_object, context=None):
830         if id_object:
831             ref_model, ref_id = id_object.split(',')
832             return {'value': {'id_value': ref_id}}
833         return {'value': {'id_value': False}}
834
835     def create_action(self, cr, uid, ids, context=None):
836         """ Create a contextual action for each of the server actions. """
837         for action in self.browse(cr, uid, ids, context=context):
838             ir_values_id = self.pool.get('ir.values').create(cr, SUPERUSER_ID, {
839                 'name': _('Run %s') % action.name,
840                 'model': action.model_id.model,
841                 'key2': 'client_action_multi',
842                 'value': "ir.actions.server,%s" % action.id,
843             }, context)
844             action.write({
845                 'menu_ir_values_id': ir_values_id,
846             })
847
848         return True
849
850     def unlink_action(self, cr, uid, ids, context=None):
851         """ Remove the contextual actions created for the server actions. """
852         for action in self.browse(cr, uid, ids, context=context):
853             if action.menu_ir_values_id:
854                 try:
855                     self.pool.get('ir.values').unlink(cr, SUPERUSER_ID, action.menu_ir_values_id.id, context)
856                 except Exception:
857                     raise osv.except_osv(_('Warning'), _('Deletion of the action record failed.'))
858         return True
859
860     def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):
861         if not action.action_id:
862             raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
863         return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
864
865     def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None):
866         eval(action.code.strip(), eval_context, mode="exec", nocopy=True)  # nocopy allows to return 'action'
867         if 'action' in eval_context:
868             return eval_context['action']
869
870     def run_action_trigger(self, cr, uid, action, eval_context=None, context=None):
871         """ Trigger a workflow signal, depending on the use_relational_model:
872
873          - `base`: base_model_pool.signal_<TRIGGER_NAME>(cr, uid, context.get('active_id'))
874          - `relational`: find the related model and object, using the relational
875            field, then target_model_pool.signal_<TRIGGER_NAME>(cr, uid, target_id)
876         """
877         obj_pool = self.pool[action.model_id.model]
878         if action.use_relational_model == 'base':
879             target_id = context.get('active_id')
880             target_pool = obj_pool
881         else:
882             value = getattr(obj_pool.browse(cr, uid, context.get('active_id'), context=context), action.wkf_field_id.name)
883             if action.wkf_field_id.ttype == 'many2one':
884                 target_id = value.id
885             else:
886                 target_id = value
887             target_pool = self.pool[action.wkf_model_id.model]
888
889         trigger_name = action.wkf_transition_id.signal
890
891         workflow.trg_validate(uid, target_pool._name, target_id, trigger_name, cr)
892
893     def run_action_multi(self, cr, uid, action, eval_context=None, context=None):
894         res = []
895         for act in action.child_ids:
896             result = self.run(cr, uid, [act.id], context)
897             if result:
898                 res.append(result)
899         return res and res[0] or False
900
901     def run_action_object_write(self, cr, uid, action, eval_context=None, context=None):
902         """ Write server action.
903
904          - 1. evaluate the value mapping
905          - 2. depending on the write configuration:
906
907           - `current`: id = active_id
908           - `other`: id = from reference object
909           - `expression`: id = from expression evaluation
910         """
911         res = {}
912         for exp in action.fields_lines:
913             if exp.type == 'equation':
914                 expr = eval(exp.value, eval_context)
915             else:
916                 expr = exp.value
917             res[exp.col1.name] = expr
918
919         if action.use_write == 'current':
920             model = action.model_id.model
921             ref_id = context.get('active_id')
922         elif action.use_write == 'other':
923             model = action.crud_model_id.model
924             ref_id = action.ref_object.id
925         elif action.use_write == 'expression':
926             model = action.crud_model_id.model
927             ref = eval(action.write_expression, eval_context)
928             if isinstance(ref, browse_record):
929                 ref_id = getattr(ref, 'id')
930             else:
931                 ref_id = int(ref)
932
933         obj_pool = self.pool[model]
934         obj_pool.write(cr, uid, [ref_id], res, context=context)
935
936     def run_action_object_create(self, cr, uid, action, eval_context=None, context=None):
937         """ Create and Copy server action.
938
939          - 1. evaluate the value mapping
940          - 2. depending on the write configuration:
941
942           - `new`: new record in the base model
943           - `copy_current`: copy the current record (id = active_id) + gives custom values
944           - `new_other`: new record in target model
945           - `copy_other`: copy the current record (id from reference object)
946             + gives custom values
947         """
948         res = {}
949         for exp in action.fields_lines:
950             if exp.type == 'equation':
951                 expr = eval(exp.value, eval_context)
952             else:
953                 expr = exp.value
954             res[exp.col1.name] = expr
955
956         if action.use_create in ['new', 'copy_current']:
957             model = action.model_id.model
958         elif action.use_create in ['new_other', 'copy_other']:
959             model = action.crud_model_id.model
960
961         obj_pool = self.pool[model]
962         if action.use_create == 'copy_current':
963             ref_id = context.get('active_id')
964             res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
965         elif action.use_create == 'copy_other':
966             ref_id = action.ref_object.id
967             res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
968         else:
969             res_id = obj_pool.create(cr, uid, res, context=context)
970
971         if action.link_new_record and action.link_field_id:
972             self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
973
974     def run(self, cr, uid, ids, context=None):
975         """ Run the server action. For each server action, the condition is
976         checked. Note that A void (aka False) condition is considered as always
977         valid. If it is verified, the run_action_<STATE> method is called. This
978         allows easy inheritance of the server actions.
979
980         :param dict context: context should contain following keys
981
982                              - active_id: id of the current object (single mode)
983                              - active_model: current model that should equal the action's model
984
985                              The following keys are optional:
986
987                              - active_ids: ids of the current records (mass mode). If active_ids
988                                and active_id are present, active_ids is given precedence.
989
990         :return: an action_id to be executed, or False is finished correctly without
991                  return action
992         """
993         if context is None:
994             context = {}
995         res = False
996         user = self.pool.get('res.users').browse(cr, uid, uid)
997         active_ids = context.get('active_ids', [context.get('active_id', None)])
998         for action in self.browse(cr, uid, ids, context):
999             obj_pool = self.pool[action.model_id.model]
1000             # evaluation context for python strings to evaluate
1001             eval_context = {
1002                 'self': obj_pool,
1003                 'object': None,
1004                 'obj': None,
1005                 'pool': self.pool,
1006                 'time': time,
1007                 'cr': cr,
1008                 'uid': uid,
1009                 'user': user,
1010             }
1011             condition = action.condition
1012             if condition is False:
1013                 condition = True
1014             if hasattr(self, 'run_action_%s_multi' % action.state):
1015                 # set active_ids in context only needed if one active_id
1016                 run_context = dict(context, active_ids=active_ids)
1017                 eval_context["context"] = run_context                
1018                 expr = eval(str(condition), eval_context)
1019                 if not expr:
1020                     continue
1021                 # call the multi method
1022                 res = getattr(self, 'run_action_%s_multi' % action.state)(cr, uid, action, eval_context=eval_context, context=run_context)
1023             elif hasattr(self, 'run_action_%s' % action.state):
1024                 for active_id in active_ids:
1025                     if context.get('active_model') == action.model_id.model and active_id:
1026                         obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
1027                         eval_context['obj'] = obj
1028                         eval_context['object'] = obj
1029                     # run context dedicated to a particular active_id
1030                     run_context = dict(context, active_ids=[active_id], active_id=active_id)
1031                     eval_context["context"] = run_context
1032                     # evaluate the condition, with the specific case that a void (aka False) condition is considered as True
1033                     expr = eval(str(condition), eval_context)
1034                     if not expr:
1035                         continue
1036                     # call the single method related to the action: run_action_<STATE>
1037                     res = getattr(self, 'run_action_%s' % action.state)(cr, uid, action, eval_context=eval_context, context=run_context)
1038         return res
1039
1040
1041 class act_window_close(osv.osv):
1042     _name = 'ir.actions.act_window_close'
1043     _inherit = 'ir.actions.actions'
1044     _table = 'ir_actions'
1045     _defaults = {
1046         'type': 'ir.actions.act_window_close',
1047     }
1048 act_window_close()
1049
1050 # This model use to register action services.
1051 TODO_STATES = [('open', 'To Do'),
1052                ('done', 'Done')]
1053 TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
1054               ('automatic', 'Launch Automatically')]
1055 class ir_actions_todo(osv.osv):
1056     """
1057     Configuration Wizards
1058     """
1059     _name = 'ir.actions.todo'
1060     _description = "Configuration Wizards"
1061     _columns={
1062         'action_id': fields.many2one(
1063             'ir.actions.actions', 'Action', select=True, required=True),
1064         'sequence': fields.integer('Sequence'),
1065         'state': fields.selection(TODO_STATES, string='Status', required=True),
1066         'name': fields.char('Name', size=64),
1067         'type': fields.selection(TODO_TYPES, 'Type', required=True,
1068             help="""Manual: Launched manually.
1069 Automatic: Runs whenever the system is reconfigured.
1070 Launch Manually Once: after having been launched manually, it sets automatically to Done."""),
1071         'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
1072         'note': fields.text('Text', translate=True),
1073     }
1074     _defaults={
1075         'state': 'open',
1076         'sequence': 10,
1077         'type': 'manual',
1078     }
1079     _order="sequence,id"
1080
1081     def action_launch(self, cr, uid, ids, context=None):
1082         """ Launch Action of Wizard"""
1083         wizard_id = ids and ids[0] or False
1084         wizard = self.browse(cr, uid, wizard_id, context=context)
1085         if wizard.type in ('automatic', 'once'):
1086             wizard.write({'state': 'done'})
1087
1088         # Load action
1089         act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
1090
1091         res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context)
1092         if act_type['type'] != 'ir.actions.act_window':
1093             return res
1094         res.setdefault('context','{}')
1095         res['nodestroy'] = True
1096
1097         # Open a specific record when res_id is provided in the context
1098         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1099         ctx = eval(res['context'], {'user': user})
1100         if ctx.get('res_id'):
1101             res.update({'res_id': ctx.pop('res_id')})
1102
1103         # disable log for automatic wizards
1104         if wizard.type == 'automatic':
1105             ctx.update({'disable_log': True})
1106         res.update({'context': ctx})
1107
1108         return res
1109
1110     def action_open(self, cr, uid, ids, context=None):
1111         """ Sets configuration wizard in TODO state"""
1112         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
1113
1114     def progress(self, cr, uid, context=None):
1115         """ Returns a dict with 3 keys {todo, done, total}.
1116
1117         These keys all map to integers and provide the number of todos
1118         marked as open, the total number of todos and the number of
1119         todos not open (which is basically a shortcut to total-todo)
1120
1121         :rtype: dict
1122         """
1123         user_groups = set(map(
1124             lambda x: x.id,
1125             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
1126         def groups_match(todo):
1127             """ Checks if the todo's groups match those of the current user
1128             """
1129             return not todo.groups_id \
1130                    or bool(user_groups.intersection((
1131                         group.id for group in todo.groups_id)))
1132
1133         done = filter(
1134             groups_match,
1135             self.browse(cr, uid,
1136                 self.search(cr, uid, [('state', '!=', 'open')], context=context),
1137                         context=context))
1138
1139         total = filter(
1140             groups_match,
1141             self.browse(cr, uid,
1142                 self.search(cr, uid, [], context=context),
1143                         context=context))
1144
1145         return {
1146             'done': len(done),
1147             'total': len(total),
1148             'todo': len(total) - len(done)
1149         }
1150
1151 ir_actions_todo()
1152
1153 class act_client(osv.osv):
1154     _name = 'ir.actions.client'
1155     _inherit = 'ir.actions.actions'
1156     _table = 'ir_act_client'
1157     _sequence = 'ir_actions_id_seq'
1158     _order = 'name'
1159
1160     def _get_params(self, cr, uid, ids, field_name, arg, context):
1161         result = {}
1162         for record in self.browse(cr, uid, ids, context=context):
1163             result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
1164         return result
1165
1166     def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
1167         if isinstance(field_value, dict):
1168             self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
1169         else:
1170             self.write(cr, uid, id, {'params_store': field_value}, context=context)
1171
1172     _columns = {
1173         'name': fields.char('Action Name', required=True, size=64, translate=True),
1174         'tag': fields.char('Client action tag', size=64, required=True,
1175                            help="An arbitrary string, interpreted by the client"
1176                                 " according to its own needs and wishes. There "
1177                                 "is no central tag repository across clients."),
1178         'res_model': fields.char('Destination Model', size=64, 
1179             help="Optional model, mostly used for needactions."),
1180         'context': fields.char('Context Value', size=250, required=True,
1181             help="Context dictionary as Python expression, empty by default (Default: {})"),
1182         'params': fields.function(_get_params, fnct_inv=_set_params,
1183                                   type='binary', 
1184                                   string="Supplementary arguments",
1185                                   help="Arguments sent to the client along with"
1186                                        "the view tag"),
1187         'params_store': fields.binary("Params storage", readonly=True)
1188     }
1189     _defaults = {
1190         'type': 'ir.actions.client',
1191         'context': '{}',
1192
1193     }
1194 act_client()
1195
1196 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: