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