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