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