[MERGE] from 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 action.res_model not in self.pool:
189                 return False
190             if action.src_model and action.src_model not in self.pool:
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[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[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[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[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[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
662
663             if action.state=='code':
664                 eval(action.code.strip(), cxt, mode="exec", nocopy=True) # nocopy allows to return 'action'
665                 if 'action' in cxt:
666                     return cxt['action']
667
668             if action.state == 'email':
669                 email_from = config['email_from']
670                 if not email_from:
671                     _logger.debug('--email-from command line option is not specified, using a fallback value instead.')
672                     if user.email:
673                         email_from = user.email
674                     else:
675                         email_from = "%s@%s" % (user.login, gethostname())
676
677                 try:
678                     address = eval(str(action.email), cxt)
679                 except Exception:
680                     address = str(action.email)
681
682                 if not address:
683                     _logger.info('No partner email address specified, not sending any email.')
684                     continue
685
686                 # handle single and multiple recipient addresses
687                 addresses = address if isinstance(address, (tuple, list)) else [address]
688                 subject = self.merge_message(cr, uid, action.subject, action, context)
689                 body = self.merge_message(cr, uid, action.message, action, context)
690
691                 ir_mail_server = self.pool.get('ir.mail_server')
692                 msg = ir_mail_server.build_email(email_from, addresses, subject, body)
693                 res_email = ir_mail_server.send_email(cr, uid, msg)
694                 if res_email:
695                     _logger.info('Email successfully sent to: %s', addresses)
696                 else:
697                     _logger.warning('Failed to send email to: %s', addresses)
698
699             if action.state == 'trigger':
700                 model = action.wkf_model_id.model
701                 m2o_field_name = action.trigger_obj_id.name
702                 target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
703                 target_id = target_id[0] if isinstance(target_id,tuple) else target_id
704                 openerp.workflow.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
705
706             if action.state == 'sms':
707                 #TODO: set the user and password from the system
708                 # for the sms gateway user / password
709                 # USE smsclient module from extra-addons
710                 _logger.warning('SMS Facility has not been implemented yet. Use smsclient module!')
711
712             if action.state == 'other':
713                 res = []
714                 for act in action.child_ids:
715                     context['active_id'] = context['active_ids'][0]
716                     result = self.run(cr, uid, [act.id], context)
717                     if result:
718                         res.append(result)
719                 return res
720
721             if action.state == 'loop':
722                 expr = eval(str(action.expression), cxt)
723                 context['object'] = obj
724                 for i in expr:
725                     context['active_id'] = i.id
726                     self.run(cr, uid, [action.loop_action.id], context)
727
728             if action.state == 'object_write':
729                 res = {}
730                 for exp in action.fields_lines:
731                     euq = exp.value
732                     if exp.type == 'equation':
733                         expr = eval(euq, cxt)
734                     else:
735                         expr = exp.value
736                     res[exp.col1.name] = expr
737
738                 if not action.write_id:
739                     if not action.srcmodel_id:
740                         obj_pool = self.pool[action.model_id.model]
741                         obj_pool.write(cr, uid, [context.get('active_id')], res)
742                     else:
743                         write_id = context.get('active_id')
744                         obj_pool = self.pool[action.srcmodel_id.model]
745                         obj_pool.write(cr, uid, [write_id], res)
746
747                 elif action.write_id:
748                     obj_pool = self.pool[action.srcmodel_id.model]
749                     rec = self.pool[action.model_id.model].browse(cr, uid, context.get('active_id'))
750                     id = eval(action.write_id, {'object': rec})
751                     try:
752                         id = int(id)
753                     except:
754                         raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
755
756                     if type(id) != type(1):
757                         raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
758                     write_id = id
759                     obj_pool.write(cr, uid, [write_id], res)
760
761             if action.state == 'object_create':
762                 res = {}
763                 for exp in action.fields_lines:
764                     euq = exp.value
765                     if exp.type == 'equation':
766                         expr = eval(euq, cxt)
767                     else:
768                         expr = exp.value
769                     res[exp.col1.name] = expr
770
771                 obj_pool = self.pool[action.srcmodel_id.model]
772                 res_id = obj_pool.create(cr, uid, res)
773                 if action.record_id:
774                     self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.record_id.name:res_id})
775
776             if action.state == 'object_copy':
777                 res = {}
778                 for exp in action.fields_lines:
779                     euq = exp.value
780                     if exp.type == 'equation':
781                         expr = eval(euq, cxt)
782                     else:
783                         expr = exp.value
784                     res[exp.col1.name] = expr
785
786                 model = action.copy_object.split(',')[0]
787                 cid = action.copy_object.split(',')[1]
788                 obj_pool = self.pool[model]
789                 obj_pool.copy(cr, uid, int(cid), res)
790
791         return False
792
793 actions_server()
794
795 class act_window_close(osv.osv):
796     _name = 'ir.actions.act_window_close'
797     _inherit = 'ir.actions.actions'
798     _table = 'ir_actions'
799     _defaults = {
800         'type': 'ir.actions.act_window_close',
801     }
802 act_window_close()
803
804 # This model use to register action services.
805 TODO_STATES = [('open', 'To Do'),
806                ('done', 'Done')]
807 TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
808               ('automatic', 'Launch Automatically')]
809 class ir_actions_todo(osv.osv):
810     """
811     Configuration Wizards
812     """
813     _name = 'ir.actions.todo'
814     _description = "Configuration Wizards"
815     _columns={
816         'action_id': fields.many2one(
817             'ir.actions.actions', 'Action', select=True, required=True),
818         'sequence': fields.integer('Sequence'),
819         'state': fields.selection(TODO_STATES, string='Status', required=True),
820         'name': fields.char('Name', size=64),
821         'type': fields.selection(TODO_TYPES, 'Type', required=True,
822             help="""Manual: Launched manually.
823 Automatic: Runs whenever the system is reconfigured.
824 Launch Manually Once: after having been launched manually, it sets automatically to Done."""),
825         'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
826         'note': fields.text('Text', translate=True),
827     }
828     _defaults={
829         'state': 'open',
830         'sequence': 10,
831         'type': 'manual',
832     }
833     _order="sequence,id"
834
835     def action_launch(self, cr, uid, ids, context=None):
836         """ Launch Action of Wizard"""
837         wizard_id = ids and ids[0] or False
838         wizard = self.browse(cr, uid, wizard_id, context=context)
839         if wizard.type in ('automatic', 'once'):
840             wizard.write({'state': 'done'})
841
842         # Load action
843         act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
844
845         res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context)
846         if act_type['type'] != 'ir.actions.act_window':
847             return res
848         res.setdefault('context','{}')
849         res['nodestroy'] = True
850
851         # Open a specific record when res_id is provided in the context
852         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
853         ctx = eval(res['context'], {'user': user})
854         if ctx.get('res_id'):
855             res.update({'res_id': ctx.pop('res_id')})
856
857         # disable log for automatic wizards
858         if wizard.type == 'automatic':
859             ctx.update({'disable_log': True})
860         res.update({'context': ctx})
861
862         return res
863
864     def action_open(self, cr, uid, ids, context=None):
865         """ Sets configuration wizard in TODO state"""
866         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
867
868     def progress(self, cr, uid, context=None):
869         """ Returns a dict with 3 keys {todo, done, total}.
870
871         These keys all map to integers and provide the number of todos
872         marked as open, the total number of todos and the number of
873         todos not open (which is basically a shortcut to total-todo)
874
875         :rtype: dict
876         """
877         user_groups = set(map(
878             lambda x: x.id,
879             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
880         def groups_match(todo):
881             """ Checks if the todo's groups match those of the current user
882             """
883             return not todo.groups_id \
884                    or bool(user_groups.intersection((
885                         group.id for group in todo.groups_id)))
886
887         done = filter(
888             groups_match,
889             self.browse(cr, uid,
890                 self.search(cr, uid, [('state', '!=', 'open')], context=context),
891                         context=context))
892
893         total = filter(
894             groups_match,
895             self.browse(cr, uid,
896                 self.search(cr, uid, [], context=context),
897                         context=context))
898
899         return {
900             'done': len(done),
901             'total': len(total),
902             'todo': len(total) - len(done)
903         }
904
905 ir_actions_todo()
906
907 class act_client(osv.osv):
908     _name = 'ir.actions.client'
909     _inherit = 'ir.actions.actions'
910     _table = 'ir_act_client'
911     _sequence = 'ir_actions_id_seq'
912     _order = 'name'
913
914     def _get_params(self, cr, uid, ids, field_name, arg, context):
915         result = {}
916         for record in self.browse(cr, uid, ids, context=context):
917             result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
918         return result
919
920     def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
921         if isinstance(field_value, dict):
922             self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
923         else:
924             self.write(cr, uid, id, {'params_store': field_value}, context=context)
925
926     _columns = {
927         'name': fields.char('Action Name', required=True, size=64, translate=True),
928         'tag': fields.char('Client action tag', size=64, required=True,
929                            help="An arbitrary string, interpreted by the client"
930                                 " according to its own needs and wishes. There "
931                                 "is no central tag repository across clients."),
932         'res_model': fields.char('Destination Model', size=64, 
933             help="Optional model, mostly used for needactions."),
934         'context': fields.char('Context Value', size=250, required=True,
935             help="Context dictionary as Python expression, empty by default (Default: {})"),
936         'params': fields.function(_get_params, fnct_inv=_set_params,
937                                   type='binary', 
938                                   string="Supplementary arguments",
939                                   help="Arguments sent to the client along with"
940                                        "the view tag"),
941         'params_store': fields.binary("Params storage", readonly=True)
942     }
943     _defaults = {
944         'type': 'ir.actions.client',
945         'context': '{}',
946
947     }
948 act_client()
949
950 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: