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