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