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