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