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