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