1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2011 OpenERP S.A. <http://www.openerp.com>
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.
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.
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/>.
20 ##############################################################################
26 from socket import gethostname
30 from openerp import SUPERUSER_ID
31 from openerp import netsvc, tools
32 from openerp.osv import fields, osv
33 import openerp.report.interface
34 from openerp.report.report_sxw import report_sxw, report_rml
35 from openerp.tools.config import config
36 from openerp.tools.safe_eval import safe_eval as eval
37 from openerp.tools.translate import _
39 _logger = logging.getLogger(__name__)
41 class actions(osv.osv):
42 _name = 'ir.actions.actions'
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.',
54 'usage': lambda *a: False,
59 class report_xml(osv.osv):
61 def _report_content(self, cursor, user, ids, name, arg, context=None):
63 for report in self.browse(cursor, user, ids, context=context):
64 data = report[name + '_data']
65 if not data and report[name[:-8]]:
68 fp = tools.file_open(report[name[:-8]], mode='rb')
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)
81 def _report_sxw(self, cursor, user, ids, name, arg, context=None):
83 for report in self.browse(cursor, user, ids, context=context):
85 res[report.id] = report.report_rml.replace('.rml', '.sxw')
87 res[report.id] = False
90 def _lookup_report(self, cr, name):
92 Look up a report definition.
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]
102 cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
103 r = cr.dictfetchone()
105 if r['report_rml'] or r['report_rml_content_data']:
107 kwargs = { 'parser': operator.attrgetter(r['parser'])(openerp.addons) }
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)
117 raise Exception, "Unhandled report type: %s" % r
119 raise Exception, "Required report does not exist: %s" % r
123 def render_report(self, cr, uid, res_ids, name, data, context=None):
125 Look up a report definition and render the report for the provided IDs.
127 new_report = self._lookup_report(cr, name)
128 return new_report.create(cr, uid, res_ids, data, context)
130 _name = 'ir.actions.report.xml'
131 _inherit = 'ir.actions.actions'
132 _table = 'ir_act_report_xml'
133 _sequence = 'ir_actions_id_seq'
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'),
148 'header': fields.boolean('Add RML Header', help="Add or not the corporate RML header"),
150 'report_xsl': fields.char('XSL Path', size=256),
151 'report_xml': fields.char('XML Path', size=256, help=''),
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),
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'),
164 'parser': fields.char('Parser Class'),
167 'type': 'ir.actions.report.xml',
171 'report_sxw_content': False,
172 'report_type': 'pdf',
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'
185 def _check_model(self, cr, uid, ids, context=None):
186 for action in self.browse(cr, uid, ids, context):
187 if not self.pool.get(action.res_model):
189 if action.src_model and not self.pool.get(action.src_model):
193 def _invalid_model_msg(self, cr, uid, ids, context=None):
194 return _('Invalid model name in the action definition.')
197 (_check_model, _invalid_model_msg, ['res_model','src_model'])
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.
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.
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
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]
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])
229 def _search_view(self, cr, uid, ids, name, arg, context=None):
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)
239 'name': fields.char('Action Name', size=64, translate=True),
240 'type': fields.char('Action Type', size=32, required=True),
241 'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='cascade'),
242 'domain': fields.char('Domain Value', size=250,
243 help="Optional domain filtering of the destination data, as a Python expression"),
244 'context': fields.char('Context Value', size=250, required=True,
245 help="Context dictionary as Python expression, empty by default (Default: {})"),
246 'res_id': fields.integer('Record ID', help="Database ID of record to open in form view, when ``view_mode`` is set to 'form' only"),
247 'res_model': fields.char('Destination Model', size=64, required=True,
248 help="Model name of the object to open in the view window"),
249 'src_model': fields.char('Source Model', size=64,
250 help="Optional model name of the objects on which this action should be visible"),
251 'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline Edit'),('inlineview','Inline View')], 'Target Window'),
252 'view_mode': fields.char('View Mode', size=250, required=True,
253 help="Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)"),
254 'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
255 help="View type: Tree type to use for the tree view, set to 'tree' for a hierarchical tree view, or 'form' for a regular list view"),
256 'usage': fields.char('Action Usage', size=32,
257 help="Used to filter menu and home actions from the user form."),
258 'view_ids': fields.one2many('ir.actions.act_window.view', 'act_window_id', 'Views'),
259 'views': fields.function(_views_get_fnc, type='binary', string='Views',
260 help="This function field computes the ordered list of views that should be enabled " \
261 "when displaying the result of an action, federating view mode, views and " \
262 "reference view. The result is returned as an ordered list of pairs (view_id,view_mode)."),
263 'limit': fields.integer('Limit', help='Default limit for the list view'),
264 'auto_refresh': fields.integer('Auto-Refresh',
265 help='Add an auto-refresh on the view'),
266 'groups_id': fields.many2many('res.groups', 'ir_act_window_group_rel',
267 'act_id', 'gid', 'Groups'),
268 'search_view_id': fields.many2one('ir.ui.view', 'Search View Ref.'),
269 'filter': fields.boolean('Filter'),
270 'auto_search':fields.boolean('Auto Search'),
271 'search_view' : fields.function(_search_view, type='text', string='Search View'),
272 'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view"),
276 'type': 'ir.actions.act_window',
278 'view_mode': 'tree,form',
287 def for_xml_id(self, cr, uid, module, xml_id, context=None):
288 """ Returns the act_window object created for the provided xml_id
290 :param module: the module the act_window originates in
291 :param xml_id: the namespace-less id of the action (the @id
292 attribute from the XML file)
293 :return: A read() view of the ir.actions.act_window
295 dataobj = self.pool.get('ir.model.data')
296 data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
297 res_id = dataobj.browse(cr, uid, data_id, context).res_id
298 return self.read(cr, uid, res_id, [], context)
306 ('calendar', 'Calendar'),
308 ('kanban', 'Kanban')]
309 class act_window_view(osv.osv):
310 _name = 'ir.actions.act_window.view'
311 _table = 'ir_act_window_view'
312 _rec_name = 'view_id'
315 'sequence': fields.integer('Sequence'),
316 'view_id': fields.many2one('ir.ui.view', 'View'),
317 'view_mode': fields.selection(VIEW_TYPES, string='View Type', required=True),
318 'act_window_id': fields.many2one('ir.actions.act_window', 'Action', ondelete='cascade'),
319 'multi': fields.boolean('On Multiple Doc.',
320 help="If set to true, the action will not be displayed on the right toolbar of a form view."),
325 def _auto_init(self, cr, context=None):
326 super(act_window_view, self)._auto_init(cr, context)
327 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
328 if not cr.fetchone():
329 cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
332 class act_wizard(osv.osv):
333 _name = 'ir.actions.wizard'
334 _inherit = 'ir.actions.actions'
335 _table = 'ir_act_wizard'
336 _sequence = 'ir_actions_id_seq'
339 'name': fields.char('Wizard Info', size=64, required=True, translate=True),
340 'type': fields.char('Action Type', size=32, required=True),
341 'wiz_name': fields.char('Wizard Name', size=64, required=True),
342 'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the wizard will not be displayed on the right toolbar of a form view."),
343 'groups_id': fields.many2many('res.groups', 'res_groups_wizard_rel', 'uid', 'gid', 'Groups'),
344 'model': fields.char('Object', size=64),
347 'type': 'ir.actions.wizard',
352 class act_url(osv.osv):
353 _name = 'ir.actions.act_url'
354 _table = 'ir_act_url'
355 _inherit = 'ir.actions.actions'
356 _sequence = 'ir_actions_id_seq'
359 'name': fields.char('Action Name', size=64, translate=True),
360 'type': fields.char('Action Type', size=32, required=True),
361 'url': fields.text('Action URL',required=True),
362 'target': fields.selection((
363 ('new', 'New Window'),
364 ('self', 'This Window')),
365 'Action Target', required=True
369 'type': 'ir.actions.act_url',
374 def model_get(self, cr, uid, context=None):
375 wkf_pool = self.pool.get('workflow')
376 ids = wkf_pool.search(cr, uid, [])
377 osvs = wkf_pool.read(cr, uid, ids, ['osv'])
380 mpool = self.pool.get('ir.model')
382 model = osv.get('osv')
383 id = mpool.search(cr, uid, [('model','=',model)])
384 name = mpool.read(cr, uid, id)[0]['name']
385 res.append((model, name))
389 class ir_model_fields(osv.osv):
390 _inherit = 'ir.model.fields'
391 _rec_name = 'field_description'
393 'complete_name': fields.char('Complete Name', size=64, select=1),
397 class server_object_lines(osv.osv):
398 _name = 'ir.server.object.lines'
399 _sequence = 'ir_actions_id_seq'
401 'server_id': fields.many2one('ir.actions.server', 'Object Mapping'),
402 'col1': fields.many2one('ir.model.fields', 'Destination', required=True),
403 'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
404 "When Formula type is selected, this field may be a Python expression "
405 " that can use the same values as for the condition field on the server action.\n"
406 "If Value type is selected, the value will be used directly without evaluation."),
407 'type': fields.selection([
409 ('equation','Formula')
410 ], 'Type', required=True, size=32, change_default=True),
415 server_object_lines()
418 # Actions that are run on the server side
420 class actions_server(osv.osv):
422 def _select_signals(self, cr, uid, context=None):
423 cr.execute("""SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t
424 WHERE w.id = a.wkf_id AND
425 (t.act_from = a.id OR t.act_to = a.id) AND
426 t.signal IS NOT NULL""")
427 result = cr.fetchall() or []
430 if rs[0] is not None and rs[1] is not None:
431 line = rs[1], "%s - (%s)" % (rs[1], rs[0])
435 def _select_objects(self, cr, uid, context=None):
436 model_pool = self.pool.get('ir.model')
437 ids = model_pool.search(cr, uid, [('name','not ilike','.')])
438 res = model_pool.read(cr, uid, ids, ['model', 'name'])
439 return [(r['model'], r['name']) for r in res] + [('','')]
441 def change_object(self, cr, uid, ids, copy_object, state, context=None):
442 if state == 'object_copy' and copy_object:
445 model_pool = self.pool.get('ir.model')
446 model = copy_object.split(',')[0]
447 mid = model_pool.search(cr, uid, [('model','=',model)])
449 'value': {'srcmodel_id': mid[0]},
455 _name = 'ir.actions.server'
456 _table = 'ir_act_server'
457 _inherit = 'ir.actions.actions'
458 _sequence = 'ir_actions_id_seq'
459 _order = 'sequence,name'
461 'name': fields.char('Action Name', required=True, size=64, translate=True),
462 'condition' : fields.char('Condition', size=256, required=True,
463 help="Condition that is tested before the action is executed, "
464 "and prevent execution if it is not verified.\n"
465 "Example: object.list_price > 5000\n"
466 "It is a Python expression that can use the following values:\n"
467 " - self: ORM model of the record on which the action is triggered\n"
468 " - object or obj: browse_record of the record on which the action is triggered\n"
469 " - pool: ORM model pool (i.e. self.pool)\n"
470 " - time: Python time module\n"
471 " - cr: database cursor\n"
472 " - uid: current user id\n"
473 " - context: current context"),
474 'state': fields.selection([
475 ('client_action','Client Action'),
477 ('loop','Iteration'),
478 ('code','Python Code'),
479 ('trigger','Trigger'),
482 ('object_create','Create Object'),
483 ('object_copy','Copy Object'),
484 ('object_write','Write Object'),
485 ('other','Multi Actions'),
486 ], 'Action Type', required=True, size=32, help="Type of the Action that is to be executed"),
487 'code':fields.text('Python Code', help="Python code to be executed if condition is met.\n"
488 "It is a Python block that can use the same values as for the condition field"),
489 'sequence': fields.integer('Sequence', help="Important when you deal with multiple actions, the execution order will be decided based on this, low number is higher priority."),
490 'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create).", ondelete='cascade'),
491 'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."),
492 'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
493 'wkf_model_id': fields.many2one('ir.model', 'Target Object', help="The object that should receive the workflow signal (must have an associated workflow)"),
494 'trigger_obj_id': fields.many2one('ir.model.fields','Relation Field', help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)"),
495 'email': fields.char('Email Address', size=512, help="Expression that returns the email address to send to. Can be based on the same values as for the condition field.\n"
496 "Example: object.invoice_address_id.email, or 'me@example.com'"),
497 'subject': fields.char('Subject', size=1024, translate=True, help="Email subject, may contain expressions enclosed in double brackets based on the same values as those "
498 "available in the condition field, e.g. `Hello [[ object.partner_id.name ]]`"),
499 'message': fields.text('Message', translate=True, help="Email contents, may contain expressions enclosed in double brackets based on the same values as those "
500 "available in the condition field, e.g. `Dear [[ object.partner_id.name ]]`"),
501 'mobile': fields.char('Mobile No', size=512, help="Provides fields that be used to fetch the mobile number, e.g. you select the invoice, then `object.invoice_address_id.mobile` is the field which gives the correct mobile number"),
502 'sms': fields.char('SMS', size=160, translate=True),
503 'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions', 'server_id', 'action_id', 'Other Actions'),
504 'usage': fields.char('Action Usage', size=32),
505 'type': fields.char('Action Type', size=32, required=True),
506 'srcmodel_id': fields.many2one('ir.model', 'Model', help="Object in which you want to create / write the object. If it is empty then refer to the Object field."),
507 'fields_lines': fields.one2many('ir.server.object.lines', 'server_id', 'Field Mappings.'),
508 'record_id':fields.many2one('ir.model.fields', 'Create Id', help="Provide the field name where the record id is stored after the create operations. If it is empty, you can not track the new record."),
509 'write_id':fields.char('Write Id', size=256, help="Provide the field name that the record id refers to for the write operation. If it is empty it will refer to the active id of the object."),
510 'loop_action':fields.many2one('ir.actions.server', 'Loop Action', help="Select the action that will be executed. Loop action will not be avaliable inside loop."),
511 'expression':fields.char('Loop Expression', size=512, help="Enter the field/expression that will return the list. E.g. select the sale order in Object, and you can have loop on the sales order line. Expression = `object.order_line`."),
512 'copy_object': fields.reference('Copy Of', selection=_select_objects, size=256),
517 'type': 'ir.actions.server',
519 'code': """# You can use the following variables:
520 # - self: ORM model of the record on which the action is triggered
521 # - object: browse_record of the record on which the action is triggered if there is one, otherwise None
522 # - pool: ORM model pool (i.e. self.pool)
523 # - time: Python time module
524 # - cr: database cursor
525 # - uid: current user id
526 # - context: current context
527 # If you plan to return an action, assign: action = {...}
531 def get_email(self, cr, uid, action, context):
532 obj_pool = self.pool.get(action.model_id.model)
533 id = context.get('active_id')
534 obj = obj_pool.browse(cr, uid, id)
538 if '/' in action.email.complete_name:
539 fields = action.email.complete_name.split('/')
540 elif '.' in action.email.complete_name:
541 fields = action.email.complete_name.split('.')
545 obj = getattr(obj, field)
547 _logger.exception('Failed to parse: %s', field)
551 def get_mobile(self, cr, uid, action, context):
552 obj_pool = self.pool.get(action.model_id.model)
553 id = context.get('active_id')
554 obj = obj_pool.browse(cr, uid, id)
558 if '/' in action.mobile.complete_name:
559 fields = action.mobile.complete_name.split('/')
560 elif '.' in action.mobile.complete_name:
561 fields = action.mobile.complete_name.split('.')
565 obj = getattr(obj, field)
567 _logger.exception('Failed to parse: %s', field)
571 def merge_message(self, cr, uid, keystr, action, context=None):
576 obj_pool = self.pool.get(action.model_id.model)
577 id = context.get('active_id')
578 obj = obj_pool.browse(cr, uid, id)
579 exp = str(match.group()[2:-2]).strip()
583 'context': dict(context), # copy context to prevent side-effects of eval
586 if result in (None, False):
587 return str("--------")
588 return tools.ustr(result)
590 com = re.compile('(\[\[.+?\]\])')
591 message = com.sub(merge, keystr)
595 # Context should contains:
597 # id : current id of the object
599 # False : Finished correctly
600 # ACTION_ID : Action to launch
602 # FIXME: refactor all the eval() calls in run()!
603 def run(self, cr, uid, ids, context=None):
606 user = self.pool.get('res.users').browse(cr, uid, uid)
607 for action in self.browse(cr, uid, ids, context):
609 obj_pool = self.pool.get(action.model_id.model)
610 if context.get('active_model') == action.model_id.model and context.get('active_id'):
611 obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
619 'context': dict(context), # copy context to prevent side-effects of eval
623 expr = eval(str(action.condition), cxt)
627 if action.state=='client_action':
628 if not action.action_id:
629 raise osv.except_osv(_('Error'), _("Please specify an action to launch !"))
630 return self.pool.get(action.action_id.type)\
631 .read(cr, uid, action.action_id.id, context=context)
633 if action.state=='code':
634 eval(action.code, cxt, mode="exec", nocopy=True) # nocopy allows to return 'action'
638 if action.state == 'email':
639 email_from = config['email_from']
641 _logger.debug('--email-from command line option is not specified, using a fallback value instead.')
643 email_from = user.email
645 email_from = "%s@%s" % (user.login, gethostname())
648 address = eval(str(action.email), cxt)
650 address = str(action.email)
653 _logger.info('No partner email address specified, not sending any email.')
656 # handle single and multiple recipient addresses
657 addresses = address if isinstance(address, (tuple, list)) else [address]
658 subject = self.merge_message(cr, uid, action.subject, action, context)
659 body = self.merge_message(cr, uid, action.message, action, context)
661 ir_mail_server = self.pool.get('ir.mail_server')
662 msg = ir_mail_server.build_email(email_from, addresses, subject, body)
663 res_email = ir_mail_server.send_email(cr, uid, msg)
665 _logger.info('Email successfully sent to: %s', addresses)
667 _logger.warning('Failed to send email to: %s', addresses)
669 if action.state == 'trigger':
670 wf_service = netsvc.LocalService("workflow")
671 model = action.wkf_model_id.model
672 m2o_field_name = action.trigger_obj_id.name
673 target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
674 target_id = target_id[0] if isinstance(target_id,tuple) else target_id
675 wf_service.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
677 if action.state == 'sms':
678 #TODO: set the user and password from the system
679 # for the sms gateway user / password
680 # USE smsclient module from extra-addons
681 _logger.warning('SMS Facility has not been implemented yet. Use smsclient module!')
683 if action.state == 'other':
685 for act in action.child_ids:
686 context['active_id'] = context['active_ids'][0]
687 result = self.run(cr, uid, [act.id], context)
692 if action.state == 'loop':
693 expr = eval(str(action.expression), cxt)
694 context['object'] = obj
696 context['active_id'] = i.id
697 self.run(cr, uid, [action.loop_action.id], context)
699 if action.state == 'object_write':
701 for exp in action.fields_lines:
703 if exp.type == 'equation':
704 expr = eval(euq, cxt)
707 res[exp.col1.name] = expr
709 if not action.write_id:
710 if not action.srcmodel_id:
711 obj_pool = self.pool.get(action.model_id.model)
712 obj_pool.write(cr, uid, [context.get('active_id')], res)
714 write_id = context.get('active_id')
715 obj_pool = self.pool.get(action.srcmodel_id.model)
716 obj_pool.write(cr, uid, [write_id], res)
718 elif action.write_id:
719 obj_pool = self.pool.get(action.srcmodel_id.model)
720 rec = self.pool.get(action.model_id.model).browse(cr, uid, context.get('active_id'))
721 id = eval(action.write_id, {'object': rec})
725 raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
727 if type(id) != type(1):
728 raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
730 obj_pool.write(cr, uid, [write_id], res)
732 if action.state == 'object_create':
734 for exp in action.fields_lines:
736 if exp.type == 'equation':
737 expr = eval(euq, cxt)
740 res[exp.col1.name] = expr
742 obj_pool = self.pool.get(action.srcmodel_id.model)
743 res_id = obj_pool.create(cr, uid, res)
745 self.pool.get(action.model_id.model).write(cr, uid, [context.get('active_id')], {action.record_id.name:res_id})
747 if action.state == 'object_copy':
749 for exp in action.fields_lines:
751 if exp.type == 'equation':
752 expr = eval(euq, cxt)
755 res[exp.col1.name] = expr
757 model = action.copy_object.split(',')[0]
758 cid = action.copy_object.split(',')[1]
759 obj_pool = self.pool.get(model)
760 obj_pool.copy(cr, uid, int(cid), res)
766 class act_window_close(osv.osv):
767 _name = 'ir.actions.act_window_close'
768 _inherit = 'ir.actions.actions'
769 _table = 'ir_actions'
771 'type': 'ir.actions.act_window_close',
775 # This model use to register action services.
776 TODO_STATES = [('open', 'To Do'),
778 TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
779 ('automatic', 'Launch Automatically')]
780 class ir_actions_todo(osv.osv):
782 Configuration Wizards
784 _name = 'ir.actions.todo'
785 _description = "Configuration Wizards"
787 'action_id': fields.many2one(
788 'ir.actions.actions', 'Action', select=True, required=True),
789 'sequence': fields.integer('Sequence'),
790 'state': fields.selection(TODO_STATES, string='Status', required=True),
791 'name': fields.char('Name', size=64),
792 'type': fields.selection(TODO_TYPES, 'Type', required=True,
793 help="""Manual: Launched manually.
794 Automatic: Runs whenever the system is reconfigured.
795 Launch Manually Once: after having been launched manually, it sets automatically to Done."""),
796 'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
797 'note': fields.text('Text', translate=True),
806 def action_launch(self, cr, uid, ids, context=None):
807 """ Launch Action of Wizard"""
808 wizard_id = ids and ids[0] or False
809 wizard = self.browse(cr, uid, wizard_id, context=context)
810 if wizard.type in ('automatic', 'once'):
811 wizard.write({'state': 'done'})
814 act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
816 res = self.pool.get(act_type['type']).read(cr, uid, wizard.action_id.id, [], context=context)
817 if act_type['type'] != 'ir.actions.act_window':
819 res.setdefault('context','{}')
820 res['nodestroy'] = True
822 # Open a specific record when res_id is provided in the context
823 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
824 ctx = eval(res['context'], {'user': user})
825 if ctx.get('res_id'):
826 res.update({'res_id': ctx.pop('res_id')})
828 # disable log for automatic wizards
829 if wizard.type == 'automatic':
830 ctx.update({'disable_log': True})
831 res.update({'context': ctx})
835 def action_open(self, cr, uid, ids, context=None):
836 """ Sets configuration wizard in TODO state"""
837 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
839 def progress(self, cr, uid, context=None):
840 """ Returns a dict with 3 keys {todo, done, total}.
842 These keys all map to integers and provide the number of todos
843 marked as open, the total number of todos and the number of
844 todos not open (which is basically a shortcut to total-todo)
848 user_groups = set(map(
850 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
851 def groups_match(todo):
852 """ Checks if the todo's groups match those of the current user
854 return not todo.groups_id \
855 or bool(user_groups.intersection((
856 group.id for group in todo.groups_id)))
861 self.search(cr, uid, [('state', '!=', 'open')], context=context),
867 self.search(cr, uid, [], context=context),
873 'todo': len(total) - len(done)
878 class act_client(osv.osv):
879 _name = 'ir.actions.client'
880 _inherit = 'ir.actions.actions'
881 _table = 'ir_act_client'
882 _sequence = 'ir_actions_id_seq'
885 def _get_params(self, cr, uid, ids, field_name, arg, context):
887 for record in self.browse(cr, uid, ids, context=context):
888 result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
891 def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
892 if isinstance(field_value, dict):
893 self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
895 self.write(cr, uid, id, {'params_store': field_value}, context=context)
898 'name': fields.char('Action Name', required=True, size=64, translate=True),
899 'tag': fields.char('Client action tag', size=64, required=True,
900 help="An arbitrary string, interpreted by the client"
901 " according to its own needs and wishes. There "
902 "is no central tag repository across clients."),
903 'res_model': fields.char('Destination Model', size=64,
904 help="Optional model, mostly used for needactions."),
905 'context': fields.char('Context Value', size=250, required=True,
906 help="Context dictionary as Python expression, empty by default (Default: {})"),
907 'params': fields.function(_get_params, fnct_inv=_set_params,
909 string="Supplementary arguments",
910 help="Arguments sent to the client along with"
912 'params_store': fields.binary("Params storage", readonly=True)
915 'type': 'ir.actions.client',
921 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: