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