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