[MERGE] Forward-port of latest saas-4 fixes, up to 0452851
[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         'menu_ir_values_id': fields.many2one('ir.values', 'More Menu entry', readonly=True,
486                                              help='More menu entry.'),
487         # Client Action
488         'action_id': fields.many2one('ir.actions.actions', 'Client Action',
489                                      help="Select the client action that has to be executed."),
490         # Python code
491         'code': fields.text('Python Code',
492                             help="Write Python code that the action will execute. Some variables are "
493                             "available for use; help about pyhon expression is given in the help tab."),
494         # Workflow signal
495         'use_relational_model': fields.selection([('base', 'Use the base model of the action'),
496                                                   ('relational', 'Use a relation field on the base model')],
497                                                  string='Target Model', required=True),
498         'wkf_transition_id': fields.many2one('workflow.transition', string='Signal to Trigger',
499                                              help="Select the workflow signal to trigger."),
500         'wkf_model_id': fields.many2one('ir.model', 'Target Model',
501                                         help="The model that will receive the workflow signal. Note that it should have a workflow associated with it."),
502         'wkf_model_name': fields.related('wkf_model_id', 'model', type='char', string='Target Model Name', store=True, readonly=True),
503         'wkf_field_id': fields.many2one('ir.model.fields', string='Relation Field',
504                                         oldname='trigger_obj_id',
505                                         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)"),
506         # Multi
507         'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions',
508                                       'server_id', 'action_id',
509                                       string='Child Actions',
510                                       help='Child server actions that will be executed. Note that the last return returned action value will be used as global return value.'),
511         # Create/Copy/Write
512         'use_create': fields.selection([('new', 'Create a new record in the Base Model'),
513                                         ('new_other', 'Create a new record in another model'),
514                                         ('copy_current', 'Copy the current record'),
515                                         ('copy_other', 'Choose and copy a record in the database')],
516                                        string="Creation Policy", required=True,
517                                        help=""),
518         'crud_model_id': fields.many2one('ir.model', 'Target Model',
519                                          oldname='srcmodel_id',
520                                          help="Model for record creation / update. Set this field only to specify a different model than the base model."),
521         'crud_model_name': fields.related('crud_model_id', 'model', type='char',
522                                           string='Create/Write Target Model Name',
523                                           store=True, readonly=True),
524         'ref_object': fields.reference('Reference record', selection=_select_objects, size=128,
525                                        oldname='copy_object'),
526         'link_new_record': fields.boolean('Attach the new record',
527                                           help="Check this if you want to link the newly-created record "
528                                           "to the current record on which the server action runs."),
529         'link_field_id': fields.many2one('ir.model.fields', 'Link using field',
530                                          oldname='record_id',
531                                          help="Provide the field where the record id is stored after the operations."),
532         'use_write': fields.selection([('current', 'Update the current record'),
533                                        ('expression', 'Update a record linked to the current record using python'),
534                                        ('other', 'Choose and Update a record in the database')],
535                                       string='Update Policy', required=True,
536                                       help=""),
537         'write_expression': fields.char('Expression',
538                                         oldname='write_id',
539                                         help="Provide an expression that, applied on the current record, gives the field to update."),
540         'fields_lines': fields.one2many('ir.server.object.lines', 'server_id',
541                                         string='Value Mapping',
542                                         help=""),
543
544         # Fake fields used to implement the placeholder assistant
545         'model_object_field': fields.many2one('ir.model.fields', string="Field",
546                                               help="Select target field from the related document model.\n"
547                                                    "If it is a relationship field you will be able to select "
548                                                    "a target field at the destination of the relationship."),
549         'sub_object': fields.many2one('ir.model', 'Sub-model', readonly=True,
550                                       help="When a relationship field is selected as first field, "
551                                            "this field shows the document model the relationship goes to."),
552         'sub_model_object_field': fields.many2one('ir.model.fields', 'Sub-field',
553                                                   help="When a relationship field is selected as first field, "
554                                                        "this field lets you select the target field within the "
555                                                        "destination document model (sub-model)."),
556         'copyvalue': fields.char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field."),
557         # Fake fields used to implement the ID finding assistant
558         'id_object': fields.reference('Record', selection=_select_objects, size=128),
559         'id_value': fields.char('Record ID'),
560     }
561
562     _defaults = {
563         'state': 'code',
564         'condition': 'True',
565         'type': 'ir.actions.server',
566         'sequence': 5,
567         'code': """# You can use the following variables:
568 #  - self: ORM model of the record on which the action is triggered
569 #  - object: browse_record of the record on which the action is triggered if there is one, otherwise None
570 #  - pool: ORM model pool (i.e. self.pool)
571 #  - cr: database cursor
572 #  - uid: current user id
573 #  - context: current context
574 #  - time: Python time module
575 #  - workflow: Workflow engine
576 # If you plan to return an action, assign: action = {...}""",
577         'use_relational_model': 'base',
578         'use_create': 'new',
579         'use_write': 'current',
580     }
581
582     def _check_expression(self, cr, uid, expression, model_id, context):
583         """ Check python expression (condition, write_expression). Each step of
584         the path must be a valid many2one field, or an integer field for the last
585         step.
586
587         :param str expression: a python expression, beginning by 'obj' or 'object'
588         :param int model_id: the base model of the server action
589         :returns tuple: (is_valid, target_model_name, error_msg)
590         """
591         if not model_id:
592             return (False, None, 'Your expression cannot be validated because the Base Model is not set.')
593         # fetch current model
594         current_model_name = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
595         # transform expression into a path that should look like 'object.many2onefield.many2onefield'
596         path = expression.split('.')
597         initial = path.pop(0)
598         if initial not in ['obj', 'object']:
599             return (False, None, 'Your expression should begin with obj or object.\nAn expression builder is available in the help tab.')
600         # analyze path
601         while path:
602             step = path.pop(0)
603             column_info = self.pool[current_model_name]._all_columns.get(step)
604             if not column_info:
605                 return (False, None, 'Part of the expression (%s) is not recognized as a column in the model %s.' % (step, current_model_name))
606             column_type = column_info.column._type
607             if column_type not in ['many2one', 'int']:
608                 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))
609             if column_type == 'int' and path:
610                 return (False, None, 'Part of the expression (%s) is an integer field that is only allowed at the end of an expression' % (step))
611             if column_type == 'many2one':
612                 current_model_name = column_info.column._obj
613         return (True, current_model_name, None)
614
615     def _check_write_expression(self, cr, uid, ids, context=None):
616         for record in self.browse(cr, uid, ids, context=context):
617             if record.write_expression and record.model_id:
618                 correct, model_name, message = self._check_expression(cr, uid, record.write_expression, record.model_id.id, context=context)
619                 if not correct:
620                     _logger.warning('Invalid expression: %s' % message)
621                     return False
622         return True
623
624     _constraints = [
625         (_check_write_expression,
626             'Incorrect Write Record Expression',
627             ['write_expression']),
628         (partial(osv.Model._check_m2m_recursion, field_name='child_ids'),
629             'Recursion found in child server actions',
630             ['child_ids']),
631     ]
632
633     def on_change_model_id(self, cr, uid, ids, model_id, wkf_model_id, crud_model_id, context=None):
634         """ When changing the action base model, reset workflow and crud config
635         to ease value coherence. """
636         values = {
637             'use_create': 'new',
638             'use_write': 'current',
639             'use_relational_model': 'base',
640             'wkf_model_id': model_id,
641             'wkf_field_id': False,
642             'crud_model_id': model_id,
643         }
644         return {'value': values}
645
646     def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
647         """ Update workflow type configuration
648
649          - update the workflow model (for base (model_id) /relational (field.relation))
650          - update wkf_transition_id to False if workflow model changes, to force
651            the user to choose a new one
652         """
653         values = {}
654         if use_relational_model == 'relational' and wkf_field_id:
655             field = self.pool['ir.model.fields'].browse(cr, uid, wkf_field_id, context=context)
656             new_wkf_model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', field.relation)], context=context)[0]
657             values['wkf_model_id'] = new_wkf_model_id
658         else:
659             values['wkf_model_id'] = model_id
660         return {'value': values}
661
662     def on_change_wkf_model_id(self, cr, uid, ids, wkf_model_id, context=None):
663         """ When changing the workflow model, update its stored name also """
664         wkf_model_name = False
665         if wkf_model_id:
666             wkf_model_name = self.pool.get('ir.model').browse(cr, uid, wkf_model_id, context).model
667         values = {'wkf_transition_id': False, 'wkf_model_name': wkf_model_name}
668         return {'value': values}
669
670     def on_change_crud_config(self, cr, uid, ids, state, use_create, use_write, ref_object, crud_model_id, model_id, context=None):
671         """ Wrapper on CRUD-type (create or write) on_change """
672         if state == 'object_create':
673             return self.on_change_create_config(cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=context)
674         elif state == 'object_write':
675             return self.on_change_write_config(cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=context)
676         else:
677             return {}
678
679     def on_change_create_config(self, cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=None):
680         """ When changing the object_create type configuration:
681
682          - `new` and `copy_current`: crud_model_id is the same as base model
683          - `new_other`: user choose crud_model_id
684          - `copy_other`: disassemble the reference object to have its model
685          - if the target model has changed, then reset the link field that is
686            probably not correct anymore
687         """
688         values = {}
689         if use_create == 'new':
690             values['crud_model_id'] = model_id
691         elif use_create == 'new_other':
692             pass
693         elif use_create == 'copy_current':
694             values['crud_model_id'] = model_id
695         elif use_create == 'copy_other' and ref_object:
696             ref_model, ref_id = ref_object.split(',')
697             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
698             values['crud_model_id'] = ref_model_id
699
700         if values.get('crud_model_id') != crud_model_id:
701             values['link_field_id'] = False
702         return {'value': values}
703
704     def on_change_write_config(self, cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=None):
705         """ When changing the object_write type configuration:
706
707          - `current`: crud_model_id is the same as base model
708          - `other`: disassemble the reference object to have its model
709          - `expression`: has its own on_change, nothing special here
710         """
711         values = {}
712         if use_write == 'current':
713             values['crud_model_id'] = model_id
714         elif use_write == 'other' and ref_object:
715             ref_model, ref_id = ref_object.split(',')
716             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
717             values['crud_model_id'] = ref_model_id
718         elif use_write == 'expression':
719             pass
720
721         if values.get('crud_model_id') != crud_model_id:
722             values['link_field_id'] = False
723         return {'value': values}
724
725     def on_change_write_expression(self, cr, uid, ids, write_expression, model_id, context=None):
726         """ Check the write_expression and update crud_model_id accordingly """
727         values = {}
728         valid, model_name, message = self._check_expression(cr, uid, write_expression, model_id, context=context)
729         if valid:
730             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', model_name)], context=context)[0]
731             values['crud_model_id'] = ref_model_id
732             return {'value': values}
733         if not message:
734             message = 'Invalid expression'
735         return {
736             'warning': {
737                 'title': 'Incorrect expression',
738                 'message': message,
739             }
740         }
741
742     def on_change_crud_model_id(self, cr, uid, ids, crud_model_id, context=None):
743         """ When changing the CRUD model, update its stored name also """
744         crud_model_name = False
745         if crud_model_id:
746             crud_model_name = self.pool.get('ir.model').browse(cr, uid, crud_model_id, context).model
747         values = {'link_field_id': False, 'crud_model_name': crud_model_name}
748         return {'value': values}
749
750     def _build_expression(self, field_name, sub_field_name):
751         """ Returns a placeholder expression for use in a template field,
752         based on the values provided in the placeholder assistant.
753
754         :param field_name: main field name
755         :param sub_field_name: sub field name (M2O)
756         :return: final placeholder expression
757         """
758         expression = ''
759         if field_name:
760             expression = "object." + field_name
761             if sub_field_name:
762                 expression += "." + sub_field_name
763         return expression
764
765     def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, context=None):
766         result = {
767             'sub_object': False,
768             'copyvalue': False,
769             'sub_model_object_field': False,
770         }
771         if model_object_field:
772             fields_obj = self.pool.get('ir.model.fields')
773             field_value = fields_obj.browse(cr, uid, model_object_field, context)
774             if field_value.ttype in ['many2one', 'one2many', 'many2many']:
775                 res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
776                 sub_field_value = False
777                 if sub_model_object_field:
778                     sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
779                 if res_ids:
780                     result.update({
781                         'sub_object': res_ids[0],
782                         'copyvalue': self._build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
783                         'sub_model_object_field': sub_model_object_field or False,
784                     })
785             else:
786                 result.update({
787                     'copyvalue': self._build_expression(field_value.name, False),
788                 })
789         return {'value': result}
790
791     def onchange_id_object(self, cr, uid, ids, id_object, context=None):
792         if id_object:
793             ref_model, ref_id = id_object.split(',')
794             return {'value': {'id_value': ref_id}}
795         return {'value': {'id_value': False}}
796
797     def create_action(self, cr, uid, ids, context=None):
798         """ Create a contextual action for each of the server actions. """
799         for action in self.browse(cr, uid, ids, context=context):
800             ir_values_id = self.pool.get('ir.values').create(cr, SUPERUSER_ID, {
801                 'name': _('Run %s') % action.name,
802                 'model': action.model_id.model,
803                 'key2': 'client_action_multi',
804                 'value': "ir.actions.server,%s" % action.id,
805             }, context)
806             action.write({
807                 'menu_ir_values_id': ir_values_id,
808             })
809
810         return True
811
812     def unlink_action(self, cr, uid, ids, context=None):
813         """ Remove the contextual actions created for the server actions. """
814         for action in self.browse(cr, uid, ids, context=context):
815             if action.menu_ir_values_id:
816                 try:
817                     self.pool.get('ir.values').unlink(cr, SUPERUSER_ID, action.menu_ir_values_id.id, context)
818                 except Exception:
819                     raise osv.except_osv(_('Warning'), _('Deletion of the action record failed.'))
820         return True
821
822     def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):
823         if not action.action_id:
824             raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
825         return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
826
827     def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None):
828         eval(action.code.strip(), eval_context, mode="exec", nocopy=True)  # nocopy allows to return 'action'
829         if 'action' in eval_context:
830             return eval_context['action']
831
832     def run_action_trigger(self, cr, uid, action, eval_context=None, context=None):
833         """ Trigger a workflow signal, depending on the use_relational_model:
834
835          - `base`: base_model_pool.signal_<TRIGGER_NAME>(cr, uid, context.get('active_id'))
836          - `relational`: find the related model and object, using the relational
837            field, then target_model_pool.signal_<TRIGGER_NAME>(cr, uid, target_id)
838         """
839         obj_pool = self.pool[action.model_id.model]
840         if action.use_relational_model == 'base':
841             target_id = context.get('active_id')
842             target_pool = obj_pool
843         else:
844             value = getattr(obj_pool.browse(cr, uid, context.get('active_id'), context=context), action.wkf_field_id.name)
845             if action.wkf_field_id.ttype == 'many2one':
846                 target_id = value.id
847             else:
848                 target_id = value
849             target_pool = self.pool[action.wkf_model_id.model]
850
851         trigger_name = action.wkf_transition_id.signal
852
853         workflow.trg_validate(uid, target_pool._name, target_id, trigger_name, cr)
854
855     def run_action_multi(self, cr, uid, action, eval_context=None, context=None):
856         res = []
857         for act in action.child_ids:
858             result = self.run(cr, uid, [act.id], context)
859             if result:
860                 res.append(result)
861         return res and res[0] or False
862
863     def run_action_object_write(self, cr, uid, action, eval_context=None, context=None):
864         """ Write server action.
865
866          - 1. evaluate the value mapping
867          - 2. depending on the write configuration:
868
869           - `current`: id = active_id
870           - `other`: id = from reference object
871           - `expression`: id = from expression evaluation
872         """
873         res = {}
874         for exp in action.fields_lines:
875             if exp.type == 'equation':
876                 expr = eval(exp.value, eval_context)
877             else:
878                 expr = exp.value
879             res[exp.col1.name] = expr
880
881         if action.use_write == 'current':
882             model = action.model_id.model
883             ref_id = context.get('active_id')
884         elif action.use_write == 'other':
885             model = action.crud_model_id.model
886             ref_id = action.ref_object.id
887         elif action.use_write == 'expression':
888             model = action.crud_model_id.model
889             ref = eval(action.write_expression, eval_context)
890             if isinstance(ref, browse_record):
891                 ref_id = getattr(ref, 'id')
892             else:
893                 ref_id = int(ref)
894
895         obj_pool = self.pool[model]
896         obj_pool.write(cr, uid, [ref_id], res, context=context)
897
898     def run_action_object_create(self, cr, uid, action, eval_context=None, context=None):
899         """ Create and Copy server action.
900
901          - 1. evaluate the value mapping
902          - 2. depending on the write configuration:
903
904           - `new`: new record in the base model
905           - `copy_current`: copy the current record (id = active_id) + gives custom values
906           - `new_other`: new record in target model
907           - `copy_other`: copy the current record (id from reference object)
908             + gives custom values
909         """
910         res = {}
911         for exp in action.fields_lines:
912             if exp.type == 'equation':
913                 expr = eval(exp.value, eval_context)
914             else:
915                 expr = exp.value
916             res[exp.col1.name] = expr
917
918         if action.use_create in ['new', 'copy_current']:
919             model = action.model_id.model
920         elif action.use_create in ['new_other', 'copy_other']:
921             model = action.crud_model_id.model
922
923         obj_pool = self.pool[model]
924         if action.use_create == 'copy_current':
925             ref_id = context.get('active_id')
926             res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
927         elif action.use_create == 'copy_other':
928             ref_id = action.ref_object.id
929             res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
930         else:
931             res_id = obj_pool.create(cr, uid, res, context=context)
932
933         if action.link_new_record and action.link_field_id:
934             self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
935
936     def _get_eval_context(self, cr, uid, action, context=None):
937         """ Prepare the context used when evaluating python code, like the
938         condition or code server actions.
939
940         :param action: the current server action
941         :type action: browse record
942         :returns: dict -- evaluation context given to (safe_)eval """
943         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
944         obj_pool = self.pool[action.model_id.model]
945         obj = None
946         if context.get('active_model') == action.model_id.model and context.get('active_id'):
947             obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
948         return {
949             'self': obj_pool,
950             'object': obj,
951             'obj': obj,
952             'pool': self.pool,
953             'time': time,
954             'datetime': datetime,
955             'dateutil': dateutil,
956             'cr': cr,
957             'uid': uid,
958             'user': user,
959             'context': context,
960             'workflow': workflow,
961             'Warning': openerp.exceptions.Warning,
962         }
963
964     def run(self, cr, uid, ids, context=None):
965         """ Runs the server action. For each server action, the condition is
966         checked. Note that a void (``False``) condition is considered as always
967         valid. If it is verified, the run_action_<STATE> method is called. This
968         allows easy overriding of the server actions.
969
970         :param dict context: context should contain following keys
971
972                              - active_id: id of the current object (single mode)
973                              - active_model: current model that should equal the action's model
974
975                              The following keys are optional:
976
977                              - active_ids: ids of the current records (mass mode). If active_ids
978                                and active_id are present, active_ids is given precedence.
979
980         :return: an action_id to be executed, or False is finished correctly without
981                  return action
982         """
983         if context is None:
984             context = {}
985         res = False
986         for action in self.browse(cr, uid, ids, context):
987             eval_context = self._get_eval_context(cr, uid, action, context=context)
988             condition = action.condition
989             if condition is False:
990                 # Void (aka False) conditions are considered as True
991                 condition = True
992             if hasattr(self, 'run_action_%s_multi' % action.state):
993                 run_context = eval_context['context']
994                 expr = eval(str(condition), eval_context)
995                 if not expr:
996                     continue
997                 # call the multi method
998                 func = getattr(self, 'run_action_%s_multi' % action.state)
999                 res = func(cr, uid, action, eval_context=eval_context, context=run_context)
1000
1001             elif hasattr(self, 'run_action_%s' % action.state):
1002                 func = getattr(self, 'run_action_%s' % action.state)
1003                 active_id = context.get('active_id')
1004                 active_ids = context.get('active_ids', [active_id] if active_id else [])
1005                 for active_id in active_ids:
1006                     # run context dedicated to a particular active_id
1007                     run_context = dict(context, active_ids=[active_id], active_id=active_id)
1008                     eval_context["context"] = run_context
1009                     expr = eval(str(condition), eval_context)
1010                     if not expr:
1011                         continue
1012                     # call the single method related to the action: run_action_<STATE>
1013                     res = func(cr, uid, action, eval_context=eval_context, context=run_context)
1014         return res
1015
1016
1017 class ir_server_object_lines(osv.osv):
1018     _name = 'ir.server.object.lines'
1019     _sequence = 'ir_actions_id_seq'
1020     _columns = {
1021         'server_id': fields.many2one('ir.actions.server', 'Related Server Action'),
1022         'col1': fields.many2one('ir.model.fields', 'Field', required=True),
1023         'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
1024                                                           "When Formula type is selected, this field may be a Python expression "
1025                                                           " that can use the same values as for the condition field on the server action.\n"
1026                                                           "If Value type is selected, the value will be used directly without evaluation."),
1027         'type': fields.selection([
1028             ('value', 'Value'),
1029             ('equation', 'Python expression')
1030         ], 'Evaluation Type', required=True, change_default=True),
1031     }
1032     _defaults = {
1033         'type': 'value',
1034     }
1035
1036
1037 TODO_STATES = [('open', 'To Do'),
1038                ('done', 'Done')]
1039 TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
1040               ('automatic', 'Launch Automatically')]
1041 class ir_actions_todo(osv.osv):
1042     """
1043     Configuration Wizards
1044     """
1045     _name = 'ir.actions.todo'
1046     _description = "Configuration Wizards"
1047     _columns={
1048         'action_id': fields.many2one(
1049             'ir.actions.actions', 'Action', select=True, required=True),
1050         'sequence': fields.integer('Sequence'),
1051         'state': fields.selection(TODO_STATES, string='Status', required=True),
1052         'name': fields.char('Name', size=64),
1053         'type': fields.selection(TODO_TYPES, 'Type', required=True,
1054             help="""Manual: Launched manually.
1055 Automatic: Runs whenever the system is reconfigured.
1056 Launch Manually Once: after having been launched manually, it sets automatically to Done."""),
1057         'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
1058         'note': fields.text('Text', translate=True),
1059     }
1060     _defaults={
1061         'state': 'open',
1062         'sequence': 10,
1063         'type': 'manual',
1064     }
1065     _order="sequence,id"
1066
1067     def name_get(self, cr, uid, ids, context=None):
1068         return [(rec.id, rec.action_id.name) for rec in self.browse(cr, uid, ids, context=context)]
1069
1070     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1071         if args is None:
1072             args = []
1073         if name:
1074             ids = self.search(cr, user, [('action_id', operator, name)] + args, limit=limit)
1075             return self.name_get(cr, user, ids, context=context)
1076         return super(ir_actions_todo, self).name_search(cr, user, name, args=args, operator=operator, context=context, limit=limit)
1077
1078
1079     def action_launch(self, cr, uid, ids, context=None):
1080         """ Launch Action of Wizard"""
1081         wizard_id = ids and ids[0] or False
1082         wizard = self.browse(cr, uid, wizard_id, context=context)
1083         if wizard.type in ('automatic', 'once'):
1084             wizard.write({'state': 'done'})
1085
1086         # Load action
1087         act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
1088
1089         res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context)
1090         if act_type['type'] != 'ir.actions.act_window':
1091             return res
1092         res.setdefault('context','{}')
1093         res['nodestroy'] = True
1094
1095         # Open a specific record when res_id is provided in the context
1096         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1097         ctx = eval(res['context'], {'user': user})
1098         if ctx.get('res_id'):
1099             res.update({'res_id': ctx.pop('res_id')})
1100
1101         # disable log for automatic wizards
1102         if wizard.type == 'automatic':
1103             ctx.update({'disable_log': True})
1104         res.update({'context': ctx})
1105
1106         return res
1107
1108     def action_open(self, cr, uid, ids, context=None):
1109         """ Sets configuration wizard in TODO state"""
1110         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
1111
1112     def progress(self, cr, uid, context=None):
1113         """ Returns a dict with 3 keys {todo, done, total}.
1114
1115         These keys all map to integers and provide the number of todos
1116         marked as open, the total number of todos and the number of
1117         todos not open (which is basically a shortcut to total-todo)
1118
1119         :rtype: dict
1120         """
1121         user_groups = set(map(
1122             lambda x: x.id,
1123             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
1124         def groups_match(todo):
1125             """ Checks if the todo's groups match those of the current user
1126             """
1127             return not todo.groups_id \
1128                    or bool(user_groups.intersection((
1129                         group.id for group in todo.groups_id)))
1130
1131         done = filter(
1132             groups_match,
1133             self.browse(cr, uid,
1134                 self.search(cr, uid, [('state', '!=', 'open')], context=context),
1135                         context=context))
1136
1137         total = filter(
1138             groups_match,
1139             self.browse(cr, uid,
1140                 self.search(cr, uid, [], context=context),
1141                         context=context))
1142
1143         return {
1144             'done': len(done),
1145             'total': len(total),
1146             'todo': len(total) - len(done)
1147         }
1148
1149
1150 class ir_actions_act_client(osv.osv):
1151     _name = 'ir.actions.client'
1152     _inherit = 'ir.actions.actions'
1153     _table = 'ir_act_client'
1154     _sequence = 'ir_actions_id_seq'
1155     _order = 'name'
1156
1157     def _get_params(self, cr, uid, ids, field_name, arg, context):
1158         result = {}
1159         for record in self.browse(cr, uid, ids, context=context):
1160             result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
1161         return result
1162
1163     def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
1164         if isinstance(field_value, dict):
1165             self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
1166         else:
1167             self.write(cr, uid, id, {'params_store': field_value}, context=context)
1168
1169     _columns = {
1170         'name': fields.char('Action Name', required=True, size=64, translate=True),
1171         'tag': fields.char('Client action tag', size=64, required=True,
1172                            help="An arbitrary string, interpreted by the client"
1173                                 " according to its own needs and wishes. There "
1174                                 "is no central tag repository across clients."),
1175         'res_model': fields.char('Destination Model', size=64, 
1176             help="Optional model, mostly used for needactions."),
1177         'context': fields.char('Context Value', size=250, required=True,
1178             help="Context dictionary as Python expression, empty by default (Default: {})"),
1179         'params': fields.function(_get_params, fnct_inv=_set_params,
1180                                   type='binary', 
1181                                   string="Supplementary arguments",
1182                                   help="Arguments sent to the client along with"
1183                                        "the view tag"),
1184         'params_store': fields.binary("Params storage", readonly=True)
1185     }
1186     _defaults = {
1187         'type': 'ir.actions.client',
1188         'context': '{}',
1189
1190     }
1191
1192 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: