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