[FIX] Fixed active_ids / active_id management, wrong getter on active_id making serve...
[odoo/odoo.git] / openerp / addons / base / ir / ir_actions.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2011 OpenERP S.A. <http://www.openerp.com>
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import logging
23 import operator
24 import os
25 import time
26
27 import openerp
28 from openerp import SUPERUSER_ID
29 from openerp import tools
30 from openerp import workflow
31 from openerp.osv import fields, osv
32 from openerp.osv.orm import browse_record
33 import openerp.report.interface
34 from openerp.report.report_sxw import report_sxw, report_rml
35 from openerp.tools.safe_eval import safe_eval as eval
36 from openerp.tools.translate import _
37 import openerp.workflow
38
39 _logger = logging.getLogger(__name__)
40
41 class actions(osv.osv):
42     _name = 'ir.actions.actions'
43     _table = 'ir_actions'
44     _order = 'name'
45     _columns = {
46         'name': fields.char('Name', size=64, required=True),
47         'type': fields.char('Action Type', required=True, size=32),
48         'usage': fields.char('Action Usage', size=32),
49         'help': fields.text('Action description',
50             help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
51             translate=True),
52     }
53     _defaults = {
54         'usage': lambda *a: False,
55     }
56 actions()
57
58
59 class report_xml(osv.osv):
60
61     def _report_content(self, cursor, user, ids, name, arg, context=None):
62         res = {}
63         for report in self.browse(cursor, user, ids, context=context):
64             data = report[name + '_data']
65             if not data and report[name[:-8]]:
66                 fp = None
67                 try:
68                     fp = tools.file_open(report[name[:-8]], mode='rb')
69                     data = fp.read()
70                 except:
71                     data = False
72                 finally:
73                     if fp:
74                         fp.close()
75             res[report.id] = data
76         return res
77
78     def _report_content_inv(self, cursor, user, id, name, value, arg, context=None):
79         self.write(cursor, user, id, {name+'_data': value}, context=context)
80
81     def _report_sxw(self, cursor, user, ids, name, arg, context=None):
82         res = {}
83         for report in self.browse(cursor, user, ids, context=context):
84             if report.report_rml:
85                 res[report.id] = report.report_rml.replace('.rml', '.sxw')
86             else:
87                 res[report.id] = False
88         return res
89
90     def _lookup_report(self, cr, name):
91         """
92         Look up a report definition.
93         """
94         opj = os.path.join
95
96         # First lookup in the deprecated place, because if the report definition
97         # has not been updated, it is more likely the correct definition is there.
98         # Only reports with custom parser sepcified in Python are still there.
99         if 'report.' + name in openerp.report.interface.report_int._reports:
100             new_report = openerp.report.interface.report_int._reports['report.' + name]
101         else:
102             cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
103             r = cr.dictfetchone()
104             if r:
105                 if r['report_rml'] or r['report_rml_content_data']:
106                     if r['parser']:
107                         kwargs = { 'parser': operator.attrgetter(r['parser'])(openerp.addons) }
108                     else:
109                         kwargs = {}
110                     new_report = report_sxw('report.'+r['report_name'], r['model'],
111                             opj('addons',r['report_rml'] or '/'), header=r['header'], register=False, **kwargs)
112                 elif r['report_xsl']:
113                     new_report = report_rml('report.'+r['report_name'], r['model'],
114                             opj('addons',r['report_xml']),
115                             r['report_xsl'] and opj('addons',r['report_xsl']), register=False)
116                 else:
117                     raise Exception, "Unhandled report type: %s" % r
118             else:
119                 raise Exception, "Required report does not exist: %s" % r
120
121         return new_report
122
123     def render_report(self, cr, uid, res_ids, name, data, context=None):
124         """
125         Look up a report definition and render the report for the provided IDs.
126         """
127         new_report = self._lookup_report(cr, name)
128         return new_report.create(cr, uid, res_ids, data, context)
129
130     _name = 'ir.actions.report.xml'
131     _inherit = 'ir.actions.actions'
132     _table = 'ir_act_report_xml'
133     _sequence = 'ir_actions_id_seq'
134     _order = 'name'
135     _columns = {
136         'name': fields.char('Name', size=64, required=True, translate=True),
137         'model': fields.char('Object', size=64, required=True),
138         'type': fields.char('Action Type', size=32, required=True),
139         'report_name': fields.char('Service Name', size=64, required=True),
140         'usage': fields.char('Action Usage', size=32),
141         'report_type': fields.char('Report Type', size=32, required=True, help="Report Type, e.g. pdf, html, raw, sxw, odt, html2html, mako2html, ..."),
142         'groups_id': fields.many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', 'Groups'),
143         'multi': fields.boolean('On Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view."),
144         'attachment': fields.char('Save as Attachment Prefix', size=128, help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.'),
145         'attachment_use': fields.boolean('Reload from Attachment', help='If you check this, then the second time the user prints with same attachment name, it returns the previous report.'),
146         'auto': fields.boolean('Custom Python Parser'),
147
148         'header': fields.boolean('Add RML Header', help="Add or not the corporate RML header"),
149
150         'report_xsl': fields.char('XSL Path', size=256),
151         'report_xml': fields.char('XML Path', size=256, help=''),
152
153         # Pending deprecation... to be replaced by report_file as this object will become the default report object (not so specific to RML anymore)
154         'report_rml': fields.char('Main Report File Path', size=256, help="The path to the main report file (depending on Report Type) or NULL if the content is in another data field"),
155         # temporary related field as report_rml is pending deprecation - this field will replace report_rml after v6.0
156         'report_file': fields.related('report_rml', type="char", size=256, required=False, readonly=False, string='Report File', help="The path to the main report file (depending on Report Type) or NULL if the content is in another field", store=True),
157
158         'report_sxw': fields.function(_report_sxw, type='char', string='SXW Path'),
159         'report_sxw_content_data': fields.binary('SXW Content'),
160         'report_rml_content_data': fields.binary('RML Content'),
161         'report_sxw_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='SXW Content',),
162         'report_rml_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='RML Content'),
163
164         'parser': fields.char('Parser Class'),
165     }
166     _defaults = {
167         'type': 'ir.actions.report.xml',
168         'multi': False,
169         'auto': True,
170         'header': True,
171         'report_sxw_content': False,
172         'report_type': 'pdf',
173         'attachment': False,
174     }
175
176 report_xml()
177
178 class act_window(osv.osv):
179     _name = 'ir.actions.act_window'
180     _table = 'ir_act_window'
181     _inherit = 'ir.actions.actions'
182     _sequence = 'ir_actions_id_seq'
183     _order = 'name'
184
185     def _check_model(self, cr, uid, ids, context=None):
186         for action in self.browse(cr, uid, ids, context):
187             if action.res_model not in self.pool:
188                 return False
189             if action.src_model and action.src_model not in self.pool:
190                 return False
191         return True
192
193     def _invalid_model_msg(self, cr, uid, ids, context=None):
194         return _('Invalid model name in the action definition.')
195
196     _constraints = [
197         (_check_model, _invalid_model_msg, ['res_model','src_model'])
198     ]
199
200     def _views_get_fnc(self, cr, uid, ids, name, arg, context=None):
201         """Returns an ordered list of the specific view modes that should be
202            enabled when displaying the result of this action, along with the
203            ID of the specific view to use for each mode, if any were required.
204
205            This function hides the logic of determining the precedence between
206            the view_modes string, the view_ids o2m, and the view_id m2o that can
207            be set on the action.
208
209            :rtype: dict in the form { action_id: list of pairs (tuples) }
210            :return: { action_id: [(view_id, view_mode), ...], ... }, where view_mode
211                     is one of the possible values for ir.ui.view.type and view_id
212                     is the ID of a specific view to use for this mode, or False for
213                     the default one.
214         """
215         res = {}
216         for act in self.browse(cr, uid, ids):
217             res[act.id] = [(view.view_id.id, view.view_mode) for view in act.view_ids]
218             view_ids_modes = [view.view_mode for view in act.view_ids]
219             modes = act.view_mode.split(',')
220             missing_modes = [mode for mode in modes if mode not in view_ids_modes]
221             if missing_modes:
222                 if act.view_id and act.view_id.type in missing_modes:
223                     # reorder missing modes to put view_id first if present
224                     missing_modes.remove(act.view_id.type)
225                     res[act.id].append((act.view_id.id, act.view_id.type))
226                 res[act.id].extend([(False, mode) for mode in missing_modes])
227         return res
228
229     def _search_view(self, cr, uid, ids, name, arg, context=None):
230         res = {}
231         for act in self.browse(cr, uid, ids, context=context):
232             field_get = self.pool[act.res_model].fields_view_get(cr, uid,
233                 act.search_view_id and act.search_view_id.id or False,
234                 'search', context=context)
235             res[act.id] = str(field_get)
236         return res
237
238     _columns = {
239         'name': fields.char('Action Name', size=64, translate=True),
240         'type': fields.char('Action Type', size=32, required=True),
241         'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='cascade'),
242         'domain': fields.char('Domain Value',
243             help="Optional domain filtering of the destination data, as a Python expression"),
244         'context': fields.char('Context Value', required=True,
245             help="Context dictionary as Python expression, empty by default (Default: {})"),
246         'res_id': fields.integer('Record ID', help="Database ID of record to open in form view, when ``view_mode`` is set to 'form' only"),
247         'res_model': fields.char('Destination Model', size=64, required=True,
248             help="Model name of the object to open in the view window"),
249         'src_model': fields.char('Source Model', size=64,
250             help="Optional model name of the objects on which this action should be visible"),
251         'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline Edit'),('inlineview','Inline View')], 'Target Window'),
252         'view_mode': fields.char('View Mode', size=250, required=True,
253             help="Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)"),
254         'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
255             help="View type: Tree type to use for the tree view, set to 'tree' for a hierarchical tree view, or 'form' for a regular list view"),
256         'usage': fields.char('Action Usage', size=32,
257             help="Used to filter menu and home actions from the user form."),
258         'view_ids': fields.one2many('ir.actions.act_window.view', 'act_window_id', 'Views'),
259         'views': fields.function(_views_get_fnc, type='binary', string='Views',
260                help="This function field computes the ordered list of views that should be enabled " \
261                     "when displaying the result of an action, federating view mode, views and " \
262                     "reference view. The result is returned as an ordered list of pairs (view_id,view_mode)."),
263         'limit': fields.integer('Limit', help='Default limit for the list view'),
264         'auto_refresh': fields.integer('Auto-Refresh',
265             help='Add an auto-refresh on the view'),
266         'groups_id': fields.many2many('res.groups', 'ir_act_window_group_rel',
267             'act_id', 'gid', 'Groups'),
268         'search_view_id': fields.many2one('ir.ui.view', 'Search View Ref.'),
269         'filter': fields.boolean('Filter'),
270         'auto_search':fields.boolean('Auto Search'),
271         'search_view' : fields.function(_search_view, type='text', string='Search View'),
272         'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view"),
273     }
274
275     _defaults = {
276         'type': 'ir.actions.act_window',
277         'view_type': 'form',
278         'view_mode': 'tree,form',
279         'context': '{}',
280         'limit': 80,
281         'target': 'current',
282         'auto_refresh': 0,
283         'auto_search':True,
284         'multi': False,
285     }
286
287     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
288         """ call the method get_empty_list_help of the model and set the window action help message
289         """
290         ids_int = isinstance(ids, (int, long))
291         if ids_int:
292             ids = [ids]
293         results = super(act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load)
294
295         if not fields or 'help' in fields:
296             context = dict(context or {})
297             eval_dict = {
298                 'active_model': context.get('active_model'),
299                 'active_id': context.get('active_id'),
300                 'active_ids': context.get('active_ids'),
301                 'uid': uid,
302             }
303             for res in results:
304                 model = res.get('res_model')
305                 if model and self.pool.get(model):
306                     try:
307                         with tools.mute_logger("openerp.tools.safe_eval"):
308                             eval_context = eval(res['context'] or "{}", eval_dict) or {}
309                     except Exception:
310                         continue
311                     custom_context = dict(context, **eval_context)
312                     res['help'] = self.pool.get(model).get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context)
313         if ids_int:
314             return results[0]
315         return results
316
317     def for_xml_id(self, cr, uid, module, xml_id, context=None):
318         """ Returns the act_window object created for the provided xml_id
319
320         :param module: the module the act_window originates in
321         :param xml_id: the namespace-less id of the action (the @id
322                        attribute from the XML file)
323         :return: A read() view of the ir.actions.act_window
324         """
325         dataobj = self.pool.get('ir.model.data')
326         data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
327         res_id = dataobj.browse(cr, uid, data_id, context).res_id
328         return self.read(cr, uid, res_id, [], context)
329
330 act_window()
331
332 VIEW_TYPES = [
333     ('tree', 'Tree'),
334     ('form', 'Form'),
335     ('graph', 'Graph'),
336     ('calendar', 'Calendar'),
337     ('gantt', 'Gantt'),
338     ('kanban', 'Kanban')]
339 class act_window_view(osv.osv):
340     _name = 'ir.actions.act_window.view'
341     _table = 'ir_act_window_view'
342     _rec_name = 'view_id'
343     _order = 'sequence'
344     _columns = {
345         'sequence': fields.integer('Sequence'),
346         'view_id': fields.many2one('ir.ui.view', 'View'),
347         'view_mode': fields.selection(VIEW_TYPES, string='View Type', required=True),
348         'act_window_id': fields.many2one('ir.actions.act_window', 'Action', ondelete='cascade'),
349         'multi': fields.boolean('On Multiple Doc.',
350             help="If set to true, the action will not be displayed on the right toolbar of a form view."),
351     }
352     _defaults = {
353         'multi': False,
354     }
355     def _auto_init(self, cr, context=None):
356         super(act_window_view, self)._auto_init(cr, context)
357         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
358         if not cr.fetchone():
359             cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
360 act_window_view()
361
362 class act_wizard(osv.osv):
363     _name = 'ir.actions.wizard'
364     _inherit = 'ir.actions.actions'
365     _table = 'ir_act_wizard'
366     _sequence = 'ir_actions_id_seq'
367     _order = 'name'
368     _columns = {
369         'name': fields.char('Wizard Info', size=64, required=True, translate=True),
370         'type': fields.char('Action Type', size=32, required=True),
371         'wiz_name': fields.char('Wizard Name', size=64, required=True),
372         '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."),
373         'groups_id': fields.many2many('res.groups', 'res_groups_wizard_rel', 'uid', 'gid', 'Groups'),
374         'model': fields.char('Object', size=64),
375     }
376     _defaults = {
377         'type': 'ir.actions.wizard',
378         'multi': False,
379     }
380 act_wizard()
381
382 class act_url(osv.osv):
383     _name = 'ir.actions.act_url'
384     _table = 'ir_act_url'
385     _inherit = 'ir.actions.actions'
386     _sequence = 'ir_actions_id_seq'
387     _order = 'name'
388     _columns = {
389         'name': fields.char('Action Name', size=64, translate=True),
390         'type': fields.char('Action Type', size=32, required=True),
391         'url': fields.text('Action URL',required=True),
392         'target': fields.selection((
393             ('new', 'New Window'),
394             ('self', 'This Window')),
395             'Action Target', required=True
396         )
397     }
398     _defaults = {
399         'type': 'ir.actions.act_url',
400         'target': 'new'
401     }
402 act_url()
403
404 def model_get(self, cr, uid, context=None):
405     wkf_pool = self.pool.get('workflow')
406     ids = wkf_pool.search(cr, uid, [])
407     osvs = wkf_pool.read(cr, uid, ids, ['osv'])
408
409     res = []
410     mpool = self.pool.get('ir.model')
411     for osv in osvs:
412         model = osv.get('osv')
413         id = mpool.search(cr, uid, [('model','=',model)])
414         name = mpool.read(cr, uid, id)[0]['name']
415         res.append((model, name))
416
417     return res
418
419 class ir_model_fields(osv.osv):
420     _inherit = 'ir.model.fields'
421     _rec_name = 'field_description'
422     _columns = {
423         'complete_name': fields.char('Complete Name', size=64, select=1),
424     }
425 ir_model_fields()
426
427 class server_object_lines(osv.osv):
428     _name = 'ir.server.object.lines'
429     _sequence = 'ir_actions_id_seq'
430     _columns = {
431         'server_id': fields.many2one('ir.actions.server', 'Related Server Action'),
432         'col1': fields.many2one('ir.model.fields', 'Field', required=True),
433         'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
434                                                           "When Formula type is selected, this field may be a Python expression "
435                                                           " that can use the same values as for the condition field on the server action.\n"
436                                                           "If Value type is selected, the value will be used directly without evaluation."),
437         'type': fields.selection([
438             ('value', 'Value'),
439             ('equation', 'Python expression')
440         ], 'Evaluation Type', required=True, change_default=True),
441     }
442     _defaults = {
443         'type': 'value',
444     }
445
446
447 ##
448 # Actions that are run on the server side
449 #
450 class actions_server(osv.osv):
451     _name = 'ir.actions.server'
452     _table = 'ir_act_server'
453     _inherit = 'ir.actions.actions'
454     _sequence = 'ir_actions_id_seq'
455     _order = 'sequence,name'
456
457     def _select_objects(self, cr, uid, context=None):
458         model_pool = self.pool.get('ir.model')
459         ids = model_pool.search(cr, uid, [('name', 'not ilike', '.')])
460         res = model_pool.read(cr, uid, ids, ['model', 'name'])
461         return [(r['model'], r['name']) for r in res] + [('', '')]
462
463     def _get_states(self, cr, uid, context=None):
464         """ Override me in order to add new states in the server action. Please
465         note that the added key length should not be higher than already-existing
466         ones. """
467         return [('code', 'Execute Python Code'),
468                 ('trigger', 'Trigger a Workflow Signal'),
469                 ('client_action', 'Run a Client Action'),
470                 ('object_create', 'Create or Copy a new Record'),
471                 ('object_write', 'Write on a Record'),
472                 ('multi', 'Execute several actions')]
473
474     def _get_states_wrapper(self, cr, uid, context=None):
475         return self._get_states(cr, uid, context)
476
477     _columns = {
478         'name': fields.char('Action Name', required=True, size=64, translate=True),
479         'condition': fields.char('Condition',
480                                  help="Condition verified before executing the server action. If it "
481                                  "is not verified, the action will not be executed. The condition is "
482                                  "a Python expression, like 'object.list_price > 5000'. A void "
483                                  "condition is considered as always True. Help about pyhon expression "
484                                  "is given in the help tab."),
485         'state': fields.selection(_get_states_wrapper, 'Action To Do', required=True,
486                                   help="Type of server action. The following values are available:\n"
487                                   "- 'Execute Python Code': a block of python code that will be executed\n"
488                                   "- 'Trigger a Workflow Signal': send a signal to a workflow\n"
489                                   "- 'Run a Client Action': choose a client action to launch\n"
490                                   "-  'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
491                                   "- 'Write on a Record': update the values of a record\n"
492                                   "- 'Execute several actions': define an action that triggers several other sever actions\n"
493                                   "- 'Send Email': automatically send an email (available in email_template)"),
494         'usage': fields.char('Action Usage', size=32),
495         'type': fields.char('Action Type', size=32, required=True),
496         # Generic
497         'sequence': fields.integer('Sequence',
498                                    help="When dealing with multiple actions, the execution order is "
499                                    "based on the sequence. Low number means high priority."),
500         'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade',
501                                     help="Base model on which the server action runs."),
502         'menu_ir_values_id': fields.many2one('ir.values', 'More Menu entry', readonly=True,
503                                              help='More menu entry.'),
504         # Client Action
505         'action_id': fields.many2one('ir.actions.actions', 'Client Action',
506                                      help="Select the client action that has to be executed."),
507         # Python code
508         'code': fields.text('Python Code',
509                             help="Write Python code that the action will execute. Some variables are "
510                             "available for use; help about pyhon expression is given in the help tab."),
511         # Workflow signal
512         'use_relational_model': fields.selection([('base', 'Use the base model of the action'),
513                                                   ('relational', 'Use a relation field on the base model')],
514                                                  string='Target Model', required=True),
515         'wkf_transition_id': fields.many2one('workflow.transition', string='Signal to Trigger',
516                                              help="Select the workflow signal to trigger."),
517         'wkf_model_id': fields.many2one('ir.model', 'Target Model',
518                                         help="The model that will receive the workflow signal. Note that it should have a workflow associated with it."),
519         'wkf_model_name': fields.related('wkf_model_id', 'model', type='char', string='Target Model Name', store=True, readonly=True),
520         'wkf_field_id': fields.many2one('ir.model.fields', string='Relation Field',
521                                         oldname='trigger_obj_id',
522                                         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)"),
523         # Multi
524         'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions',
525                                       'server_id', 'action_id',
526                                       string='Child Actions',
527                                       help='Child server actions that will be executed. Note that the last return returned action value will be used as global return value.'),
528         # Create/Copy/Write
529         'use_create': fields.selection([('new', 'Create a new record in the Base Model'),
530                                         ('new_other', 'Create a new record in another model'),
531                                         ('copy_current', 'Copy the current record'),
532                                         ('copy_other', 'Copy another record')],
533                                        string="Creation Policy", required=True,
534                                        help=""),
535         'crud_model_id': fields.many2one('ir.model', 'Target Model',
536                                          oldname='srcmodel_id',
537                                          help="Model for record creation / update. Set this field only to specify a different model than the base model."),
538         'ref_object': fields.reference('Reference record', selection=_select_objects, size=128,
539                                        oldname='copy_object'),
540         'link_new_record': fields.boolean('Link to current record',
541                                           help="Check this if you want to link the newly-created record "
542                                           "to the current record on which the server action runs."),
543         'link_field_id': fields.many2one('ir.model.fields', 'Link Field',
544                                          oldname='record_id',
545                                          help="Provide the field where the record id is stored after the operations."),
546         'use_write': fields.selection([('current', 'Update the current record'),
547                                        ('other', 'Update another record'),
548                                        ('expression', 'Update according a Python expression')],
549                                       string='Update Policy', required=True,
550                                       help=""),
551         'write_expression': fields.char('Write Record Expression',
552                                         oldname='write_id',
553                                         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."),
554         'fields_lines': fields.one2many('ir.server.object.lines', 'server_id',
555                                         string='Value Mapping',
556                                         help=""),
557
558         # Fake fields used to implement the placeholder assistant
559         'model_object_field': fields.many2one('ir.model.fields', string="Field",
560                                               help="Select target field from the related document model.\n"
561                                                    "If it is a relationship field you will be able to select "
562                                                    "a target field at the destination of the relationship."),
563         'sub_object': fields.many2one('ir.model', 'Sub-model', readonly=True,
564                                       help="When a relationship field is selected as first field, "
565                                            "this field shows the document model the relationship goes to."),
566         'sub_model_object_field': fields.many2one('ir.model.fields', 'Sub-field',
567                                                   help="When a relationship field is selected as first field, "
568                                                        "this field lets you select the target field within the "
569                                                        "destination document model (sub-model)."),
570         'copyvalue': fields.char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field."),
571     }
572
573     _defaults = {
574         'state': 'code',
575         'condition': 'True',
576         'type': 'ir.actions.server',
577         'sequence': 5,
578         'use_relational_model': 'base',
579         'use_create': 'new',
580         'use_write': 'current',
581     }
582
583     def _check_expression(self, cr, uid, expression, model_id, context):
584         """ Check python expression (condition, write_expression) """
585         if not model_id:
586             return (False, None, 'Your expression cannot be validated because the Base Model is not set.')
587         # fetch current model
588         current_model_name = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
589         # transform expression into a path that should look like 'object.many2onefield.many2onefield'
590         path = expression.split('.')
591         initial = path.pop(0)
592         if initial not in ['obj', 'object']:
593             return (False, None, 'Your expression should begin with obj or object.\nAn expression builder is available in the help tab.')
594         # analyze path
595         while path:
596             step = path.pop(0)
597             column_info = self.pool[current_model_name]._all_columns.get(step)
598             if not column_info:
599                 return (False, None, 'Part of the expression (%s) is not recognized as a column in the model %s.' % (step, current_model_name))
600             column_type = column_info.column._type
601             if column_type not in ['many2one']:
602                 return (False, None, 'Part of the expression (%s) is not a valid column type (is %s, should be a many2one)' % (step, column_type))
603             current_model_name = column_info.column._obj
604         return (True, current_model_name, None)
605
606     def _check_write_expression(self, cr, uid, ids, context=None):
607         for record in self.browse(cr, uid, ids, context=context):
608             if record.write_expression and record.model_id:
609                 correct, model_name, message = self._check_expression(cr, uid, record.write_expression, record.model_id.id, context=context)
610                 if not correct:
611                     _logger.warning('Invalid expression: %s' % message)
612                     return False
613         return True
614
615     _constraints = [
616         (_check_write_expression,
617             'Incorrect Write Record Expression',
618             ['write_expression']),
619     ]
620
621     def on_change_model_id(self, cr, uid, ids, model_id, wkf_model_id, crud_model_id, context=None):
622         """ When changing the action base model, reset workflow and crud config
623         to ease value coherence """
624         values = {
625             'use_create': 'new',
626             'use_write': 'current',
627             'use_relational_model': 'base',
628             'wkf_model_id': model_id,
629             'crud_model_id': model_id,
630         }
631         return {'value': values}
632
633     def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
634         """ Update workflow configuration
635         - update the workflow model (for base (model_id) /relational (field.relation))
636         - update wkf_transition_id to False if workflow model changes, to force the user
637         to choose a new one
638         """
639         values = {}
640         if use_relational_model == 'relational' and wkf_field_id:
641             field = self.pool['ir.model.fields'].browse(cr, uid, wkf_field_id, context=context)
642             new_wkf_model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', field.relation)], context=context)[0]
643             values['wkf_model_id'] = new_wkf_model_id
644         else:
645             values['wkf_model_id'] = model_id
646         if values.get('wkf_model_id') != wkf_model_id:
647             values['wkf_transition_id'] = False
648         return {'value': values}
649
650     def on_change_wkf_model_id(self, cr, uid, ids, wkf_model_id, context=None):
651         """ When changing the workflow model, update its stored name also """
652         wkf_model_name = False
653         if wkf_model_id:
654             wkf_model_name = self.pool.get('ir.model').browse(cr, uid, wkf_model_id, context).model
655         return {'value': {'wkf_model_name': wkf_model_name}}
656
657     def on_change_crud_config(self, cr, uid, ids, state, use_create, use_write, ref_object, crud_model_id, model_id, context=None):
658         """ TODO """
659         if state == 'object_create':
660             return self.on_change_create_config(cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=context)
661         elif state == 'object_write':
662             return self.on_change_write_config(cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=context)
663         else:
664             return {}
665
666     def on_change_create_config(self, cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=None):
667         """ TODO """
668         values = {}
669         if use_create == 'new':
670             values['crud_model_id'] = model_id
671         elif use_create == 'new_other':
672             pass
673         elif use_create == 'copy_current':
674             values['crud_model_id'] = model_id
675         elif use_create == 'copy_other' and ref_object:
676             ref_model, ref_id = ref_object.split(',')
677             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
678             values['crud_model_id'] = ref_model_id
679
680         if values.get('crud_model_id') != crud_model_id:
681             values['link_field_id'] = False
682         return {'value': values}
683
684     def on_change_write_config(self, cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=None):
685         """ TODO """
686         values = {}
687         if use_write == 'current':
688             values['crud_model_id'] = model_id
689         elif use_write == 'other' and ref_object:
690             ref_model, ref_id = ref_object.split(',')
691             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
692             values['crud_model_id'] = ref_model_id
693         elif use_write == 'expression':
694             pass
695
696         if values.get('crud_model_id') != crud_model_id:
697             values['link_field_id'] = False
698         return {'value': values}
699
700     def on_change_write_expression(self, cr, uid, ids, write_expression, model_id, context=None):
701         """ Check the write_expression, about fields and models. """
702         values = {}
703         valid, model_name, message = self._check_expression(cr, uid, write_expression, model_id, context=context)
704         if valid:
705             ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', model_name)], context=context)[0]
706             values['crud_model_id'] = ref_model_id
707             return {'value': values}
708         if not message:
709             message = 'Invalid expression'
710         return {
711             'warning': {
712                 'title': 'Incorrect expression',
713                 'message': message,
714             }
715         }
716
717     def build_expression(self, field_name, sub_field_name):
718         """Returns a placeholder expression for use in a template field,
719            based on the values provided in the placeholder assistant.
720
721           :param field_name: main field name
722           :param sub_field_name: sub field name (M2O)
723           :return: final placeholder expression
724         """
725         expression = ''
726         if field_name:
727             expression = "object." + field_name
728             if sub_field_name:
729                 expression += "." + sub_field_name
730         return expression
731
732     def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, context=None):
733         result = {
734             'sub_object': False,
735             'copyvalue': False,
736             'sub_model_object_field': False,
737         }
738         if model_object_field:
739             fields_obj = self.pool.get('ir.model.fields')
740             field_value = fields_obj.browse(cr, uid, model_object_field, context)
741             if field_value.ttype in ['many2one', 'one2many', 'many2many']:
742                 res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
743                 sub_field_value = False
744                 if sub_model_object_field:
745                     sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
746                 if res_ids:
747                     result.update({
748                         'sub_object': res_ids[0],
749                         'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
750                         'sub_model_object_field': sub_model_object_field or False,
751                     })
752             else:
753                 result.update({
754                     'copyvalue': self.build_expression(field_value.name, False),
755                 })
756         return {'value': result}
757
758     def create_action(self, cr, uid, ids, context=None):
759         """ Create a contextual action for each of the server actions. """
760         for action in self.browse(cr, uid, ids, context=context):
761             ir_values_id = self.pool.get('ir.values').create(cr, SUPERUSER_ID, {
762                 'name': _('Run %s') % action.name,
763                 'model': action.model_id.model,
764                 'key2': 'client_action_multi',
765                 'value': "ir.actions.server,%s" % action.id,
766             }, context)
767             action.write({
768                 'menu_ir_values_id': ir_values_id,
769             })
770
771         return True
772
773     def unlink_action(self, cr, uid, ids, context=None):
774         """ Remove the contextual actions created for the server actions. """
775         for action in self.browse(cr, uid, ids, context=context):
776             if action.menu_ir_values_id:
777                 try:
778                     self.pool.get('ir.values').unlink(cr, SUPERUSER_ID, action.menu_ir_values_id.id, context)
779                 except Exception:
780                     raise osv.except_osv(_('Warning'), _('Deletion of the action record failed.'))
781         return True
782
783     def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):
784         if not action.action_id:
785             raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
786         return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
787
788     def run_action_code(self, cr, uid, action, eval_context=None, context=None):
789         eval(action.code.strip(), eval_context, mode="exec", nocopy=True)  # nocopy allows to return 'action'
790         if 'action' in eval_context:
791             return eval_context['action']
792
793     def run_action_trigger(self, cr, uid, action, eval_context=None, context=None):
794         """ Trigger a workflow signal, depending on the use_relational_model:
795         - `base`: base_model_pool.signal_<TRIGGER_NAME>(cr, uid, context.get('active_id'))
796         - `relational`: find the related model and object, using the relational
797         field, then target_model_pool.signal_<TRIGGER_NAME>(cr, uid, target_id)
798         """
799         obj_pool = self.pool[action.model_id.model]
800         if action.use_relational_model == 'base':
801             target_id = context.get('active_id')
802             target_pool = obj_pool
803         else:
804             value = getattr(obj_pool.browse(cr, uid, context.get('active_id'), context=context), action.wkf_field_id.name)
805             if action.wkf_field_id.ttype == 'many2one':
806                 target_id = value.id
807             else:
808                 target_id = value
809             target_pool = self.pool[action.wkf_model_id.model]
810
811         trigger_name = action.wkf_transition_id.signal
812
813         workflow.trg_validate(uid, target_pool._name, target_id, trigger_name, cr)
814
815     def run_action_multi(self, cr, uid, action, eval_context=None, context=None):
816         # TDE FIXME: loops are not considered here ^^
817         res = []
818         for act in action.child_ids:
819             result = self.run(cr, uid, [act.id], context)
820             if result:
821                 res.append(result)
822         return res and res[0] or False
823
824     def run_action_object_write(self, cr, uid, action, eval_context=None, context=None):
825         res = {}
826         for exp in action.fields_lines:
827             if exp.type == 'equation':
828                 expr = eval(exp.value, eval_context)
829             else:
830                 expr = exp.value
831             res[exp.col1.name] = expr
832
833         if action.use_write == 'current':
834             model = action.model_id.model
835             ref_id = context.get('active_id')
836         elif action.use_write == 'other':
837             model = action.crud_model_id.model
838             ref_id = action.ref_object.id
839         elif action.use_write == 'expression':
840             model = action.crud_model_id.model
841             ref = eval(action.write_expression, eval_context)
842             if isinstance(ref, browse_record):
843                 ref_id = getattr(ref, 'id')
844             else:
845                 ref_id = int(ref)
846
847         obj_pool = self.pool[model]
848         obj_pool.write(cr, uid, [ref_id], res, context=context)
849
850     def run_action_object_create(self, cr, uid, action, eval_context=None, context=None):
851         res = {}
852         for exp in action.fields_lines:
853             if exp.type == 'equation':
854                 expr = eval(exp.value, eval_context)
855             else:
856                 expr = exp.value
857             res[exp.col1.name] = expr
858
859         if action.use_create in ['new', 'copy_current']:
860             model = action.model_id.model
861         elif action.use_create in ['new_other', 'copy_other']:
862             model = action.crud_model_id.model
863
864         obj_pool = self.pool[model]
865         if action.use_create == 'copy_current':
866             ref_id = context.get('active_id')
867             res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
868         elif action.use_create == 'copy_other':
869             ref_id = action.ref_object.id
870             res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
871         else:
872             res_id = obj_pool.create(cr, uid, res, context=context)
873
874         if action.link_new_record and action.link_field_id:
875             self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
876
877     def run(self, cr, uid, ids, context=None):
878         """ Run the server action, by check the condition and then calling
879         run_action_<STATE>, i.e. run_action_code, allowing easy inheritance
880         of the server actions.
881
882         A void (aka False) condition is considered as always valid.
883
884         Note coming from previous implementation: FIXME: refactor all the eval()
885         calls in run()!
886
887         :param dict context: context should contain following keys:
888             - active_id: current id of the object
889             - active_model: current model that should equal the action's model
890             - TDE: ?? ids: original ids
891
892         :return: False: finished correctly or action_id: action to lanch
893         """
894         if context is None:
895             context = {}
896         res = False
897         user = self.pool.get('res.users').browse(cr, uid, uid)
898         active_ids = context.get('active_ids', [context.get('active_id', None)])
899         for action in self.browse(cr, uid, ids, context):
900             obj = None
901             obj_pool = self.pool[action.model_id.model]
902             for active_id in active_ids:
903                 if context.get('active_model') == action.model_id.model and active_id:
904                     obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
905                 # run context dedicated to a particular active_id
906                 run_context = dict(context, active_ids=[active_id], active_id=active_id)
907                 # evaluation context for python strings to evaluate
908                 eval_context = {
909                     'self': obj_pool,
910                     'object': obj,
911                     'obj': obj,
912                     'pool': self.pool,
913                     'time': time,
914                     'cr': cr,
915                     'context': dict(run_context),  # copy context to prevent side-effects of eval
916                     'uid': uid,
917                     'user': user
918                 }
919
920                 # evaluate the condition, with the specific case that a void (aka False) condition is considered as True
921                 condition = action.condition
922                 if action.condition is False:
923                     condition = True
924                 expr = eval(str(condition), eval_context)
925                 if not expr:
926                     continue
927                 # call the method related to the action: run_action_<STATE>
928                 if hasattr(self, 'run_action_%s' % action.state):
929                     res = getattr(self, 'run_action_%s' % action.state)(cr, uid, action, eval_context=eval_context, context=run_context)
930         return res
931
932
933 class act_window_close(osv.osv):
934     _name = 'ir.actions.act_window_close'
935     _inherit = 'ir.actions.actions'
936     _table = 'ir_actions'
937     _defaults = {
938         'type': 'ir.actions.act_window_close',
939     }
940 act_window_close()
941
942 # This model use to register action services.
943 TODO_STATES = [('open', 'To Do'),
944                ('done', 'Done')]
945 TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
946               ('automatic', 'Launch Automatically')]
947 class ir_actions_todo(osv.osv):
948     """
949     Configuration Wizards
950     """
951     _name = 'ir.actions.todo'
952     _description = "Configuration Wizards"
953     _columns={
954         'action_id': fields.many2one(
955             'ir.actions.actions', 'Action', select=True, required=True),
956         'sequence': fields.integer('Sequence'),
957         'state': fields.selection(TODO_STATES, string='Status', required=True),
958         'name': fields.char('Name', size=64),
959         'type': fields.selection(TODO_TYPES, 'Type', required=True,
960             help="""Manual: Launched manually.
961 Automatic: Runs whenever the system is reconfigured.
962 Launch Manually Once: after having been launched manually, it sets automatically to Done."""),
963         'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
964         'note': fields.text('Text', translate=True),
965     }
966     _defaults={
967         'state': 'open',
968         'sequence': 10,
969         'type': 'manual',
970     }
971     _order="sequence,id"
972
973     def action_launch(self, cr, uid, ids, context=None):
974         """ Launch Action of Wizard"""
975         wizard_id = ids and ids[0] or False
976         wizard = self.browse(cr, uid, wizard_id, context=context)
977         if wizard.type in ('automatic', 'once'):
978             wizard.write({'state': 'done'})
979
980         # Load action
981         act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
982
983         res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context)
984         if act_type['type'] != 'ir.actions.act_window':
985             return res
986         res.setdefault('context','{}')
987         res['nodestroy'] = True
988
989         # Open a specific record when res_id is provided in the context
990         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
991         ctx = eval(res['context'], {'user': user})
992         if ctx.get('res_id'):
993             res.update({'res_id': ctx.pop('res_id')})
994
995         # disable log for automatic wizards
996         if wizard.type == 'automatic':
997             ctx.update({'disable_log': True})
998         res.update({'context': ctx})
999
1000         return res
1001
1002     def action_open(self, cr, uid, ids, context=None):
1003         """ Sets configuration wizard in TODO state"""
1004         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
1005
1006     def progress(self, cr, uid, context=None):
1007         """ Returns a dict with 3 keys {todo, done, total}.
1008
1009         These keys all map to integers and provide the number of todos
1010         marked as open, the total number of todos and the number of
1011         todos not open (which is basically a shortcut to total-todo)
1012
1013         :rtype: dict
1014         """
1015         user_groups = set(map(
1016             lambda x: x.id,
1017             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
1018         def groups_match(todo):
1019             """ Checks if the todo's groups match those of the current user
1020             """
1021             return not todo.groups_id \
1022                    or bool(user_groups.intersection((
1023                         group.id for group in todo.groups_id)))
1024
1025         done = filter(
1026             groups_match,
1027             self.browse(cr, uid,
1028                 self.search(cr, uid, [('state', '!=', 'open')], context=context),
1029                         context=context))
1030
1031         total = filter(
1032             groups_match,
1033             self.browse(cr, uid,
1034                 self.search(cr, uid, [], context=context),
1035                         context=context))
1036
1037         return {
1038             'done': len(done),
1039             'total': len(total),
1040             'todo': len(total) - len(done)
1041         }
1042
1043 ir_actions_todo()
1044
1045 class act_client(osv.osv):
1046     _name = 'ir.actions.client'
1047     _inherit = 'ir.actions.actions'
1048     _table = 'ir_act_client'
1049     _sequence = 'ir_actions_id_seq'
1050     _order = 'name'
1051
1052     def _get_params(self, cr, uid, ids, field_name, arg, context):
1053         result = {}
1054         for record in self.browse(cr, uid, ids, context=context):
1055             result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
1056         return result
1057
1058     def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
1059         if isinstance(field_value, dict):
1060             self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
1061         else:
1062             self.write(cr, uid, id, {'params_store': field_value}, context=context)
1063
1064     _columns = {
1065         'name': fields.char('Action Name', required=True, size=64, translate=True),
1066         'tag': fields.char('Client action tag', size=64, required=True,
1067                            help="An arbitrary string, interpreted by the client"
1068                                 " according to its own needs and wishes. There "
1069                                 "is no central tag repository across clients."),
1070         'res_model': fields.char('Destination Model', size=64, 
1071             help="Optional model, mostly used for needactions."),
1072         'context': fields.char('Context Value', size=250, required=True,
1073             help="Context dictionary as Python expression, empty by default (Default: {})"),
1074         'params': fields.function(_get_params, fnct_inv=_set_params,
1075                                   type='binary', 
1076                                   string="Supplementary arguments",
1077                                   help="Arguments sent to the client along with"
1078                                        "the view tag"),
1079         'params_store': fields.binary("Params storage", readonly=True)
1080     }
1081     _defaults = {
1082         'type': 'ir.actions.client',
1083         'context': '{}',
1084
1085     }
1086 act_client()
1087
1088 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: