[MERGE] forward port of branch saas-5 up to adf07a9
[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                     continue
340                 if not fields or 'help' in fields:
341                     custom_context = dict(context, **eval_context)
342                     res['help'] = self.pool.get(model).get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context)
343         if ids_int:
344             return results[0]
345         return results
346
347     def for_xml_id(self, cr, uid, module, xml_id, context=None):
348         """ Returns the act_window object created for the provided xml_id
349
350         :param module: the module the act_window originates in
351         :param xml_id: the namespace-less id of the action (the @id
352                        attribute from the XML file)
353         :return: A read() view of the ir.actions.act_window
354         """
355         dataobj = self.pool.get('ir.model.data')
356         data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
357         res_id = dataobj.browse(cr, uid, data_id, context).res_id
358         return self.read(cr, uid, res_id, [], context)
359
360 VIEW_TYPES = [
361     ('tree', 'Tree'),
362     ('form', 'Form'),
363     ('graph', 'Graph'),
364     ('calendar', 'Calendar'),
365     ('gantt', 'Gantt'),
366     ('kanban', 'Kanban')]
367 class ir_actions_act_window_view(osv.osv):
368     _name = 'ir.actions.act_window.view'
369     _table = 'ir_act_window_view'
370     _rec_name = 'view_id'
371     _order = 'sequence'
372     _columns = {
373         'sequence': fields.integer('Sequence'),
374         'view_id': fields.many2one('ir.ui.view', 'View'),
375         'view_mode': fields.selection(VIEW_TYPES, string='View Type', required=True),
376         'act_window_id': fields.many2one('ir.actions.act_window', 'Action', ondelete='cascade'),
377         'multi': fields.boolean('On Multiple Doc.',
378             help="If set to true, the action will not be displayed on the right toolbar of a form view."),
379     }
380     _defaults = {
381         'multi': False,
382     }
383     def _auto_init(self, cr, context=None):
384         super(ir_actions_act_window_view, self)._auto_init(cr, context)
385         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
386         if not cr.fetchone():
387             cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
388
389
390 class ir_actions_act_window_close(osv.osv):
391     _name = 'ir.actions.act_window_close'
392     _inherit = 'ir.actions.actions'
393     _table = 'ir_actions'
394     _defaults = {
395         'type': 'ir.actions.act_window_close',
396     }
397
398
399 class ir_actions_act_url(osv.osv):
400     _name = 'ir.actions.act_url'
401     _table = 'ir_act_url'
402     _inherit = 'ir.actions.actions'
403     _sequence = 'ir_actions_id_seq'
404     _order = 'name'
405     _columns = {
406         'name': fields.char('Action Name', size=64, translate=True),
407         'type': fields.char('Action Type', size=32, required=True),
408         'url': fields.text('Action URL',required=True),
409         'target': fields.selection((
410             ('new', 'New Window'),
411             ('self', 'This Window')),
412             'Action Target', required=True
413         )
414     }
415     _defaults = {
416         'type': 'ir.actions.act_url',
417         'target': 'new'
418     }
419
420
421 class ir_actions_server(osv.osv):
422     """ Server actions model. Server action work on a base model and offer various
423     type of actions that can be executed automatically, for example using base
424     action rules, of manually, by adding the action in the 'More' contextual
425     menu.
426
427     Since OpenERP 8.0 a button 'Create Menu Action' button is available on the
428     action form view. It creates an entry in the More menu of the base model.
429     This allows to create server actions and run them in mass mode easily through
430     the interface.
431
432     The available actions are :
433
434     - 'Execute Python Code': a block of python code that will be executed
435     - 'Trigger a Workflow Signal': send a signal to a workflow
436     - 'Run a Client Action': choose a client action to launch
437     - 'Create or Copy a new Record': create a new record with new values, or
438       copy an existing record in your database
439     - 'Write on a Record': update the values of a record
440     - 'Execute several actions': define an action that triggers several other
441       server actions
442     """
443     _name = 'ir.actions.server'
444     _table = 'ir_act_server'
445     _inherit = 'ir.actions.actions'
446     _sequence = 'ir_actions_id_seq'
447     _order = 'sequence,name'
448
449     def _select_objects(self, cr, uid, context=None):
450         model_pool = self.pool.get('ir.model')
451         ids = model_pool.search(cr, uid, [], limit=None)
452         res = model_pool.read(cr, uid, ids, ['model', 'name'])
453         return [(r['model'], r['name']) for r in res] + [('', '')]
454
455     def _get_states(self, cr, uid, context=None):
456         """ Override me in order to add new states in the server action. Please
457         note that the added key length should not be higher than already-existing
458         ones. """
459         return [('code', 'Execute Python Code'),
460                 ('trigger', 'Trigger a Workflow Signal'),
461                 ('client_action', 'Run a Client Action'),
462                 ('object_create', 'Create or Copy a new Record'),
463                 ('object_write', 'Write on a Record'),
464                 ('multi', 'Execute several actions')]
465
466     def _get_states_wrapper(self, cr, uid, context=None):
467         return self._get_states(cr, uid, context)
468
469     _columns = {
470         'name': fields.char('Action Name', required=True, size=64, translate=True),
471         'condition': fields.char('Condition',
472                                  help="Condition verified before executing the server action. If it "
473                                  "is not verified, the action will not be executed. The condition is "
474                                  "a Python expression, like 'object.list_price > 5000'. A void "
475                                  "condition is considered as always True. Help about python expression "
476                                  "is given in the help tab."),
477         'state': fields.selection(_get_states_wrapper, 'Action To Do', required=True,
478                                   help="Type of server action. The following values are available:\n"
479                                   "- 'Execute Python Code': a block of python code that will be executed\n"
480                                   "- 'Trigger a Workflow Signal': send a signal to a workflow\n"
481                                   "- 'Run a Client Action': choose a client action to launch\n"
482                                   "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
483                                   "- 'Write on a Record': update the values of a record\n"
484                                   "- 'Execute several actions': define an action that triggers several other server actions\n"
485                                   "- 'Send Email': automatically send an email (available in email_template)"),
486         'usage': fields.char('Action Usage', size=32),
487         'type': fields.char('Action Type', size=32, required=True),
488         # Generic
489         'sequence': fields.integer('Sequence',
490                                    help="When dealing with multiple actions, the execution order is "
491                                    "based on the sequence. Low number means high priority."),
492         'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade',
493                                     help="Base model on which the server action runs."),
494         'model_name': fields.related('model_id', 'model', type='char',
495                                      string='Model Name', readonly=True),
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
656         if model_id:
657             values['model_name'] = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
658
659         return {'value': values}
660
661     def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
662         """ Update workflow type configuration
663
664          - update the workflow model (for base (model_id) /relational (field.relation))
665          - update wkf_transition_id to False if workflow model changes, to force
666            the user to choose a new one
667         """
668         values = {}
669         if use_relational_model == 'relational' and wkf_field_id:
670             field = self.pool['ir.model.fields'].browse(cr, uid, wkf_field_id, context=context)
671             new_wkf_model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', field.relation)], context=context)[0]
672             values['wkf_model_id'] = new_wkf_model_id
673         else:
674             values['wkf_model_id'] = model_id
675         return {'value': values}
676
677     def on_change_wkf_model_id(self, cr, uid, ids, wkf_model_id, context=None):
678         """ When changing the workflow model, update its stored name also """
679         wkf_model_name = False
680         if wkf_model_id:
681             wkf_model_name = self.pool.get('ir.model').browse(cr, uid, wkf_model_id, context).model
682         values = {'wkf_transition_id': False, 'wkf_model_name': wkf_model_name}
683         return {'value': values}
684
685     def on_change_crud_config(self, cr, uid, ids, state, use_create, use_write, ref_object, crud_model_id, model_id, context=None):
686         """ Wrapper on CRUD-type (create or write) on_change """
687         if state == 'object_create':
688             return self.on_change_create_config(cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=context)
689         elif state == 'object_write':
690             return self.on_change_write_config(cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=context)
691         else:
692             return {}
693
694     def on_change_create_config(self, cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=None):
695         """ When changing the object_create type configuration:
696
697          - `new` and `copy_current`: crud_model_id is the same as base model
698          - `new_other`: user choose crud_model_id
699          - `copy_other`: disassemble the reference object to have its model
700          - if the target model has changed, then reset the link field that is
701            probably not correct anymore
702         """
703         values = {}
704         if use_create == 'new':
705             values['crud_model_id'] = model_id
706         elif use_create == 'new_other':
707             pass
708         elif use_create == 'copy_current':
709             values['crud_model_id'] = model_id
710         elif use_create == 'copy_other' and ref_object:
711             ref_model, ref_id = ref_object.split(',')
712             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
713             values['crud_model_id'] = ref_model_id
714
715         if values.get('crud_model_id') != crud_model_id:
716             values['link_field_id'] = False
717         return {'value': values}
718
719     def on_change_write_config(self, cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=None):
720         """ When changing the object_write type configuration:
721
722          - `current`: crud_model_id is the same as base model
723          - `other`: disassemble the reference object to have its model
724          - `expression`: has its own on_change, nothing special here
725         """
726         values = {}
727         if use_write == 'current':
728             values['crud_model_id'] = model_id
729         elif use_write == 'other' and ref_object:
730             ref_model, ref_id = ref_object.split(',')
731             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
732             values['crud_model_id'] = ref_model_id
733         elif use_write == 'expression':
734             pass
735
736         if values.get('crud_model_id') != crud_model_id:
737             values['link_field_id'] = False
738         return {'value': values}
739
740     def on_change_write_expression(self, cr, uid, ids, write_expression, model_id, context=None):
741         """ Check the write_expression and update crud_model_id accordingly """
742         values = {}
743         valid, model_name, message = self._check_expression(cr, uid, write_expression, model_id, context=context)
744         if valid:
745             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', model_name)], context=context)[0]
746             values['crud_model_id'] = ref_model_id
747             return {'value': values}
748         if not message:
749             message = 'Invalid expression'
750         return {
751             'warning': {
752                 'title': 'Incorrect expression',
753                 'message': message,
754             }
755         }
756
757     def on_change_crud_model_id(self, cr, uid, ids, crud_model_id, context=None):
758         """ When changing the CRUD model, update its stored name also """
759         crud_model_name = False
760         if crud_model_id:
761             crud_model_name = self.pool.get('ir.model').browse(cr, uid, crud_model_id, context).model
762         
763         values = {'link_field_id': False, 'crud_model_name': crud_model_name}
764         return {'value': values}
765
766     def _build_expression(self, field_name, sub_field_name):
767         """ Returns a placeholder expression for use in a template field,
768         based on the values provided in the placeholder assistant.
769
770         :param field_name: main field name
771         :param sub_field_name: sub field name (M2O)
772         :return: final placeholder expression
773         """
774         expression = ''
775         if field_name:
776             expression = "object." + field_name
777             if sub_field_name:
778                 expression += "." + sub_field_name
779         return expression
780
781     def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, context=None):
782         result = {
783             'sub_object': False,
784             'copyvalue': False,
785             'sub_model_object_field': False,
786         }
787         if model_object_field:
788             fields_obj = self.pool.get('ir.model.fields')
789             field_value = fields_obj.browse(cr, uid, model_object_field, context)
790             if field_value.ttype in ['many2one', 'one2many', 'many2many']:
791                 res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
792                 sub_field_value = False
793                 if sub_model_object_field:
794                     sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
795                 if res_ids:
796                     result.update({
797                         'sub_object': res_ids[0],
798                         'copyvalue': self._build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
799                         'sub_model_object_field': sub_model_object_field or False,
800                     })
801             else:
802                 result.update({
803                     'copyvalue': self._build_expression(field_value.name, False),
804                 })
805         return {'value': result}
806
807     def onchange_id_object(self, cr, uid, ids, id_object, context=None):
808         if id_object:
809             ref_model, ref_id = id_object.split(',')
810             return {'value': {'id_value': ref_id}}
811         return {'value': {'id_value': False}}
812
813     def create_action(self, cr, uid, ids, context=None):
814         """ Create a contextual action for each of the server actions. """
815         for action in self.browse(cr, uid, ids, context=context):
816             ir_values_id = self.pool.get('ir.values').create(cr, SUPERUSER_ID, {
817                 'name': _('Run %s') % action.name,
818                 'model': action.model_id.model,
819                 'key2': 'client_action_multi',
820                 'value': "ir.actions.server,%s" % action.id,
821             }, context)
822             action.write({
823                 'menu_ir_values_id': ir_values_id,
824             })
825
826         return True
827
828     def unlink_action(self, cr, uid, ids, context=None):
829         """ Remove the contextual actions created for the server actions. """
830         for action in self.browse(cr, uid, ids, context=context):
831             if action.menu_ir_values_id:
832                 try:
833                     self.pool.get('ir.values').unlink(cr, SUPERUSER_ID, action.menu_ir_values_id.id, context)
834                 except Exception:
835                     raise osv.except_osv(_('Warning'), _('Deletion of the action record failed.'))
836         return True
837
838     def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):
839         if not action.action_id:
840             raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
841         return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
842
843     def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None):
844         eval(action.code.strip(), eval_context, mode="exec", nocopy=True)  # nocopy allows to return 'action'
845         if 'action' in eval_context:
846             return eval_context['action']
847
848     def run_action_trigger(self, cr, uid, action, eval_context=None, context=None):
849         """ Trigger a workflow signal, depending on the use_relational_model:
850
851          - `base`: base_model_pool.signal_<TRIGGER_NAME>(cr, uid, context.get('active_id'))
852          - `relational`: find the related model and object, using the relational
853            field, then target_model_pool.signal_<TRIGGER_NAME>(cr, uid, target_id)
854         """
855         obj_pool = self.pool[action.model_id.model]
856         if action.use_relational_model == 'base':
857             target_id = context.get('active_id')
858             target_pool = obj_pool
859         else:
860             value = getattr(obj_pool.browse(cr, uid, context.get('active_id'), context=context), action.wkf_field_id.name)
861             if action.wkf_field_id.ttype == 'many2one':
862                 target_id = value.id
863             else:
864                 target_id = value
865             target_pool = self.pool[action.wkf_model_id.model]
866
867         trigger_name = action.wkf_transition_id.signal
868
869         workflow.trg_validate(uid, target_pool._name, target_id, trigger_name, cr)
870
871     def run_action_multi(self, cr, uid, action, eval_context=None, context=None):
872         res = []
873         for act in action.child_ids:
874             result = self.run(cr, uid, [act.id], context)
875             if result:
876                 res.append(result)
877         return res and res[0] or False
878
879     def run_action_object_write(self, cr, uid, action, eval_context=None, context=None):
880         """ Write server action.
881
882          - 1. evaluate the value mapping
883          - 2. depending on the write configuration:
884
885           - `current`: id = active_id
886           - `other`: id = from reference object
887           - `expression`: id = from expression evaluation
888         """
889         res = {}
890         for exp in action.fields_lines:
891             if exp.type == 'equation':
892                 expr = eval(exp.value, eval_context)
893             else:
894                 expr = exp.value
895             res[exp.col1.name] = expr
896
897         if action.use_write == 'current':
898             model = action.model_id.model
899             ref_id = context.get('active_id')
900         elif action.use_write == 'other':
901             model = action.crud_model_id.model
902             ref_id = action.ref_object.id
903         elif action.use_write == 'expression':
904             model = action.crud_model_id.model
905             ref = eval(action.write_expression, eval_context)
906             if isinstance(ref, browse_record):
907                 ref_id = getattr(ref, 'id')
908             else:
909                 ref_id = int(ref)
910
911         obj_pool = self.pool[model]
912         obj_pool.write(cr, uid, [ref_id], res, context=context)
913
914     def run_action_object_create(self, cr, uid, action, eval_context=None, context=None):
915         """ Create and Copy server action.
916
917          - 1. evaluate the value mapping
918          - 2. depending on the write configuration:
919
920           - `new`: new record in the base model
921           - `copy_current`: copy the current record (id = active_id) + gives custom values
922           - `new_other`: new record in target model
923           - `copy_other`: copy the current record (id from reference object)
924             + gives custom values
925         """
926         res = {}
927         for exp in action.fields_lines:
928             if exp.type == 'equation':
929                 expr = eval(exp.value, eval_context)
930             else:
931                 expr = exp.value
932             res[exp.col1.name] = expr
933
934         if action.use_create in ['new', 'copy_current']:
935             model = action.model_id.model
936         elif action.use_create in ['new_other', 'copy_other']:
937             model = action.crud_model_id.model
938
939         obj_pool = self.pool[model]
940         if action.use_create == 'copy_current':
941             ref_id = context.get('active_id')
942             res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
943         elif action.use_create == 'copy_other':
944             ref_id = action.ref_object.id
945             res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
946         else:
947             res_id = obj_pool.create(cr, uid, res, context=context)
948
949         if action.link_new_record and action.link_field_id:
950             self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
951
952     def _get_eval_context(self, cr, uid, action, context=None):
953         """ Prepare the context used when evaluating python code, like the
954         condition or code server actions.
955
956         :param action: the current server action
957         :type action: browse record
958         :returns: dict -- evaluation context given to (safe_)eval """
959         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
960         obj_pool = self.pool[action.model_id.model]
961         obj = None
962         if context.get('active_model') == action.model_id.model and context.get('active_id'):
963             obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
964         return {
965             'self': obj_pool,
966             'object': obj,
967             'obj': obj,
968             'pool': self.pool,
969             'time': time,
970             'datetime': datetime,
971             'dateutil': dateutil,
972             'cr': cr,
973             'uid': uid,
974             'user': user,
975             'context': context,
976             'workflow': workflow,
977             'Warning': openerp.exceptions.Warning,
978         }
979
980     def run(self, cr, uid, ids, context=None):
981         """ Runs the server action. For each server action, the condition is
982         checked. Note that a void (``False``) condition is considered as always
983         valid. If it is verified, the run_action_<STATE> method is called. This
984         allows easy overriding of the server actions.
985
986         :param dict context: context should contain following keys
987
988                              - active_id: id of the current object (single mode)
989                              - active_model: current model that should equal the action's model
990
991                              The following keys are optional:
992
993                              - active_ids: ids of the current records (mass mode). If active_ids
994                                and active_id are present, active_ids is given precedence.
995
996         :return: an action_id to be executed, or False is finished correctly without
997                  return action
998         """
999         if context is None:
1000             context = {}
1001         res = False
1002         for action in self.browse(cr, uid, ids, context):
1003             eval_context = self._get_eval_context(cr, uid, action, context=context)
1004             condition = action.condition
1005             if condition is False:
1006                 # Void (aka False) conditions are considered as True
1007                 condition = True
1008             if hasattr(self, 'run_action_%s_multi' % action.state):
1009                 run_context = eval_context['context']
1010                 expr = eval(str(condition), eval_context)
1011                 if not expr:
1012                     continue
1013                 # call the multi method
1014                 func = getattr(self, 'run_action_%s_multi' % action.state)
1015                 res = func(cr, uid, action, eval_context=eval_context, context=run_context)
1016
1017             elif hasattr(self, 'run_action_%s' % action.state):
1018                 func = getattr(self, 'run_action_%s' % action.state)
1019                 active_id = context.get('active_id')
1020                 active_ids = context.get('active_ids', [active_id] if active_id else [])
1021                 for active_id in active_ids:
1022                     # run context dedicated to a particular active_id
1023                     run_context = dict(context, active_ids=[active_id], active_id=active_id)
1024                     eval_context["context"] = run_context
1025                     expr = eval(str(condition), eval_context)
1026                     if not expr:
1027                         continue
1028                     # call the single method related to the action: run_action_<STATE>
1029                     res = func(cr, uid, action, eval_context=eval_context, context=run_context)
1030         return res
1031
1032
1033 class ir_server_object_lines(osv.osv):
1034     _name = 'ir.server.object.lines'
1035     _sequence = 'ir_actions_id_seq'
1036     _columns = {
1037         'server_id': fields.many2one('ir.actions.server', 'Related Server Action'),
1038         'col1': fields.many2one('ir.model.fields', 'Field', required=True),
1039         'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
1040                                                           "When Formula type is selected, this field may be a Python expression "
1041                                                           " that can use the same values as for the condition field on the server action.\n"
1042                                                           "If Value type is selected, the value will be used directly without evaluation."),
1043         'type': fields.selection([
1044             ('value', 'Value'),
1045             ('equation', 'Python expression')
1046         ], 'Evaluation Type', required=True, change_default=True),
1047     }
1048     _defaults = {
1049         'type': 'value',
1050     }
1051
1052
1053 TODO_STATES = [('open', 'To Do'),
1054                ('done', 'Done')]
1055 TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
1056               ('automatic', 'Launch Automatically')]
1057 class ir_actions_todo(osv.osv):
1058     """
1059     Configuration Wizards
1060     """
1061     _name = 'ir.actions.todo'
1062     _description = "Configuration Wizards"
1063     _columns={
1064         'action_id': fields.many2one(
1065             'ir.actions.actions', 'Action', select=True, required=True),
1066         'sequence': fields.integer('Sequence'),
1067         'state': fields.selection(TODO_STATES, string='Status', required=True),
1068         'name': fields.char('Name', size=64),
1069         'type': fields.selection(TODO_TYPES, 'Type', required=True,
1070             help="""Manual: Launched manually.
1071 Automatic: Runs whenever the system is reconfigured.
1072 Launch Manually Once: after having been launched manually, it sets automatically to Done."""),
1073         'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
1074         'note': fields.text('Text', translate=True),
1075     }
1076     _defaults={
1077         'state': 'open',
1078         'sequence': 10,
1079         'type': 'manual',
1080     }
1081     _order="sequence,id"
1082
1083     def name_get(self, cr, uid, ids, context=None):
1084         return [(rec.id, rec.action_id.name) for rec in self.browse(cr, uid, ids, context=context)]
1085
1086     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1087         if args is None:
1088             args = []
1089         if name:
1090             ids = self.search(cr, user, [('action_id', operator, name)] + args, limit=limit)
1091             return self.name_get(cr, user, ids, context=context)
1092         return super(ir_actions_todo, self).name_search(cr, user, name, args=args, operator=operator, context=context, limit=limit)
1093
1094
1095     def action_launch(self, cr, uid, ids, context=None):
1096         """ Launch Action of Wizard"""
1097         wizard_id = ids and ids[0] or False
1098         wizard = self.browse(cr, uid, wizard_id, context=context)
1099         if wizard.type in ('automatic', 'once'):
1100             wizard.write({'state': 'done'})
1101
1102         # Load action
1103         act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
1104
1105         res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context)
1106         if act_type['type'] != 'ir.actions.act_window':
1107             return res
1108         res.setdefault('context','{}')
1109         res['nodestroy'] = True
1110
1111         # Open a specific record when res_id is provided in the context
1112         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1113         ctx = eval(res['context'], {'user': user})
1114         if ctx.get('res_id'):
1115             res.update({'res_id': ctx.pop('res_id')})
1116
1117         # disable log for automatic wizards
1118         if wizard.type == 'automatic':
1119             ctx.update({'disable_log': True})
1120         res.update({'context': ctx})
1121
1122         return res
1123
1124     def action_open(self, cr, uid, ids, context=None):
1125         """ Sets configuration wizard in TODO state"""
1126         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
1127
1128     def progress(self, cr, uid, context=None):
1129         """ Returns a dict with 3 keys {todo, done, total}.
1130
1131         These keys all map to integers and provide the number of todos
1132         marked as open, the total number of todos and the number of
1133         todos not open (which is basically a shortcut to total-todo)
1134
1135         :rtype: dict
1136         """
1137         user_groups = set(map(
1138             lambda x: x.id,
1139             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
1140         def groups_match(todo):
1141             """ Checks if the todo's groups match those of the current user
1142             """
1143             return not todo.groups_id \
1144                    or bool(user_groups.intersection((
1145                         group.id for group in todo.groups_id)))
1146
1147         done = filter(
1148             groups_match,
1149             self.browse(cr, uid,
1150                 self.search(cr, uid, [('state', '!=', 'open')], context=context),
1151                         context=context))
1152
1153         total = filter(
1154             groups_match,
1155             self.browse(cr, uid,
1156                 self.search(cr, uid, [], context=context),
1157                         context=context))
1158
1159         return {
1160             'done': len(done),
1161             'total': len(total),
1162             'todo': len(total) - len(done)
1163         }
1164
1165
1166 class ir_actions_act_client(osv.osv):
1167     _name = 'ir.actions.client'
1168     _inherit = 'ir.actions.actions'
1169     _table = 'ir_act_client'
1170     _sequence = 'ir_actions_id_seq'
1171     _order = 'name'
1172
1173     def _get_params(self, cr, uid, ids, field_name, arg, context):
1174         result = {}
1175         for record in self.browse(cr, uid, ids, context=context):
1176             result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
1177         return result
1178
1179     def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
1180         if isinstance(field_value, dict):
1181             self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
1182         else:
1183             self.write(cr, uid, id, {'params_store': field_value}, context=context)
1184
1185     _columns = {
1186         'name': fields.char('Action Name', required=True, size=64, translate=True),
1187         'tag': fields.char('Client action tag', size=64, required=True,
1188                            help="An arbitrary string, interpreted by the client"
1189                                 " according to its own needs and wishes. There "
1190                                 "is no central tag repository across clients."),
1191         'res_model': fields.char('Destination Model', size=64, 
1192             help="Optional model, mostly used for needactions."),
1193         'context': fields.char('Context Value', size=250, required=True,
1194             help="Context dictionary as Python expression, empty by default (Default: {})"),
1195         'params': fields.function(_get_params, fnct_inv=_set_params,
1196                                   type='binary', 
1197                                   string="Supplementary arguments",
1198                                   help="Arguments sent to the client along with"
1199                                        "the view tag"),
1200         'params_store': fields.binary("Params storage", readonly=True)
1201     }
1202     _defaults = {
1203         'type': 'ir.actions.client',
1204         'context': '{}',
1205
1206     }
1207
1208 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: