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