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