[MERGE] Sync with trunk.
[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-2011 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 import logging
23 import operator
24 import os
25 import re
26 from socket import gethostname
27 import time
28
29 import openerp
30 from openerp import SUPERUSER_ID
31 from openerp import tools
32 from openerp.osv import fields, osv
33 import openerp.report.interface
34 from openerp.report.report_sxw import report_sxw, report_rml
35 from openerp.tools.config import config
36 from openerp.tools.safe_eval import safe_eval as eval
37 from openerp.tools.translate import _
38 import openerp.workflow
39
40 _logger = logging.getLogger(__name__)
41
42 class actions(osv.osv):
43     _name = 'ir.actions.actions'
44     _table = 'ir_actions'
45     _order = 'name'
46     _columns = {
47         'name': fields.char('Name', size=64, required=True),
48         'type': fields.char('Action Type', required=True, size=32),
49         'usage': fields.char('Action Usage', size=32),
50         'help': fields.text('Action description',
51             help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
52             translate=True),
53     }
54     _defaults = {
55         'usage': lambda *a: False,
56     }
57 actions()
58
59
60 class report_xml(osv.osv):
61
62     def _report_content(self, cursor, user, ids, name, arg, context=None):
63         res = {}
64         for report in self.browse(cursor, user, ids, context=context):
65             data = report[name + '_data']
66             if not data and report[name[:-8]]:
67                 fp = None
68                 try:
69                     fp = tools.file_open(report[name[:-8]], mode='rb')
70                     data = fp.read()
71                 except:
72                     data = False
73                 finally:
74                     if fp:
75                         fp.close()
76             res[report.id] = data
77         return res
78
79     def _report_content_inv(self, cursor, user, id, name, value, arg, context=None):
80         self.write(cursor, user, id, {name+'_data': value}, context=context)
81
82     def _report_sxw(self, cursor, user, ids, name, arg, context=None):
83         res = {}
84         for report in self.browse(cursor, user, ids, context=context):
85             if report.report_rml:
86                 res[report.id] = report.report_rml.replace('.rml', '.sxw')
87             else:
88                 res[report.id] = False
89         return res
90
91     def _lookup_report(self, cr, name):
92         """
93         Look up a report definition.
94         """
95         opj = os.path.join
96
97         # First lookup in the deprecated place, because if the report definition
98         # has not been updated, it is more likely the correct definition is there.
99         # Only reports with custom parser sepcified in Python are still there.
100         if 'report.' + name in openerp.report.interface.report_int._reports:
101             new_report = openerp.report.interface.report_int._reports['report.' + name]
102         else:
103             cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
104             r = cr.dictfetchone()
105             if r:
106                 if r['report_rml'] or r['report_rml_content_data']:
107                     if r['parser']:
108                         kwargs = { 'parser': operator.attrgetter(r['parser'])(openerp.addons) }
109                     else:
110                         kwargs = {}
111                     new_report = report_sxw('report.'+r['report_name'], r['model'],
112                             opj('addons',r['report_rml'] or '/'), header=r['header'], register=False, **kwargs)
113                 elif r['report_xsl']:
114                     new_report = report_rml('report.'+r['report_name'], r['model'],
115                             opj('addons',r['report_xml']),
116                             r['report_xsl'] and opj('addons',r['report_xsl']), register=False)
117                 else:
118                     raise Exception, "Unhandled report type: %s" % r
119             else:
120                 raise Exception, "Required report does not exist: %s" % r
121
122         return new_report
123
124     def render_report(self, cr, uid, res_ids, name, data, context=None):
125         """
126         Look up a report definition and render the report for the provided IDs.
127         """
128         new_report = self._lookup_report(cr, name)
129         return new_report.create(cr, uid, res_ids, data, context)
130
131     _name = 'ir.actions.report.xml'
132     _inherit = 'ir.actions.actions'
133     _table = 'ir_act_report_xml'
134     _sequence = 'ir_actions_id_seq'
135     _order = 'name'
136     _columns = {
137         'name': fields.char('Name', size=64, required=True, translate=True),
138         'model': fields.char('Object', size=64, required=True),
139         'type': fields.char('Action Type', size=32, required=True),
140         'report_name': fields.char('Service Name', size=64, required=True),
141         'usage': fields.char('Action Usage', size=32),
142         'report_type': fields.char('Report Type', size=32, required=True, help="Report Type, e.g. pdf, html, raw, sxw, odt, html2html, mako2html, ..."),
143         'groups_id': fields.many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', 'Groups'),
144         '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."),
145         '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.'),
146         '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.'),
147         'auto': fields.boolean('Custom Python Parser'),
148
149         'header': fields.boolean('Add RML Header', help="Add or not the corporate RML header"),
150
151         'report_xsl': fields.char('XSL Path', size=256),
152         'report_xml': fields.char('XML Path', size=256, help=''),
153
154         # Pending deprecation... to be replaced by report_file as this object will become the default report object (not so specific to RML anymore)
155         'report_rml': fields.char('Main Report File Path', size=256, help="The path to the main report file (depending on Report Type) or NULL if the content is in another data field"),
156         # temporary related field as report_rml is pending deprecation - this field will replace report_rml after v6.0
157         'report_file': fields.related('report_rml', type="char", size=256, 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),
158
159         'report_sxw': fields.function(_report_sxw, type='char', string='SXW Path'),
160         'report_sxw_content_data': fields.binary('SXW Content'),
161         'report_rml_content_data': fields.binary('RML Content'),
162         'report_sxw_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='SXW Content',),
163         'report_rml_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='RML Content'),
164
165         'parser': fields.char('Parser Class'),
166     }
167     _defaults = {
168         'type': 'ir.actions.report.xml',
169         'multi': False,
170         'auto': True,
171         'header': True,
172         'report_sxw_content': False,
173         'report_type': 'pdf',
174         'attachment': False,
175     }
176
177 report_xml()
178
179 class act_window(osv.osv):
180     _name = 'ir.actions.act_window'
181     _table = 'ir_act_window'
182     _inherit = 'ir.actions.actions'
183     _sequence = 'ir_actions_id_seq'
184     _order = 'name'
185
186     def _check_model(self, cr, uid, ids, context=None):
187         for action in self.browse(cr, uid, ids, context):
188             if not self.pool.get(action.res_model):
189                 return False
190             if action.src_model and not self.pool.get(action.src_model):
191                 return False
192         return True
193
194     def _invalid_model_msg(self, cr, uid, ids, context=None):
195         return _('Invalid model name in the action definition.')
196
197     _constraints = [
198         (_check_model, _invalid_model_msg, ['res_model','src_model'])
199     ]
200
201     def _views_get_fnc(self, cr, uid, ids, name, arg, context=None):
202         """Returns an ordered list of the specific view modes that should be
203            enabled when displaying the result of this action, along with the
204            ID of the specific view to use for each mode, if any were required.
205
206            This function hides the logic of determining the precedence between
207            the view_modes string, the view_ids o2m, and the view_id m2o that can
208            be set on the action.
209
210            :rtype: dict in the form { action_id: list of pairs (tuples) }
211            :return: { action_id: [(view_id, view_mode), ...], ... }, where view_mode
212                     is one of the possible values for ir.ui.view.type and view_id
213                     is the ID of a specific view to use for this mode, or False for
214                     the default one.
215         """
216         res = {}
217         for act in self.browse(cr, uid, ids):
218             res[act.id] = [(view.view_id.id, view.view_mode) for view in act.view_ids]
219             view_ids_modes = [view.view_mode for view in act.view_ids]
220             modes = act.view_mode.split(',')
221             missing_modes = [mode for mode in modes if mode not in view_ids_modes]
222             if missing_modes:
223                 if act.view_id and act.view_id.type in missing_modes:
224                     # reorder missing modes to put view_id first if present
225                     missing_modes.remove(act.view_id.type)
226                     res[act.id].append((act.view_id.id, act.view_id.type))
227                 res[act.id].extend([(False, mode) for mode in missing_modes])
228         return res
229
230     def _search_view(self, cr, uid, ids, name, arg, context=None):
231         res = {}
232         for act in self.browse(cr, uid, ids, context=context):
233             field_get = self.pool[act.res_model].fields_view_get(cr, uid,
234                 act.search_view_id and act.search_view_id.id or False,
235                 'search', context=context)
236             res[act.id] = str(field_get)
237         return res
238
239     _columns = {
240         'name': fields.char('Action Name', size=64, translate=True),
241         'type': fields.char('Action Type', size=32, required=True),
242         'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='cascade'),
243         'domain': fields.char('Domain Value',
244             help="Optional domain filtering of the destination data, as a Python expression"),
245         'context': fields.char('Context Value', required=True,
246             help="Context dictionary as Python expression, empty by default (Default: {})"),
247         'res_id': fields.integer('Record ID', help="Database ID of record to open in form view, when ``view_mode`` is set to 'form' only"),
248         'res_model': fields.char('Destination Model', size=64, required=True,
249             help="Model name of the object to open in the view window"),
250         'src_model': fields.char('Source Model', size=64,
251             help="Optional model name of the objects on which this action should be visible"),
252         'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline Edit'),('inlineview','Inline View')], 'Target Window'),
253         'view_mode': fields.char('View Mode', size=250, required=True,
254             help="Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)"),
255         'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
256             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"),
257         'usage': fields.char('Action Usage', size=32,
258             help="Used to filter menu and home actions from the user form."),
259         'view_ids': fields.one2many('ir.actions.act_window.view', 'act_window_id', 'Views'),
260         'views': fields.function(_views_get_fnc, type='binary', string='Views',
261                help="This function field computes the ordered list of views that should be enabled " \
262                     "when displaying the result of an action, federating view mode, views and " \
263                     "reference view. The result is returned as an ordered list of pairs (view_id,view_mode)."),
264         'limit': fields.integer('Limit', help='Default limit for the list view'),
265         'auto_refresh': fields.integer('Auto-Refresh',
266             help='Add an auto-refresh on the view'),
267         'groups_id': fields.many2many('res.groups', 'ir_act_window_group_rel',
268             'act_id', 'gid', 'Groups'),
269         'search_view_id': fields.many2one('ir.ui.view', 'Search View Ref.'),
270         'filter': fields.boolean('Filter'),
271         'auto_search':fields.boolean('Auto Search'),
272         'search_view' : fields.function(_search_view, type='text', string='Search View'),
273         'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view"),
274     }
275
276     _defaults = {
277         'type': 'ir.actions.act_window',
278         'view_type': 'form',
279         'view_mode': 'tree,form',
280         'context': '{}',
281         'limit': 80,
282         'target': 'current',
283         'auto_refresh': 0,
284         'auto_search':True,
285         'multi': False,
286     }
287
288     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
289         """ call the method get_empty_list_help of the model and set the window action help message
290         """
291         ids_int = isinstance(ids, (int, long))
292         if ids_int:
293             ids = [ids]
294         results = super(act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load)
295
296         if not fields or 'help' in fields:
297             context = dict(context or {})
298             eval_dict = {
299                 'active_model': context.get('active_model'),
300                 'active_id': context.get('active_id'),
301                 'active_ids': context.get('active_ids'),
302                 'uid': uid,
303             }
304             for res in results:
305                 model = res.get('res_model')
306                 if model and self.pool.get(model):
307                     try:
308                         with tools.mute_logger("openerp.tools.safe_eval"):
309                             eval_context = eval(res['context'] or "{}", eval_dict) or {}
310                     except Exception:
311                         continue
312                     custom_context = dict(context, **eval_context)
313                     res['help'] = self.pool.get(model).get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context)
314         if ids_int:
315             return results[0]
316         return results
317
318     def for_xml_id(self, cr, uid, module, xml_id, context=None):
319         """ Returns the act_window object created for the provided xml_id
320
321         :param module: the module the act_window originates in
322         :param xml_id: the namespace-less id of the action (the @id
323                        attribute from the XML file)
324         :return: A read() view of the ir.actions.act_window
325         """
326         dataobj = self.pool.get('ir.model.data')
327         data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
328         res_id = dataobj.browse(cr, uid, data_id, context).res_id
329         return self.read(cr, uid, res_id, [], context)
330
331 act_window()
332
333 VIEW_TYPES = [
334     ('tree', 'Tree'),
335     ('form', 'Form'),
336     ('graph', 'Graph'),
337     ('calendar', 'Calendar'),
338     ('gantt', 'Gantt'),
339     ('kanban', 'Kanban')]
340 class act_window_view(osv.osv):
341     _name = 'ir.actions.act_window.view'
342     _table = 'ir_act_window_view'
343     _rec_name = 'view_id'
344     _order = 'sequence'
345     _columns = {
346         'sequence': fields.integer('Sequence'),
347         'view_id': fields.many2one('ir.ui.view', 'View'),
348         'view_mode': fields.selection(VIEW_TYPES, string='View Type', required=True),
349         'act_window_id': fields.many2one('ir.actions.act_window', 'Action', ondelete='cascade'),
350         'multi': fields.boolean('On Multiple Doc.',
351             help="If set to true, the action will not be displayed on the right toolbar of a form view."),
352     }
353     _defaults = {
354         'multi': False,
355     }
356     def _auto_init(self, cr, context=None):
357         super(act_window_view, self)._auto_init(cr, context)
358         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
359         if not cr.fetchone():
360             cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
361 act_window_view()
362
363 class act_wizard(osv.osv):
364     _name = 'ir.actions.wizard'
365     _inherit = 'ir.actions.actions'
366     _table = 'ir_act_wizard'
367     _sequence = 'ir_actions_id_seq'
368     _order = 'name'
369     _columns = {
370         'name': fields.char('Wizard Info', size=64, required=True, translate=True),
371         'type': fields.char('Action Type', size=32, required=True),
372         'wiz_name': fields.char('Wizard Name', size=64, required=True),
373         'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the wizard will not be displayed on the right toolbar of a form view."),
374         'groups_id': fields.many2many('res.groups', 'res_groups_wizard_rel', 'uid', 'gid', 'Groups'),
375         'model': fields.char('Object', size=64),
376     }
377     _defaults = {
378         'type': 'ir.actions.wizard',
379         'multi': False,
380     }
381 act_wizard()
382
383 class act_url(osv.osv):
384     _name = 'ir.actions.act_url'
385     _table = 'ir_act_url'
386     _inherit = 'ir.actions.actions'
387     _sequence = 'ir_actions_id_seq'
388     _order = 'name'
389     _columns = {
390         'name': fields.char('Action Name', size=64, translate=True),
391         'type': fields.char('Action Type', size=32, required=True),
392         'url': fields.text('Action URL',required=True),
393         'target': fields.selection((
394             ('new', 'New Window'),
395             ('self', 'This Window')),
396             'Action Target', required=True
397         )
398     }
399     _defaults = {
400         'type': 'ir.actions.act_url',
401         'target': 'new'
402     }
403 act_url()
404
405 def model_get(self, cr, uid, context=None):
406     wkf_pool = self.pool.get('workflow')
407     ids = wkf_pool.search(cr, uid, [])
408     osvs = wkf_pool.read(cr, uid, ids, ['osv'])
409
410     res = []
411     mpool = self.pool.get('ir.model')
412     for osv in osvs:
413         model = osv.get('osv')
414         id = mpool.search(cr, uid, [('model','=',model)])
415         name = mpool.read(cr, uid, id)[0]['name']
416         res.append((model, name))
417
418     return res
419
420 class ir_model_fields(osv.osv):
421     _inherit = 'ir.model.fields'
422     _rec_name = 'field_description'
423     _columns = {
424         'complete_name': fields.char('Complete Name', size=64, select=1),
425     }
426 ir_model_fields()
427
428 class server_object_lines(osv.osv):
429     _name = 'ir.server.object.lines'
430     _sequence = 'ir_actions_id_seq'
431     _columns = {
432         'server_id': fields.many2one('ir.actions.server', 'Object Mapping'),
433         'col1': fields.many2one('ir.model.fields', 'Destination', required=True),
434         'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
435                                                           "When Formula type is selected, this field may be a Python expression "
436                                                           " that can use the same values as for the condition field on the server action.\n"
437                                                           "If Value type is selected, the value will be used directly without evaluation."),
438         'type': fields.selection([
439             ('value','Value'),
440             ('equation','Formula')
441         ], 'Type', required=True, size=32, change_default=True),
442     }
443     _defaults = {
444         'type': 'equation',
445     }
446 server_object_lines()
447
448 ##
449 # Actions that are run on the server side
450 #
451 class actions_server(osv.osv):
452
453     def _select_signals(self, cr, uid, context=None):
454         cr.execute("""SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t
455                       WHERE w.id = a.wkf_id AND
456                             (t.act_from = a.id OR t.act_to = a.id) AND
457                             t.signal IS NOT NULL""")
458         result = cr.fetchall() or []
459         res = []
460         for rs in result:
461             if rs[0] is not None and rs[1] is not None:
462                 line = rs[1], "%s - (%s)" % (rs[1], rs[0])
463                 res.append(line)
464         return res
465
466     def _select_objects(self, cr, uid, context=None):
467         model_pool = self.pool.get('ir.model')
468         ids = model_pool.search(cr, uid, [('name','not ilike','.')])
469         res = model_pool.read(cr, uid, ids, ['model', 'name'])
470         return [(r['model'], r['name']) for r in res] +  [('','')]
471
472     def change_object(self, cr, uid, ids, copy_object, state, context=None):
473         if state == 'object_copy' and copy_object:
474             if context is None:
475                 context = {}
476             model_pool = self.pool.get('ir.model')
477             model = copy_object.split(',')[0]
478             mid = model_pool.search(cr, uid, [('model','=',model)])
479             return {
480                 'value': {'srcmodel_id': mid[0]},
481                 'context': context
482             }
483         else:
484             return {}
485
486     _name = 'ir.actions.server'
487     _table = 'ir_act_server'
488     _inherit = 'ir.actions.actions'
489     _sequence = 'ir_actions_id_seq'
490     _order = 'sequence,name'
491     _columns = {
492         'name': fields.char('Action Name', required=True, size=64, translate=True),
493         'condition' : fields.char('Condition', size=256, required=True,
494                                   help="Condition that is tested before the action is executed, "
495                                        "and prevent execution if it is not verified.\n"
496                                        "Example: object.list_price > 5000\n"
497                                        "It is a Python expression that can use the following values:\n"
498                                        " - self: ORM model of the record on which the action is triggered\n"
499                                        " - object or obj: browse_record of the record on which the action is triggered\n"
500                                        " - pool: ORM model pool (i.e. self.pool)\n"
501                                        " - time: Python time module\n"
502                                        " - cr: database cursor\n"
503                                        " - uid: current user id\n"
504                                        " - context: current context"),
505         'state': fields.selection([
506             ('client_action','Client Action'),
507             ('dummy','Dummy'),
508             ('loop','Iteration'),
509             ('code','Python Code'),
510             ('trigger','Trigger'),
511             ('email','Email'),
512             ('sms','SMS'),
513             ('object_create','Create Object'),
514             ('object_copy','Copy Object'),
515             ('object_write','Write Object'),
516             ('other','Multi Actions'),
517         ], 'Action Type', required=True, size=32, help="Type of the Action that is to be executed"),
518         'code':fields.text('Python Code', help="Python code to be executed if condition is met.\n"
519                                                "It is a Python block that can use the same values as for the condition field"),
520         'sequence': fields.integer('Sequence', help="Important when you deal with multiple actions, the execution order will be decided based on this, low number is higher priority."),
521         'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create).", ondelete='cascade'),
522         'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."),
523         'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
524         'wkf_model_id': fields.many2one('ir.model', 'Target Object', help="The object that should receive the workflow signal (must have an associated workflow)"),
525         'trigger_obj_id': fields.many2one('ir.model.fields','Relation Field', 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)"),
526         'email': fields.char('Email Address', size=512, help="Expression that returns the email address to send to. Can be based on the same values as for the condition field.\n"
527                                                              "Example: object.invoice_address_id.email, or 'me@example.com'"),
528         'subject': fields.char('Subject', size=1024, translate=True, help="Email subject, may contain expressions enclosed in double brackets based on the same values as those "
529                                                                           "available in the condition field, e.g. `Hello [[ object.partner_id.name ]]`"),
530         'message': fields.text('Message', translate=True, help="Email contents, may contain expressions enclosed in double brackets based on the same values as those "
531                                                                           "available in the condition field, e.g. `Dear [[ object.partner_id.name ]]`"),
532         'mobile': fields.char('Mobile No', size=512, help="Provides fields that be used to fetch the mobile number, e.g. you select the invoice, then `object.invoice_address_id.mobile` is the field which gives the correct mobile number"),
533         'sms': fields.char('SMS', size=160, translate=True),
534         'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions', 'server_id', 'action_id', 'Other Actions'),
535         'usage': fields.char('Action Usage', size=32),
536         'type': fields.char('Action Type', size=32, required=True),
537         'srcmodel_id': fields.many2one('ir.model', 'Model', help="Object in which you want to create / write the object. If it is empty then refer to the Object field."),
538         'fields_lines': fields.one2many('ir.server.object.lines', 'server_id', 'Field Mappings.'),
539         'record_id':fields.many2one('ir.model.fields', 'Create Id', help="Provide the field name where the record id is stored after the create operations. If it is empty, you can not track the new record."),
540         'write_id':fields.char('Write Id', size=256, help="Provide the field name that the record id refers to for the write operation. If it is empty it will refer to the active id of the object."),
541         'loop_action':fields.many2one('ir.actions.server', 'Loop Action', help="Select the action that will be executed. Loop action will not be avaliable inside loop."),
542         'expression':fields.char('Loop Expression', size=512, help="Enter the field/expression that will return the list. E.g. select the sale order in Object, and you can have loop on the sales order line. Expression = `object.order_line`."),
543         'copy_object': fields.reference('Copy Of', selection=_select_objects, size=256),
544     }
545     _defaults = {
546         'state': 'dummy',
547         'condition': 'True',
548         'type': 'ir.actions.server',
549         'sequence': 5,
550         'code': """# You can use the following variables:
551 #  - self: ORM model of the record on which the action is triggered
552 #  - object: browse_record of the record on which the action is triggered if there is one, otherwise None
553 #  - pool: ORM model pool (i.e. self.pool)
554 #  - time: Python time module
555 #  - cr: database cursor
556 #  - uid: current user id
557 #  - context: current context
558 # If you plan to return an action, assign: action = {...}
559 """,
560     }
561
562     def get_email(self, cr, uid, action, context):
563         obj_pool = self.pool.get(action.model_id.model)
564         id = context.get('active_id')
565         obj = obj_pool.browse(cr, uid, id)
566
567         fields = None
568
569         if '/' in action.email.complete_name:
570             fields = action.email.complete_name.split('/')
571         elif '.' in action.email.complete_name:
572             fields = action.email.complete_name.split('.')
573
574         for field in fields:
575             try:
576                 obj = getattr(obj, field)
577             except Exception:
578                 _logger.exception('Failed to parse: %s', field)
579
580         return obj
581
582     def get_mobile(self, cr, uid, action, context):
583         obj_pool = self.pool.get(action.model_id.model)
584         id = context.get('active_id')
585         obj = obj_pool.browse(cr, uid, id)
586
587         fields = None
588
589         if '/' in action.mobile.complete_name:
590             fields = action.mobile.complete_name.split('/')
591         elif '.' in action.mobile.complete_name:
592             fields = action.mobile.complete_name.split('.')
593
594         for field in fields:
595             try:
596                 obj = getattr(obj, field)
597             except Exception:
598                 _logger.exception('Failed to parse: %s', field)
599
600         return obj
601
602     def merge_message(self, cr, uid, keystr, action, context=None):
603         if context is None:
604             context = {}
605
606         def merge(match):
607             obj_pool = self.pool.get(action.model_id.model)
608             id = context.get('active_id')
609             obj = obj_pool.browse(cr, uid, id)
610             exp = str(match.group()[2:-2]).strip()
611             result = eval(exp,
612                           {
613                             'object': obj,
614                             'context': dict(context), # copy context to prevent side-effects of eval
615                             'time': time,
616                           })
617             if result in (None, False):
618                 return str("--------")
619             return tools.ustr(result)
620
621         com = re.compile('(\[\[.+?\]\])')
622         message = com.sub(merge, keystr)
623
624         return message
625
626     # Context should contains:
627     #   ids : original ids
628     #   id  : current id of the object
629     # OUT:
630     #   False : Finished correctly
631     #   ACTION_ID : Action to launch
632
633     # FIXME: refactor all the eval() calls in run()!
634     def run(self, cr, uid, ids, context=None):
635         if context is None:
636             context = {}
637         user = self.pool.get('res.users').browse(cr, uid, uid)
638         for action in self.browse(cr, uid, ids, context):
639             obj = None
640             obj_pool = self.pool.get(action.model_id.model)
641             if context.get('active_model') == action.model_id.model and context.get('active_id'):
642                 obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
643             cxt = {
644                 'self': obj_pool,
645                 'object': obj,
646                 'obj': obj,
647                 'pool': self.pool,
648                 'time': time,
649                 'cr': cr,
650                 'context': dict(context), # copy context to prevent side-effects of eval
651                 'uid': uid,
652                 'user': user
653             }
654             expr = eval(str(action.condition), cxt)
655             if not expr:
656                 continue
657
658             if action.state=='client_action':
659                 if not action.action_id:
660                     raise osv.except_osv(_('Error'), _("Please specify an action to launch !"))
661                 return self.pool.get(action.action_id.type)\
662                     .read(cr, uid, action.action_id.id, context=context)
663
664             if action.state=='code':
665                 eval(action.code.strip(), cxt, mode="exec", nocopy=True) # nocopy allows to return 'action'
666                 if 'action' in cxt:
667                     return cxt['action']
668
669             if action.state == 'email':
670                 email_from = config['email_from']
671                 if not email_from:
672                     _logger.debug('--email-from command line option is not specified, using a fallback value instead.')
673                     if user.email:
674                         email_from = user.email
675                     else:
676                         email_from = "%s@%s" % (user.login, gethostname())
677
678                 try:
679                     address = eval(str(action.email), cxt)
680                 except Exception:
681                     address = str(action.email)
682
683                 if not address:
684                     _logger.info('No partner email address specified, not sending any email.')
685                     continue
686
687                 # handle single and multiple recipient addresses
688                 addresses = address if isinstance(address, (tuple, list)) else [address]
689                 subject = self.merge_message(cr, uid, action.subject, action, context)
690                 body = self.merge_message(cr, uid, action.message, action, context)
691
692                 ir_mail_server = self.pool.get('ir.mail_server')
693                 msg = ir_mail_server.build_email(email_from, addresses, subject, body)
694                 res_email = ir_mail_server.send_email(cr, uid, msg)
695                 if res_email:
696                     _logger.info('Email successfully sent to: %s', addresses)
697                 else:
698                     _logger.warning('Failed to send email to: %s', addresses)
699
700             if action.state == 'trigger':
701                 model = action.wkf_model_id.model
702                 m2o_field_name = action.trigger_obj_id.name
703                 target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
704                 target_id = target_id[0] if isinstance(target_id,tuple) else target_id
705                 openerp.workflow.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
706
707             if action.state == 'sms':
708                 #TODO: set the user and password from the system
709                 # for the sms gateway user / password
710                 # USE smsclient module from extra-addons
711                 _logger.warning('SMS Facility has not been implemented yet. Use smsclient module!')
712
713             if action.state == 'other':
714                 res = []
715                 for act in action.child_ids:
716                     context['active_id'] = context['active_ids'][0]
717                     result = self.run(cr, uid, [act.id], context)
718                     if result:
719                         res.append(result)
720                 return res
721
722             if action.state == 'loop':
723                 expr = eval(str(action.expression), cxt)
724                 context['object'] = obj
725                 for i in expr:
726                     context['active_id'] = i.id
727                     self.run(cr, uid, [action.loop_action.id], context)
728
729             if action.state == 'object_write':
730                 res = {}
731                 for exp in action.fields_lines:
732                     euq = exp.value
733                     if exp.type == 'equation':
734                         expr = eval(euq, cxt)
735                     else:
736                         expr = exp.value
737                     res[exp.col1.name] = expr
738
739                 if not action.write_id:
740                     if not action.srcmodel_id:
741                         obj_pool = self.pool.get(action.model_id.model)
742                         obj_pool.write(cr, uid, [context.get('active_id')], res)
743                     else:
744                         write_id = context.get('active_id')
745                         obj_pool = self.pool.get(action.srcmodel_id.model)
746                         obj_pool.write(cr, uid, [write_id], res)
747
748                 elif action.write_id:
749                     obj_pool = self.pool.get(action.srcmodel_id.model)
750                     rec = self.pool.get(action.model_id.model).browse(cr, uid, context.get('active_id'))
751                     id = eval(action.write_id, {'object': rec})
752                     try:
753                         id = int(id)
754                     except:
755                         raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
756
757                     if type(id) != type(1):
758                         raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
759                     write_id = id
760                     obj_pool.write(cr, uid, [write_id], res)
761
762             if action.state == 'object_create':
763                 res = {}
764                 for exp in action.fields_lines:
765                     euq = exp.value
766                     if exp.type == 'equation':
767                         expr = eval(euq, cxt)
768                     else:
769                         expr = exp.value
770                     res[exp.col1.name] = expr
771
772                 obj_pool = self.pool.get(action.srcmodel_id.model)
773                 res_id = obj_pool.create(cr, uid, res)
774                 if action.record_id:
775                     self.pool.get(action.model_id.model).write(cr, uid, [context.get('active_id')], {action.record_id.name:res_id})
776
777             if action.state == 'object_copy':
778                 res = {}
779                 for exp in action.fields_lines:
780                     euq = exp.value
781                     if exp.type == 'equation':
782                         expr = eval(euq, cxt)
783                     else:
784                         expr = exp.value
785                     res[exp.col1.name] = expr
786
787                 model = action.copy_object.split(',')[0]
788                 cid = action.copy_object.split(',')[1]
789                 obj_pool = self.pool.get(model)
790                 obj_pool.copy(cr, uid, int(cid), res)
791
792         return False
793
794 actions_server()
795
796 class act_window_close(osv.osv):
797     _name = 'ir.actions.act_window_close'
798     _inherit = 'ir.actions.actions'
799     _table = 'ir_actions'
800     _defaults = {
801         'type': 'ir.actions.act_window_close',
802     }
803 act_window_close()
804
805 # This model use to register action services.
806 TODO_STATES = [('open', 'To Do'),
807                ('done', 'Done')]
808 TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
809               ('automatic', 'Launch Automatically')]
810 class ir_actions_todo(osv.osv):
811     """
812     Configuration Wizards
813     """
814     _name = 'ir.actions.todo'
815     _description = "Configuration Wizards"
816     _columns={
817         'action_id': fields.many2one(
818             'ir.actions.actions', 'Action', select=True, required=True),
819         'sequence': fields.integer('Sequence'),
820         'state': fields.selection(TODO_STATES, string='Status', required=True),
821         'name': fields.char('Name', size=64),
822         'type': fields.selection(TODO_TYPES, 'Type', required=True,
823             help="""Manual: Launched manually.
824 Automatic: Runs whenever the system is reconfigured.
825 Launch Manually Once: after having been launched manually, it sets automatically to Done."""),
826         'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
827         'note': fields.text('Text', translate=True),
828     }
829     _defaults={
830         'state': 'open',
831         'sequence': 10,
832         'type': 'manual',
833     }
834     _order="sequence,id"
835
836     def action_launch(self, cr, uid, ids, context=None):
837         """ Launch Action of Wizard"""
838         wizard_id = ids and ids[0] or False
839         wizard = self.browse(cr, uid, wizard_id, context=context)
840         if wizard.type in ('automatic', 'once'):
841             wizard.write({'state': 'done'})
842
843         # Load action
844         act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
845
846         res = self.pool.get(act_type['type']).read(cr, uid, wizard.action_id.id, [], context=context)
847         if act_type['type'] != 'ir.actions.act_window':
848             return res
849         res.setdefault('context','{}')
850         res['nodestroy'] = True
851
852         # Open a specific record when res_id is provided in the context
853         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
854         ctx = eval(res['context'], {'user': user})
855         if ctx.get('res_id'):
856             res.update({'res_id': ctx.pop('res_id')})
857
858         # disable log for automatic wizards
859         if wizard.type == 'automatic':
860             ctx.update({'disable_log': True})
861         res.update({'context': ctx})
862
863         return res
864
865     def action_open(self, cr, uid, ids, context=None):
866         """ Sets configuration wizard in TODO state"""
867         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
868
869     def progress(self, cr, uid, context=None):
870         """ Returns a dict with 3 keys {todo, done, total}.
871
872         These keys all map to integers and provide the number of todos
873         marked as open, the total number of todos and the number of
874         todos not open (which is basically a shortcut to total-todo)
875
876         :rtype: dict
877         """
878         user_groups = set(map(
879             lambda x: x.id,
880             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
881         def groups_match(todo):
882             """ Checks if the todo's groups match those of the current user
883             """
884             return not todo.groups_id \
885                    or bool(user_groups.intersection((
886                         group.id for group in todo.groups_id)))
887
888         done = filter(
889             groups_match,
890             self.browse(cr, uid,
891                 self.search(cr, uid, [('state', '!=', 'open')], context=context),
892                         context=context))
893
894         total = filter(
895             groups_match,
896             self.browse(cr, uid,
897                 self.search(cr, uid, [], context=context),
898                         context=context))
899
900         return {
901             'done': len(done),
902             'total': len(total),
903             'todo': len(total) - len(done)
904         }
905
906 ir_actions_todo()
907
908 class act_client(osv.osv):
909     _name = 'ir.actions.client'
910     _inherit = 'ir.actions.actions'
911     _table = 'ir_act_client'
912     _sequence = 'ir_actions_id_seq'
913     _order = 'name'
914
915     def _get_params(self, cr, uid, ids, field_name, arg, context):
916         result = {}
917         for record in self.browse(cr, uid, ids, context=context):
918             result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
919         return result
920
921     def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
922         if isinstance(field_value, dict):
923             self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
924         else:
925             self.write(cr, uid, id, {'params_store': field_value}, context=context)
926
927     _columns = {
928         'name': fields.char('Action Name', required=True, size=64, translate=True),
929         'tag': fields.char('Client action tag', size=64, required=True,
930                            help="An arbitrary string, interpreted by the client"
931                                 " according to its own needs and wishes. There "
932                                 "is no central tag repository across clients."),
933         'res_model': fields.char('Destination Model', size=64, 
934             help="Optional model, mostly used for needactions."),
935         'context': fields.char('Context Value', size=250, required=True,
936             help="Context dictionary as Python expression, empty by default (Default: {})"),
937         'params': fields.function(_get_params, fnct_inv=_set_params,
938                                   type='binary', 
939                                   string="Supplementary arguments",
940                                   help="Arguments sent to the client along with"
941                                        "the view tag"),
942         'params_store': fields.binary("Params storage", readonly=True)
943     }
944     _defaults = {
945         'type': 'ir.actions.client',
946         'context': '{}',
947
948     }
949 act_client()
950
951 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: