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 ##############################################################################
25 from socket import gethostname
28 from openerp import SUPERUSER_ID
29 from openerp import netsvc, tools
30 from openerp.osv import fields, osv
31 import openerp.report.interface
32 from openerp.report.report_sxw import report_sxw, report_rml
33 from openerp.tools.config import config
34 from openerp.tools.safe_eval import safe_eval as eval
35 from openerp.tools.translate import _
37 _logger = logging.getLogger(__name__)
39 class actions(osv.osv):
40 _name = 'ir.actions.actions'
44 'name': fields.char('Name', size=64, required=True),
45 'type': fields.char('Action Type', required=True, size=32),
46 'usage': fields.char('Action Usage', size=32),
47 'help': fields.text('Action description',
48 help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
52 'usage': lambda *a: False,
57 class report_xml(osv.osv):
59 def _report_content(self, cursor, user, ids, name, arg, context=None):
61 for report in self.browse(cursor, user, ids, context=context):
62 data = report[name + '_data']
63 if not data and report[name[:-8]]:
66 fp = tools.file_open(report[name[:-8]], mode='rb')
76 def _report_content_inv(self, cursor, user, id, name, value, arg, context=None):
77 self.write(cursor, user, id, {name+'_data': value}, context=context)
79 def _report_sxw(self, cursor, user, ids, name, arg, context=None):
81 for report in self.browse(cursor, user, ids, context=context):
83 res[report.id] = report.report_rml.replace('.rml', '.sxw')
85 res[report.id] = False
88 def render_report(self, cr, uid, res_ids, name, data, context=None):
90 Look up a report definition and render the report for the provided IDs.
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]
103 cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
104 r = cr.dictfetchone()
106 if r['report_rml'] or r['report_rml_content_data']:
108 kwargs = { 'parser': operator.attrgetter(r['parser'])(openerp.addons) }
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)
118 raise Exception, "Unhandled report type: %s" % r
120 raise Exception, "Required report does not exist: %s" % r
122 return new_report.create(cr, uid, res_ids, data, context)
124 _name = 'ir.actions.report.xml'
125 _inherit = 'ir.actions.actions'
126 _table = 'ir_act_report_xml'
127 _sequence = 'ir_actions_id_seq'
130 'name': fields.char('Name', size=64, required=True, translate=True),
131 'model': fields.char('Object', size=64, required=True),
132 'type': fields.char('Action Type', size=32, required=True),
133 'report_name': fields.char('Service Name', size=64, required=True),
134 'usage': fields.char('Action Usage', size=32),
135 'report_type': fields.char('Report Type', size=32, required=True, help="Report Type, e.g. pdf, html, raw, sxw, odt, html2html, mako2html, ..."),
136 'groups_id': fields.many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', 'Groups'),
137 '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."),
138 '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.'),
139 '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.'),
140 'auto': fields.boolean('Custom Python Parser'),
142 'header': fields.boolean('Add RML Header', help="Add or not the corporate RML header"),
144 'report_xsl': fields.char('XSL Path', size=256),
145 'report_xml': fields.char('XML Path', size=256, help=''),
147 # Pending deprecation... to be replaced by report_file as this object will become the default report object (not so specific to RML anymore)
148 '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"),
149 # temporary related field as report_rml is pending deprecation - this field will replace report_rml after v6.0
150 '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),
152 'report_sxw': fields.function(_report_sxw, type='char', string='SXW Path'),
153 'report_sxw_content_data': fields.binary('SXW Content'),
154 'report_rml_content_data': fields.binary('RML Content'),
155 'report_sxw_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='SXW Content',),
156 'report_rml_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='RML Content'),
158 'parser': fields.char('Parser Class'),
161 'type': 'ir.actions.report.xml',
165 'report_sxw_content': False,
166 'report_type': 'pdf',
172 class act_window(osv.osv):
173 _name = 'ir.actions.act_window'
174 _table = 'ir_act_window'
175 _inherit = 'ir.actions.actions'
176 _sequence = 'ir_actions_id_seq'
179 def _check_model(self, cr, uid, ids, context=None):
180 for action in self.browse(cr, uid, ids, context):
181 if not self.pool.get(action.res_model):
183 if action.src_model and not self.pool.get(action.src_model):
187 def _invalid_model_msg(self, cr, uid, ids, context=None):
188 return _('Invalid model name in the action definition.')
191 (_check_model, _invalid_model_msg, ['res_model','src_model'])
194 def _views_get_fnc(self, cr, uid, ids, name, arg, context=None):
195 """Returns an ordered list of the specific view modes that should be
196 enabled when displaying the result of this action, along with the
197 ID of the specific view to use for each mode, if any were required.
199 This function hides the logic of determining the precedence between
200 the view_modes string, the view_ids o2m, and the view_id m2o that can
201 be set on the action.
203 :rtype: dict in the form { action_id: list of pairs (tuples) }
204 :return: { action_id: [(view_id, view_mode), ...], ... }, where view_mode
205 is one of the possible values for ir.ui.view.type and view_id
206 is the ID of a specific view to use for this mode, or False for
210 for act in self.browse(cr, uid, ids):
211 res[act.id] = [(view.view_id.id, view.view_mode) for view in act.view_ids]
212 view_ids_modes = [view.view_mode for view in act.view_ids]
213 modes = act.view_mode.split(',')
214 missing_modes = [mode for mode in modes if mode not in view_ids_modes]
216 if act.view_id and act.view_id.type in missing_modes:
217 # reorder missing modes to put view_id first if present
218 missing_modes.remove(act.view_id.type)
219 res[act.id].append((act.view_id.id, act.view_id.type))
220 res[act.id].extend([(False, mode) for mode in missing_modes])
223 def _search_view(self, cr, uid, ids, name, arg, context=None):
225 for act in self.browse(cr, uid, ids, context=context):
226 field_get = self.pool[act.res_model].fields_view_get(cr, uid,
227 act.search_view_id and act.search_view_id.id or False,
228 'search', context=context)
229 res[act.id] = str(field_get)
233 'name': fields.char('Action Name', size=64, translate=True),
234 'type': fields.char('Action Type', size=32, required=True),
235 'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='cascade'),
236 'domain': fields.char('Domain Value', size=250,
237 help="Optional domain filtering of the destination data, as a Python expression"),
238 'context': fields.char('Context Value', size=250, required=True,
239 help="Context dictionary as Python expression, empty by default (Default: {})"),
240 'res_id': fields.integer('Record ID', help="Database ID of record to open in form view, when ``view_mode`` is set to 'form' only"),
241 'res_model': fields.char('Destination Model', size=64, required=True,
242 help="Model name of the object to open in the view window"),
243 'src_model': fields.char('Source Model', size=64,
244 help="Optional model name of the objects on which this action should be visible"),
245 'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline Edit'),('inlineview','Inline View')], 'Target Window'),
246 'view_mode': fields.char('View Mode', size=250, required=True,
247 help="Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)"),
248 'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
249 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"),
250 'usage': fields.char('Action Usage', size=32,
251 help="Used to filter menu and home actions from the user form."),
252 'view_ids': fields.one2many('ir.actions.act_window.view', 'act_window_id', 'Views'),
253 'views': fields.function(_views_get_fnc, type='binary', string='Views',
254 help="This function field computes the ordered list of views that should be enabled " \
255 "when displaying the result of an action, federating view mode, views and " \
256 "reference view. The result is returned as an ordered list of pairs (view_id,view_mode)."),
257 'limit': fields.integer('Limit', help='Default limit for the list view'),
258 'auto_refresh': fields.integer('Auto-Refresh',
259 help='Add an auto-refresh on the view'),
260 'groups_id': fields.many2many('res.groups', 'ir_act_window_group_rel',
261 'act_id', 'gid', 'Groups'),
262 'search_view_id': fields.many2one('ir.ui.view', 'Search View Ref.'),
263 'filter': fields.boolean('Filter'),
264 'auto_search':fields.boolean('Auto Search'),
265 'search_view' : fields.function(_search_view, type='text', string='Search View'),
266 '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"),
270 'type': 'ir.actions.act_window',
272 'view_mode': 'tree,form',
281 def for_xml_id(self, cr, uid, module, xml_id, context=None):
282 """ Returns the act_window object created for the provided xml_id
284 :param module: the module the act_window originates in
285 :param xml_id: the namespace-less id of the action (the @id
286 attribute from the XML file)
287 :return: A read() view of the ir.actions.act_window
289 dataobj = self.pool.get('ir.model.data')
290 data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
291 res_id = dataobj.browse(cr, uid, data_id, context).res_id
292 return self.read(cr, uid, res_id, [], context)
300 ('calendar', 'Calendar'),
302 ('kanban', 'Kanban')]
303 class act_window_view(osv.osv):
304 _name = 'ir.actions.act_window.view'
305 _table = 'ir_act_window_view'
306 _rec_name = 'view_id'
309 'sequence': fields.integer('Sequence'),
310 'view_id': fields.many2one('ir.ui.view', 'View'),
311 'view_mode': fields.selection(VIEW_TYPES, string='View Type', required=True),
312 'act_window_id': fields.many2one('ir.actions.act_window', 'Action', ondelete='cascade'),
313 'multi': fields.boolean('On Multiple Doc.',
314 help="If set to true, the action will not be displayed on the right toolbar of a form view."),
319 def _auto_init(self, cr, context=None):
320 super(act_window_view, self)._auto_init(cr, context)
321 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
322 if not cr.fetchone():
323 cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
326 class act_wizard(osv.osv):
327 _name = 'ir.actions.wizard'
328 _inherit = 'ir.actions.actions'
329 _table = 'ir_act_wizard'
330 _sequence = 'ir_actions_id_seq'
333 'name': fields.char('Wizard Info', size=64, required=True, translate=True),
334 'type': fields.char('Action Type', size=32, required=True),
335 'wiz_name': fields.char('Wizard Name', size=64, required=True),
336 '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."),
337 'groups_id': fields.many2many('res.groups', 'res_groups_wizard_rel', 'uid', 'gid', 'Groups'),
338 'model': fields.char('Object', size=64),
341 'type': 'ir.actions.wizard',
346 class act_url(osv.osv):
347 _name = 'ir.actions.act_url'
348 _table = 'ir_act_url'
349 _inherit = 'ir.actions.actions'
350 _sequence = 'ir_actions_id_seq'
353 'name': fields.char('Action Name', size=64, translate=True),
354 'type': fields.char('Action Type', size=32, required=True),
355 'url': fields.text('Action URL',required=True),
356 'target': fields.selection((
357 ('new', 'New Window'),
358 ('self', 'This Window')),
359 'Action Target', required=True
363 'type': 'ir.actions.act_url',
368 def model_get(self, cr, uid, context=None):
369 wkf_pool = self.pool.get('workflow')
370 ids = wkf_pool.search(cr, uid, [])
371 osvs = wkf_pool.read(cr, uid, ids, ['osv'])
374 mpool = self.pool.get('ir.model')
376 model = osv.get('osv')
377 id = mpool.search(cr, uid, [('model','=',model)])
378 name = mpool.read(cr, uid, id)[0]['name']
379 res.append((model, name))
383 class ir_model_fields(osv.osv):
384 _inherit = 'ir.model.fields'
385 _rec_name = 'field_description'
387 'complete_name': fields.char('Complete Name', size=64, select=1),
391 class server_object_lines(osv.osv):
392 _name = 'ir.server.object.lines'
393 _sequence = 'ir_actions_id_seq'
395 'server_id': fields.many2one('ir.actions.server', 'Object Mapping'),
396 'col1': fields.many2one('ir.model.fields', 'Destination', required=True),
397 'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
398 "When Formula type is selected, this field may be a Python expression "
399 " that can use the same values as for the condition field on the server action.\n"
400 "If Value type is selected, the value will be used directly without evaluation."),
401 'type': fields.selection([
403 ('equation','Formula')
404 ], 'Type', required=True, size=32, change_default=True),
409 server_object_lines()
412 # Actions that are run on the server side
414 class actions_server(osv.osv):
416 def _select_signals(self, cr, uid, context=None):
417 cr.execute("""SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t
418 WHERE w.id = a.wkf_id AND
419 (t.act_from = a.id OR t.act_to = a.id) AND
420 t.signal IS NOT NULL""")
421 result = cr.fetchall() or []
424 if rs[0] is not None and rs[1] is not None:
425 line = rs[1], "%s - (%s)" % (rs[1], rs[0])
429 def _select_objects(self, cr, uid, context=None):
430 model_pool = self.pool.get('ir.model')
431 ids = model_pool.search(cr, uid, [('name','not ilike','.')])
432 res = model_pool.read(cr, uid, ids, ['model', 'name'])
433 return [(r['model'], r['name']) for r in res] + [('','')]
435 def change_object(self, cr, uid, ids, copy_object, state, context=None):
436 if state == 'object_copy' and copy_object:
439 model_pool = self.pool.get('ir.model')
440 model = copy_object.split(',')[0]
441 mid = model_pool.search(cr, uid, [('model','=',model)])
443 'value': {'srcmodel_id': mid[0]},
449 _name = 'ir.actions.server'
450 _table = 'ir_act_server'
451 _inherit = 'ir.actions.actions'
452 _sequence = 'ir_actions_id_seq'
453 _order = 'sequence,name'
455 'name': fields.char('Action Name', required=True, size=64, translate=True),
456 'condition' : fields.char('Condition', size=256, required=True,
457 help="Condition that is tested before the action is executed, "
458 "and prevent execution if it is not verified.\n"
459 "Example: object.list_price > 5000\n"
460 "It is a Python expression that can use the following values:\n"
461 " - self: ORM model of the record on which the action is triggered\n"
462 " - object or obj: browse_record of the record on which the action is triggered\n"
463 " - pool: ORM model pool (i.e. self.pool)\n"
464 " - time: Python time module\n"
465 " - cr: database cursor\n"
466 " - uid: current user id\n"
467 " - context: current context"),
468 'state': fields.selection([
469 ('client_action','Client Action'),
471 ('loop','Iteration'),
472 ('code','Python Code'),
473 ('trigger','Trigger'),
476 ('object_create','Create Object'),
477 ('object_copy','Copy Object'),
478 ('object_write','Write Object'),
479 ('other','Multi Actions'),
480 ], 'Action Type', required=True, size=32, help="Type of the Action that is to be executed"),
481 'code':fields.text('Python Code', help="Python code to be executed if condition is met.\n"
482 "It is a Python block that can use the same values as for the condition field"),
483 '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."),
484 'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create).", ondelete='cascade'),
485 'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."),
486 'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
487 'wkf_model_id': fields.many2one('ir.model', 'Target Object', help="The object that should receive the workflow signal (must have an associated workflow)"),
488 '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)"),
489 '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"
490 "Example: object.invoice_address_id.email, or 'me@example.com'"),
491 '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 "
492 "available in the condition field, e.g. `Hello [[ object.partner_id.name ]]`"),
493 'message': fields.text('Message', translate=True, help="Email contents, may contain expressions enclosed in double brackets based on the same values as those "
494 "available in the condition field, e.g. `Dear [[ object.partner_id.name ]]`"),
495 '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"),
496 'sms': fields.char('SMS', size=160, translate=True),
497 'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions', 'server_id', 'action_id', 'Other Actions'),
498 'usage': fields.char('Action Usage', size=32),
499 'type': fields.char('Action Type', size=32, required=True),
500 '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."),
501 'fields_lines': fields.one2many('ir.server.object.lines', 'server_id', 'Field Mappings.'),
502 '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."),
503 '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."),
504 '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."),
505 '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`."),
506 'copy_object': fields.reference('Copy Of', selection=_select_objects, size=256),
511 'type': 'ir.actions.server',
513 'code': """# You can use the following variables:
514 # - self: ORM model of the record on which the action is triggered
515 # - object: browse_record of the record on which the action is triggered if there is one, otherwise None
516 # - pool: ORM model pool (i.e. self.pool)
517 # - time: Python time module
518 # - cr: database cursor
519 # - uid: current user id
520 # - context: current context
521 # If you plan to return an action, assign: action = {...}
525 def get_email(self, cr, uid, action, context):
526 obj_pool = self.pool.get(action.model_id.model)
527 id = context.get('active_id')
528 obj = obj_pool.browse(cr, uid, id)
532 if '/' in action.email.complete_name:
533 fields = action.email.complete_name.split('/')
534 elif '.' in action.email.complete_name:
535 fields = action.email.complete_name.split('.')
539 obj = getattr(obj, field)
541 _logger.exception('Failed to parse: %s', field)
545 def get_mobile(self, cr, uid, action, context):
546 obj_pool = self.pool.get(action.model_id.model)
547 id = context.get('active_id')
548 obj = obj_pool.browse(cr, uid, id)
552 if '/' in action.mobile.complete_name:
553 fields = action.mobile.complete_name.split('/')
554 elif '.' in action.mobile.complete_name:
555 fields = action.mobile.complete_name.split('.')
559 obj = getattr(obj, field)
561 _logger.exception('Failed to parse: %s', field)
565 def merge_message(self, cr, uid, keystr, action, context=None):
570 obj_pool = self.pool.get(action.model_id.model)
571 id = context.get('active_id')
572 obj = obj_pool.browse(cr, uid, id)
573 exp = str(match.group()[2:-2]).strip()
577 'context': dict(context), # copy context to prevent side-effects of eval
580 if result in (None, False):
581 return str("--------")
582 return tools.ustr(result)
584 com = re.compile('(\[\[.+?\]\])')
585 message = com.sub(merge, keystr)
589 # Context should contains:
591 # id : current id of the object
593 # False : Finished correctly
594 # ACTION_ID : Action to launch
596 # FIXME: refactor all the eval() calls in run()!
597 def run(self, cr, uid, ids, context=None):
600 user = self.pool.get('res.users').browse(cr, uid, uid)
601 for action in self.browse(cr, uid, ids, context):
603 obj_pool = self.pool.get(action.model_id.model)
604 if context.get('active_model') == action.model_id.model and context.get('active_id'):
605 obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
613 'context': dict(context), # copy context to prevent side-effects of eval
617 expr = eval(str(action.condition), cxt)
621 if action.state=='client_action':
622 if not action.action_id:
623 raise osv.except_osv(_('Error'), _("Please specify an action to launch !"))
624 return self.pool.get(action.action_id.type)\
625 .read(cr, uid, action.action_id.id, context=context)
627 if action.state=='code':
628 eval(action.code, cxt, mode="exec", nocopy=True) # nocopy allows to return 'action'
632 if action.state == 'email':
633 email_from = config['email_from']
635 _logger.debug('--email-from command line option is not specified, using a fallback value instead.')
637 email_from = user.email
639 email_from = "%s@%s" % (user.login, gethostname())
642 address = eval(str(action.email), cxt)
644 address = str(action.email)
647 _logger.info('No partner email address specified, not sending any email.')
650 # handle single and multiple recipient addresses
651 addresses = address if isinstance(address, (tuple, list)) else [address]
652 subject = self.merge_message(cr, uid, action.subject, action, context)
653 body = self.merge_message(cr, uid, action.message, action, context)
655 ir_mail_server = self.pool.get('ir.mail_server')
656 msg = ir_mail_server.build_email(email_from, addresses, subject, body)
657 res_email = ir_mail_server.send_email(cr, uid, msg)
659 _logger.info('Email successfully sent to: %s', addresses)
661 _logger.warning('Failed to send email to: %s', addresses)
663 if action.state == 'trigger':
664 wf_service = netsvc.LocalService("workflow")
665 model = action.wkf_model_id.model
666 m2o_field_name = action.trigger_obj_id.name
667 target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
668 target_id = target_id[0] if isinstance(target_id,tuple) else target_id
669 wf_service.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
671 if action.state == 'sms':
672 #TODO: set the user and password from the system
673 # for the sms gateway user / password
674 # USE smsclient module from extra-addons
675 _logger.warning('SMS Facility has not been implemented yet. Use smsclient module!')
677 if action.state == 'other':
679 for act in action.child_ids:
680 context['active_id'] = context['active_ids'][0]
681 result = self.run(cr, uid, [act.id], context)
686 if action.state == 'loop':
687 expr = eval(str(action.expression), cxt)
688 context['object'] = obj
690 context['active_id'] = i.id
691 self.run(cr, uid, [action.loop_action.id], context)
693 if action.state == 'object_write':
695 for exp in action.fields_lines:
697 if exp.type == 'equation':
698 expr = eval(euq, cxt)
701 res[exp.col1.name] = expr
703 if not action.write_id:
704 if not action.srcmodel_id:
705 obj_pool = self.pool.get(action.model_id.model)
706 obj_pool.write(cr, uid, [context.get('active_id')], res)
708 write_id = context.get('active_id')
709 obj_pool = self.pool.get(action.srcmodel_id.model)
710 obj_pool.write(cr, uid, [write_id], res)
712 elif action.write_id:
713 obj_pool = self.pool.get(action.srcmodel_id.model)
714 rec = self.pool.get(action.model_id.model).browse(cr, uid, context.get('active_id'))
715 id = eval(action.write_id, {'object': rec})
719 raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
721 if type(id) != type(1):
722 raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
724 obj_pool.write(cr, uid, [write_id], res)
726 if action.state == 'object_create':
728 for exp in action.fields_lines:
730 if exp.type == 'equation':
731 expr = eval(euq, cxt)
734 res[exp.col1.name] = expr
736 obj_pool = self.pool.get(action.srcmodel_id.model)
737 res_id = obj_pool.create(cr, uid, res)
739 self.pool.get(action.model_id.model).write(cr, uid, [context.get('active_id')], {action.record_id.name:res_id})
741 if action.state == 'object_copy':
743 for exp in action.fields_lines:
745 if exp.type == 'equation':
746 expr = eval(euq, cxt)
749 res[exp.col1.name] = expr
751 model = action.copy_object.split(',')[0]
752 cid = action.copy_object.split(',')[1]
753 obj_pool = self.pool.get(model)
754 obj_pool.copy(cr, uid, int(cid), res)
760 class act_window_close(osv.osv):
761 _name = 'ir.actions.act_window_close'
762 _inherit = 'ir.actions.actions'
763 _table = 'ir_actions'
765 'type': 'ir.actions.act_window_close',
769 # This model use to register action services.
770 TODO_STATES = [('open', 'To Do'),
772 TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
773 ('automatic', 'Launch Automatically')]
774 class ir_actions_todo(osv.osv):
776 Configuration Wizards
778 _name = 'ir.actions.todo'
779 _description = "Configuration Wizards"
781 'action_id': fields.many2one(
782 'ir.actions.actions', 'Action', select=True, required=True),
783 'sequence': fields.integer('Sequence'),
784 'state': fields.selection(TODO_STATES, string='Status', required=True),
785 'name': fields.char('Name', size=64),
786 'type': fields.selection(TODO_TYPES, 'Type', required=True,
787 help="""Manual: Launched manually.
788 Automatic: Runs whenever the system is reconfigured.
789 Launch Manually Once: after having been launched manually, it sets automatically to Done."""),
790 'groups_id': fields.many2many('res.groups', 'res_groups_action_rel', 'uid', 'gid', 'Groups'),
791 'note': fields.text('Text', translate=True),
800 def action_launch(self, cr, uid, ids, context=None):
801 """ Launch Action of Wizard"""
802 wizard_id = ids and ids[0] or False
803 wizard = self.browse(cr, uid, wizard_id, context=context)
804 if wizard.type in ('automatic', 'once'):
805 wizard.write({'state': 'done'})
808 act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
810 res = self.pool.get(act_type['type']).read(cr, uid, wizard.action_id.id, [], context=context)
811 if act_type['type'] != 'ir.actions.act_window':
813 res.setdefault('context','{}')
814 res['nodestroy'] = True
816 # Open a specific record when res_id is provided in the context
817 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
818 ctx = eval(res['context'], {'user': user})
819 if ctx.get('res_id'):
820 res.update({'res_id': ctx.pop('res_id')})
822 # disable log for automatic wizards
823 if wizard.type == 'automatic':
824 ctx.update({'disable_log': True})
825 res.update({'context': ctx})
829 def action_open(self, cr, uid, ids, context=None):
830 """ Sets configuration wizard in TODO state"""
831 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
833 def progress(self, cr, uid, context=None):
834 """ Returns a dict with 3 keys {todo, done, total}.
836 These keys all map to integers and provide the number of todos
837 marked as open, the total number of todos and the number of
838 todos not open (which is basically a shortcut to total-todo)
842 user_groups = set(map(
844 self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
845 def groups_match(todo):
846 """ Checks if the todo's groups match those of the current user
848 return not todo.groups_id \
849 or bool(user_groups.intersection((
850 group.id for group in todo.groups_id)))
855 self.search(cr, uid, [('state', '!=', 'open')], context=context),
861 self.search(cr, uid, [], context=context),
867 'todo': len(total) - len(done)
872 class act_client(osv.osv):
873 _name = 'ir.actions.client'
874 _inherit = 'ir.actions.actions'
875 _table = 'ir_act_client'
876 _sequence = 'ir_actions_id_seq'
879 def _get_params(self, cr, uid, ids, field_name, arg, context):
881 for record in self.browse(cr, uid, ids, context=context):
882 result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
885 def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
886 if isinstance(field_value, dict):
887 self.write(cr, uid, id, {'params_store': repr(field_value)}, context=context)
889 self.write(cr, uid, id, {'params_store': field_value}, context=context)
892 'name': fields.char('Action Name', required=True, size=64, translate=True),
893 'tag': fields.char('Client action tag', size=64, required=True,
894 help="An arbitrary string, interpreted by the client"
895 " according to its own needs and wishes. There "
896 "is no central tag repository across clients."),
897 'res_model': fields.char('Destination Model', size=64,
898 help="Optional model, mostly used for needactions."),
899 'context': fields.char('Context Value', size=250, required=True,
900 help="Context dictionary as Python expression, empty by default (Default: {})"),
901 'params': fields.function(_get_params, fnct_inv=_set_params,
903 string="Supplementary arguments",
904 help="Arguments sent to the client along with"
906 'params_store': fields.binary("Params storage", readonly=True)
909 'type': 'ir.actions.client',
915 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: