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