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