##############################################################################
#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2011 OpenERP S.A. <http://www.openerp.com>
+# Copyright (C) 2004-2014 OpenERP S.A. <http://www.openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
#
##############################################################################
+from functools import partial
import logging
import operator
import os
import time
+import datetime
+import dateutil
import openerp
from openerp import SUPERUSER_ID
from openerp import tools
from openerp import workflow
+import openerp.api
from openerp.osv import fields, osv
from openerp.osv.orm import browse_record
import openerp.report.interface
_logger = logging.getLogger(__name__)
+
class actions(osv.osv):
_name = 'ir.actions.actions'
_table = 'ir_actions'
_order = 'name'
_columns = {
- 'name': fields.char('Name', size=64, required=True),
- 'type': fields.char('Action Type', required=True, size=32),
- 'usage': fields.char('Action Usage', size=32),
+ 'name': fields.char('Name', required=True),
+ 'type': fields.char('Action Type', required=True),
+ 'usage': fields.char('Action Usage'),
+ 'xml_id': fields.function(osv.osv.get_external_id, type='char', string="External ID"),
'help': fields.text('Action description',
help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
translate=True),
_defaults = {
'usage': lambda *a: False,
}
-actions()
+ def unlink(self, cr, uid, ids, context=None):
+ """unlink ir.action.todo which are related to actions which will be deleted.
+ NOTE: ondelete cascade will not work on ir.actions.actions so we will need to do it manually."""
+ todo_obj = self.pool.get('ir.actions.todo')
+ if not ids:
+ return True
+ if isinstance(ids, (int, long)):
+ ids = [ids]
+ todo_ids = todo_obj.search(cr, uid, [('action_id', 'in', ids)], context=context)
+ todo_obj.unlink(cr, uid, todo_ids, context=context)
+ return super(actions, self).unlink(cr, uid, ids, context=context)
-class report_xml(osv.osv):
+class ir_actions_report_xml(osv.osv):
def _report_content(self, cursor, user, ids, name, arg, context=None):
res = {}
cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
r = cr.dictfetchone()
if r:
- if r['report_rml'] or r['report_rml_content_data']:
+ if r['report_type'] in ['qweb-pdf', 'qweb-html']:
+ return r['report_name']
+ elif r['report_rml'] or r['report_rml_content_data']:
if r['parser']:
kwargs = { 'parser': operator.attrgetter(r['parser'])(openerp.addons) }
else:
kwargs = {}
new_report = report_sxw('report.'+r['report_name'], r['model'],
opj('addons',r['report_rml'] or '/'), header=r['header'], register=False, **kwargs)
- elif r['report_xsl']:
+ elif r['report_xsl'] and r['report_xml']:
new_report = report_rml('report.'+r['report_name'], r['model'],
opj('addons',r['report_xml']),
r['report_xsl'] and opj('addons',r['report_xsl']), register=False)
else:
raise Exception, "Unhandled report type: %s" % r
else:
- raise Exception, "Required report does not exist: %s" % r
+ raise Exception, "Required report does not exist: %s" % name
return new_report
Look up a report definition and render the report for the provided IDs.
"""
new_report = self._lookup_report(cr, name)
- return new_report.create(cr, uid, res_ids, data, context)
+
+ if isinstance(new_report, (str, unicode)): # Qweb report
+ # The only case where a QWeb report is rendered with this method occurs when running
+ # yml tests originally written for RML reports.
+ if openerp.tools.config['test_enable'] and not tools.config['test_report_directory']:
+ # Only generate the pdf when a destination folder has been provided.
+ return self.pool['report'].get_html(cr, uid, res_ids, new_report, data=data, context=context), 'html'
+ else:
+ return self.pool['report'].get_pdf(cr, uid, res_ids, new_report, data=data, context=context), 'pdf'
+ else:
+ return new_report.create(cr, uid, res_ids, data, context)
_name = 'ir.actions.report.xml'
_inherit = 'ir.actions.actions'
_sequence = 'ir_actions_id_seq'
_order = 'name'
_columns = {
- 'name': fields.char('Name', size=64, required=True, translate=True),
- 'model': fields.char('Object', size=64, required=True),
- 'type': fields.char('Action Type', size=32, required=True),
- 'report_name': fields.char('Service Name', size=64, required=True),
- 'usage': fields.char('Action Usage', size=32),
- 'report_type': fields.char('Report Type', size=32, required=True, help="Report Type, e.g. pdf, html, raw, sxw, odt, html2html, mako2html, ..."),
+ 'type': fields.char('Action Type', required=True),
+ 'name': fields.char('Name', required=True, translate=True),
+
+ 'model': fields.char('Model', required=True),
+ 'report_type': fields.selection([('qweb-pdf', 'PDF'),
+ ('qweb-html', 'HTML'),
+ ('controller', 'Controller'),
+ ('pdf', 'RML pdf (deprecated)'),
+ ('sxw', 'RML sxw (deprecated)'),
+ ('webkit', 'Webkit (deprecated)'),
+ ], 'Report Type', required=True, help="HTML will open the report directly in your browser, PDF will use wkhtmltopdf to render the HTML into a PDF file and let you download it, Controller allows you to define the url of a custom controller outputting any kind of report."),
+ 'report_name': fields.char('Template Name', required=True, help="For QWeb reports, name of the template used in the rendering. The method 'render_html' of the model 'report.template_name' will be called (if any) to give the html. For RML reports, this is the LocalService name."),
'groups_id': fields.many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', 'Groups'),
+
+ # options
'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."),
- '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.'),
'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.'),
- 'auto': fields.boolean('Custom Python Parser'),
+ 'attachment': fields.char('Save as Attachment Prefix', 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.'),
+ # Deprecated rml stuff
+ 'usage': fields.char('Action Usage'),
'header': fields.boolean('Add RML Header', help="Add or not the corporate RML header"),
+ 'parser': fields.char('Parser Class'),
+ 'auto': fields.boolean('Custom Python Parser'),
- 'report_xsl': fields.char('XSL Path', size=256),
- 'report_xml': fields.char('XML Path', size=256, help=''),
+ 'report_xsl': fields.char('XSL Path'),
+ 'report_xml': fields.char('XML Path'),
- # Pending deprecation... to be replaced by report_file as this object will become the default report object (not so specific to RML anymore)
- '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"),
- # temporary related field as report_rml is pending deprecation - this field will replace report_rml after v6.0
- '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),
+ 'report_rml': fields.char('Main Report File Path/controller', help="The path to the main report file/controller (depending on Report Type) or NULL if the content is in another data field"),
+ 'report_file': fields.related('report_rml', type="char", 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),
'report_sxw': fields.function(_report_sxw, type='char', string='SXW Path'),
'report_sxw_content_data': fields.binary('SXW Content'),
'report_rml_content_data': fields.binary('RML Content'),
'report_sxw_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='SXW Content',),
'report_rml_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='RML Content'),
-
- 'parser': fields.char('Parser Class'),
}
_defaults = {
'type': 'ir.actions.report.xml',
'attachment': False,
}
-report_xml()
-class act_window(osv.osv):
+class ir_actions_act_window(osv.osv):
_name = 'ir.actions.act_window'
_table = 'ir_act_window'
_inherit = 'ir.actions.actions'
return res
_columns = {
- 'name': fields.char('Action Name', size=64, translate=True),
- 'type': fields.char('Action Type', size=32, required=True),
- 'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='cascade'),
+ 'name': fields.char('Action Name', translate=True),
+ 'type': fields.char('Action Type', required=True),
+ 'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='set null'),
'domain': fields.char('Domain Value',
help="Optional domain filtering of the destination data, as a Python expression"),
'context': fields.char('Context Value', required=True,
help="Context dictionary as Python expression, empty by default (Default: {})"),
'res_id': fields.integer('Record ID', help="Database ID of record to open in form view, when ``view_mode`` is set to 'form' only"),
- 'res_model': fields.char('Destination Model', size=64, required=True,
+ 'res_model': fields.char('Destination Model', required=True,
help="Model name of the object to open in the view window"),
- 'src_model': fields.char('Source Model', size=64,
+ 'src_model': fields.char('Source Model',
help="Optional model name of the objects on which this action should be visible"),
'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline Edit'),('inlineview','Inline View')], 'Target Window'),
- 'view_mode': fields.char('View Mode', size=250, required=True,
+ 'view_mode': fields.char('View Mode', required=True,
help="Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)"),
'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
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"),
- 'usage': fields.char('Action Usage', size=32,
+ 'usage': fields.char('Action Usage',
help="Used to filter menu and home actions from the user form."),
'view_ids': fields.one2many('ir.actions.act_window.view', 'act_window_id', 'Views'),
'views': fields.function(_views_get_fnc, type='binary', string='Views',
'filter': fields.boolean('Filter'),
'auto_search':fields.boolean('Auto Search'),
'search_view' : fields.function(_search_view, type='text', string='Search View'),
- '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"),
+ 'multi': fields.boolean('Restrict to lists', help="If checked and the action is bound to a model, it will only appear in the More menu on list views"),
}
_defaults = {
ids_int = isinstance(ids, (int, long))
if ids_int:
ids = [ids]
- results = super(act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load)
-
- if not fields or 'help' in fields:
- context = dict(context or {})
- eval_dict = {
- 'active_model': context.get('active_model'),
- 'active_id': context.get('active_id'),
- 'active_ids': context.get('active_ids'),
- 'uid': uid,
- }
- for res in results:
- model = res.get('res_model')
- if model and self.pool.get(model):
- try:
- with tools.mute_logger("openerp.tools.safe_eval"):
- eval_context = eval(res['context'] or "{}", eval_dict) or {}
- except Exception:
- continue
+ results = super(ir_actions_act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load)
+
+ context = dict(context or {})
+ eval_dict = {
+ 'active_model': context.get('active_model'),
+ 'active_id': context.get('active_id'),
+ 'active_ids': context.get('active_ids'),
+ 'uid': uid,
+ 'context': context,
+ }
+ for res in results:
+ model = res.get('res_model')
+ if model in self.pool:
+ try:
+ with tools.mute_logger("openerp.tools.safe_eval"):
+ eval_context = eval(res['context'] or "{}", eval_dict) or {}
+ res['context'] = str(eval_context)
+ except Exception:
+ continue
+ if not fields or 'help' in fields:
custom_context = dict(context, **eval_context)
- res['help'] = self.pool.get(model).get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context)
+ res['help'] = self.pool[model].get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context)
if ids_int:
return results[0]
return results
dataobj = self.pool.get('ir.model.data')
data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
res_id = dataobj.browse(cr, uid, data_id, context).res_id
- return self.read(cr, uid, res_id, [], context)
-
-act_window()
+ return self.read(cr, uid, [res_id], [], context)[0]
VIEW_TYPES = [
('tree', 'Tree'),
('calendar', 'Calendar'),
('gantt', 'Gantt'),
('kanban', 'Kanban')]
-class act_window_view(osv.osv):
+class ir_actions_act_window_view(osv.osv):
_name = 'ir.actions.act_window.view'
_table = 'ir_act_window_view'
_rec_name = 'view_id'
'multi': False,
}
def _auto_init(self, cr, context=None):
- super(act_window_view, self)._auto_init(cr, context)
+ super(ir_actions_act_window_view, self)._auto_init(cr, context)
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
if not cr.fetchone():
cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
-act_window_view()
-class act_wizard(osv.osv):
- _name = 'ir.actions.wizard'
+
+class ir_actions_act_window_close(osv.osv):
+ _name = 'ir.actions.act_window_close'
_inherit = 'ir.actions.actions'
- _table = 'ir_act_wizard'
- _sequence = 'ir_actions_id_seq'
- _order = 'name'
- _columns = {
- 'name': fields.char('Wizard Info', size=64, required=True, translate=True),
- 'type': fields.char('Action Type', size=32, required=True),
- 'wiz_name': fields.char('Wizard Name', size=64, required=True),
- '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."),
- 'groups_id': fields.many2many('res.groups', 'res_groups_wizard_rel', 'uid', 'gid', 'Groups'),
- 'model': fields.char('Object', size=64),
- }
+ _table = 'ir_actions'
_defaults = {
- 'type': 'ir.actions.wizard',
- 'multi': False,
+ 'type': 'ir.actions.act_window_close',
}
-act_wizard()
-class act_url(osv.osv):
+
+class ir_actions_act_url(osv.osv):
_name = 'ir.actions.act_url'
_table = 'ir_act_url'
_inherit = 'ir.actions.actions'
_sequence = 'ir_actions_id_seq'
_order = 'name'
_columns = {
- 'name': fields.char('Action Name', size=64, translate=True),
- 'type': fields.char('Action Type', size=32, required=True),
+ 'name': fields.char('Action Name', translate=True),
+ 'type': fields.char('Action Type', required=True),
'url': fields.text('Action URL',required=True),
'target': fields.selection((
('new', 'New Window'),
'type': 'ir.actions.act_url',
'target': 'new'
}
-act_url()
-
-def model_get(self, cr, uid, context=None):
- wkf_pool = self.pool.get('workflow')
- ids = wkf_pool.search(cr, uid, [])
- osvs = wkf_pool.read(cr, uid, ids, ['osv'])
-
- res = []
- mpool = self.pool.get('ir.model')
- for osv in osvs:
- model = osv.get('osv')
- id = mpool.search(cr, uid, [('model','=',model)])
- name = mpool.read(cr, uid, id)[0]['name']
- res.append((model, name))
-
- return res
-
-class ir_model_fields(osv.osv):
- _inherit = 'ir.model.fields'
- _rec_name = 'field_description'
- _columns = {
- 'complete_name': fields.char('Complete Name', size=64, select=1),
- }
-ir_model_fields()
-class server_object_lines(osv.osv):
- _name = 'ir.server.object.lines'
- _sequence = 'ir_actions_id_seq'
- _columns = {
- 'server_id': fields.many2one('ir.actions.server', 'Related Server Action'),
- 'col1': fields.many2one('ir.model.fields', 'Field', required=True),
- 'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
- "When Formula type is selected, this field may be a Python expression "
- " that can use the same values as for the condition field on the server action.\n"
- "If Value type is selected, the value will be used directly without evaluation."),
- 'type': fields.selection([
- ('value', 'Value'),
- ('equation', 'Python expression')
- ], 'Evaluation Type', required=True, change_default=True),
- }
- _defaults = {
- 'type': 'value',
- }
+class ir_actions_server(osv.osv):
+ """ Server actions model. Server action work on a base model and offer various
+ type of actions that can be executed automatically, for example using base
+ action rules, of manually, by adding the action in the 'More' contextual
+ menu.
-##
-# Actions that are run on the server side
-#
-class actions_server(osv.osv):
+ Since OpenERP 8.0 a button 'Create Menu Action' button is available on the
+ action form view. It creates an entry in the More menu of the base model.
+ This allows to create server actions and run them in mass mode easily through
+ the interface.
+
+ The available actions are :
+
+ - 'Execute Python Code': a block of python code that will be executed
+ - 'Trigger a Workflow Signal': send a signal to a workflow
+ - 'Run a Client Action': choose a client action to launch
+ - 'Create or Copy a new Record': create a new record with new values, or
+ copy an existing record in your database
+ - 'Write on a Record': update the values of a record
+ - 'Execute several actions': define an action that triggers several other
+ server actions
+ """
_name = 'ir.actions.server'
_table = 'ir_act_server'
_inherit = 'ir.actions.actions'
def _select_objects(self, cr, uid, context=None):
model_pool = self.pool.get('ir.model')
- ids = model_pool.search(cr, uid, [('name', 'not ilike', '.')])
+ ids = model_pool.search(cr, uid, [], limit=None)
res = model_pool.read(cr, uid, ids, ['model', 'name'])
return [(r['model'], r['name']) for r in res] + [('', '')]
return self._get_states(cr, uid, context)
_columns = {
- 'name': fields.char('Action Name', required=True, size=64, translate=True),
+ 'name': fields.char('Action Name', required=True, translate=True),
'condition': fields.char('Condition',
help="Condition verified before executing the server action. If it "
"is not verified, the action will not be executed. The condition is "
"a Python expression, like 'object.list_price > 5000'. A void "
- "condition is considered as always True. Help about pyhon expression "
+ "condition is considered as always True. Help about python expression "
"is given in the help tab."),
'state': fields.selection(_get_states_wrapper, 'Action To Do', required=True,
help="Type of server action. The following values are available:\n"
"- 'Execute Python Code': a block of python code that will be executed\n"
"- 'Trigger a Workflow Signal': send a signal to a workflow\n"
"- 'Run a Client Action': choose a client action to launch\n"
- "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
+ "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
"- 'Write on a Record': update the values of a record\n"
- "- 'Execute several actions': define an action that triggers several other sever actions\n"
+ "- 'Execute several actions': define an action that triggers several other server actions\n"
"- 'Send Email': automatically send an email (available in email_template)"),
- 'usage': fields.char('Action Usage', size=32),
- 'type': fields.char('Action Type', size=32, required=True),
+ 'usage': fields.char('Action Usage'),
+ 'type': fields.char('Action Type', required=True),
# Generic
'sequence': fields.integer('Sequence',
help="When dealing with multiple actions, the execution order is "
"based on the sequence. Low number means high priority."),
'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade',
help="Base model on which the server action runs."),
+ 'model_name': fields.related('model_id', 'model', type='char',
+ string='Model Name', readonly=True),
'menu_ir_values_id': fields.many2one('ir.values', 'More Menu entry', readonly=True,
- help='More menu entry.'),
+ help='More menu entry.', copy=False),
# Client Action
'action_id': fields.many2one('ir.actions.actions', 'Client Action',
help="Select the client action that has to be executed."),
'use_create': fields.selection([('new', 'Create a new record in the Base Model'),
('new_other', 'Create a new record in another model'),
('copy_current', 'Copy the current record'),
- ('copy_other', 'Copy another record')],
+ ('copy_other', 'Choose and copy a record in the database')],
string="Creation Policy", required=True,
help=""),
'crud_model_id': fields.many2one('ir.model', 'Target Model',
oldname='srcmodel_id',
help="Model for record creation / update. Set this field only to specify a different model than the base model."),
+ 'crud_model_name': fields.related('crud_model_id', 'model', type='char',
+ string='Create/Write Target Model Name',
+ store=True, readonly=True),
'ref_object': fields.reference('Reference record', selection=_select_objects, size=128,
oldname='copy_object'),
- 'link_new_record': fields.boolean('Link to current record',
+ 'link_new_record': fields.boolean('Attach the new record',
help="Check this if you want to link the newly-created record "
"to the current record on which the server action runs."),
- 'link_field_id': fields.many2one('ir.model.fields', 'Link Field',
+ 'link_field_id': fields.many2one('ir.model.fields', 'Link using field',
oldname='record_id',
help="Provide the field where the record id is stored after the operations."),
'use_write': fields.selection([('current', 'Update the current record'),
- ('other', 'Update another record'),
- ('expression', 'Update according a Python expression')],
+ ('expression', 'Update a record linked to the current record using python'),
+ ('other', 'Choose and Update a record in the database')],
string='Update Policy', required=True,
help=""),
- 'write_expression': fields.char('Write Record Expression',
+ 'write_expression': fields.char('Expression',
oldname='write_id',
- 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."),
+ help="Provide an expression that, applied on the current record, gives the field to update."),
'fields_lines': fields.one2many('ir.server.object.lines', 'server_id',
string='Value Mapping',
- help=""),
+ copy=True),
# Fake fields used to implement the placeholder assistant
'model_object_field': fields.many2one('ir.model.fields', string="Field",
"this field lets you select the target field within the "
"destination document model (sub-model)."),
'copyvalue': fields.char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field."),
+ # Fake fields used to implement the ID finding assistant
+ 'id_object': fields.reference('Record', selection=_select_objects, size=128),
+ 'id_value': fields.char('Record ID'),
}
_defaults = {
'condition': 'True',
'type': 'ir.actions.server',
'sequence': 5,
+ 'code': """# You can use the following variables:
+# - self: ORM model of the record on which the action is triggered
+# - object: Record on which the action is triggered if there is one, otherwise None
+# - pool: ORM model pool (i.e. self.pool)
+# - cr: database cursor
+# - uid: current user id
+# - context: current context
+# - time: Python time module
+# - workflow: Workflow engine
+# If you plan to return an action, assign: action = {...}""",
'use_relational_model': 'base',
'use_create': 'new',
'use_write': 'current',
}
def _check_expression(self, cr, uid, expression, model_id, context):
- """ Check python expression (condition, write_expression) """
+ """ Check python expression (condition, write_expression). Each step of
+ the path must be a valid many2one field, or an integer field for the last
+ step.
+
+ :param str expression: a python expression, beginning by 'obj' or 'object'
+ :param int model_id: the base model of the server action
+ :returns tuple: (is_valid, target_model_name, error_msg)
+ """
if not model_id:
return (False, None, 'Your expression cannot be validated because the Base Model is not set.')
# fetch current model
# analyze path
while path:
step = path.pop(0)
- column_info = self.pool[current_model_name]._all_columns.get(step)
- if not column_info:
+ field = self.pool[current_model_name]._fields.get(step)
+ if not field:
return (False, None, 'Part of the expression (%s) is not recognized as a column in the model %s.' % (step, current_model_name))
- column_type = column_info.column._type
- if column_type not in ['many2one']:
- return (False, None, 'Part of the expression (%s) is not a valid column type (is %s, should be a many2one)' % (step, column_type))
- current_model_name = column_info.column._obj
+ ftype = field.type
+ if ftype not in ['many2one', 'int']:
+ return (False, None, 'Part of the expression (%s) is not a valid column type (is %s, should be a many2one or an int)' % (step, ftype))
+ if ftype == 'int' and path:
+ return (False, None, 'Part of the expression (%s) is an integer field that is only allowed at the end of an expression' % (step))
+ if ftype == 'many2one':
+ current_model_name = field.comodel_name
return (True, current_model_name, None)
def _check_write_expression(self, cr, uid, ids, context=None):
(_check_write_expression,
'Incorrect Write Record Expression',
['write_expression']),
+ (partial(osv.Model._check_m2m_recursion, field_name='child_ids'),
+ 'Recursion found in child server actions',
+ ['child_ids']),
]
def on_change_model_id(self, cr, uid, ids, model_id, wkf_model_id, crud_model_id, context=None):
""" When changing the action base model, reset workflow and crud config
- to ease value coherence """
+ to ease value coherence. """
values = {
'use_create': 'new',
'use_write': 'current',
'use_relational_model': 'base',
'wkf_model_id': model_id,
+ 'wkf_field_id': False,
'crud_model_id': model_id,
}
+
+ if model_id:
+ values['model_name'] = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
+
return {'value': values}
def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
- """ Update workflow configuration
- - update the workflow model (for base (model_id) /relational (field.relation))
- - update wkf_transition_id to False if workflow model changes, to force the user
- to choose a new one
+ """ Update workflow type configuration
+
+ - update the workflow model (for base (model_id) /relational (field.relation))
+ - update wkf_transition_id to False if workflow model changes, to force
+ the user to choose a new one
"""
values = {}
if use_relational_model == 'relational' and wkf_field_id:
values['wkf_model_id'] = new_wkf_model_id
else:
values['wkf_model_id'] = model_id
- if values.get('wkf_model_id') != wkf_model_id:
- values['wkf_transition_id'] = False
return {'value': values}
def on_change_wkf_model_id(self, cr, uid, ids, wkf_model_id, context=None):
wkf_model_name = False
if wkf_model_id:
wkf_model_name = self.pool.get('ir.model').browse(cr, uid, wkf_model_id, context).model
- return {'value': {'wkf_model_name': wkf_model_name}}
+ values = {'wkf_transition_id': False, 'wkf_model_name': wkf_model_name}
+ return {'value': values}
def on_change_crud_config(self, cr, uid, ids, state, use_create, use_write, ref_object, crud_model_id, model_id, context=None):
- """ TODO """
+ """ Wrapper on CRUD-type (create or write) on_change """
if state == 'object_create':
return self.on_change_create_config(cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=context)
elif state == 'object_write':
return {}
def on_change_create_config(self, cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=None):
- """ TODO """
+ """ When changing the object_create type configuration:
+
+ - `new` and `copy_current`: crud_model_id is the same as base model
+ - `new_other`: user choose crud_model_id
+ - `copy_other`: disassemble the reference object to have its model
+ - if the target model has changed, then reset the link field that is
+ probably not correct anymore
+ """
values = {}
if use_create == 'new':
values['crud_model_id'] = model_id
return {'value': values}
def on_change_write_config(self, cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=None):
- """ TODO """
+ """ When changing the object_write type configuration:
+
+ - `current`: crud_model_id is the same as base model
+ - `other`: disassemble the reference object to have its model
+ - `expression`: has its own on_change, nothing special here
+ """
values = {}
if use_write == 'current':
values['crud_model_id'] = model_id
return {'value': values}
def on_change_write_expression(self, cr, uid, ids, write_expression, model_id, context=None):
- """ Check the write_expression, about fields and models. """
+ """ Check the write_expression and update crud_model_id accordingly """
values = {}
- valid, model_name, message = self._check_expression(cr, uid, write_expression, model_id, context=context)
- if valid:
+ if write_expression:
+ valid, model_name, message = self._check_expression(cr, uid, write_expression, model_id, context=context)
+ else:
+ valid, model_name, message = True, None, False
+ if model_id:
+ model_name = self.pool['ir.model'].browse(cr, uid, model_id, context).model
+ if not valid:
+ return {
+ 'warning': {
+ 'title': 'Incorrect expression',
+ 'message': message or 'Invalid expression',
+ }
+ }
+ if model_name:
ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', model_name)], context=context)[0]
values['crud_model_id'] = ref_model_id
return {'value': values}
- if not message:
- message = 'Invalid expression'
- return {
- 'warning': {
- 'title': 'Incorrect expression',
- 'message': message,
- }
- }
+ return {'value': {}}
+
+ def on_change_crud_model_id(self, cr, uid, ids, crud_model_id, context=None):
+ """ When changing the CRUD model, update its stored name also """
+ crud_model_name = False
+ if crud_model_id:
+ crud_model_name = self.pool.get('ir.model').browse(cr, uid, crud_model_id, context).model
+ values = {'link_field_id': False, 'crud_model_name': crud_model_name}
+ return {'value': values}
- def build_expression(self, field_name, sub_field_name):
- """Returns a placeholder expression for use in a template field,
- based on the values provided in the placeholder assistant.
+ def _build_expression(self, field_name, sub_field_name):
+ """ Returns a placeholder expression for use in a template field,
+ based on the values provided in the placeholder assistant.
- :param field_name: main field name
- :param sub_field_name: sub field name (M2O)
- :return: final placeholder expression
+ :param field_name: main field name
+ :param sub_field_name: sub field name (M2O)
+ :return: final placeholder expression
"""
expression = ''
if field_name:
if res_ids:
result.update({
'sub_object': res_ids[0],
- 'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
+ 'copyvalue': self._build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
'sub_model_object_field': sub_model_object_field or False,
})
else:
result.update({
- 'copyvalue': self.build_expression(field_value.name, False),
+ 'copyvalue': self._build_expression(field_value.name, False),
})
return {'value': result}
+ def onchange_id_object(self, cr, uid, ids, id_object, context=None):
+ if id_object:
+ ref_model, ref_id = id_object.split(',')
+ return {'value': {'id_value': ref_id}}
+ return {'value': {'id_value': False}}
+
def create_action(self, cr, uid, ids, context=None):
""" Create a contextual action for each of the server actions. """
for action in self.browse(cr, uid, ids, context=context):
def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):
if not action.action_id:
raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
- return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
+ return self.pool[action.action_id.type].read(cr, uid, [action.action_id.id], context=context)[0]
- def run_action_code(self, cr, uid, action, eval_context=None, context=None):
+ def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None):
eval(action.code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action'
if 'action' in eval_context:
return eval_context['action']
def run_action_trigger(self, cr, uid, action, eval_context=None, context=None):
""" Trigger a workflow signal, depending on the use_relational_model:
- - `base`: base_model_pool.signal_<TRIGGER_NAME>(cr, uid, context.get('active_id'))
- - `relational`: find the related model and object, using the relational
- field, then target_model_pool.signal_<TRIGGER_NAME>(cr, uid, target_id)
- """
- obj_pool = self.pool[action.model_id.model]
- if action.use_relational_model == 'base':
- target_id = context.get('active_id')
- target_pool = obj_pool
- else:
- value = getattr(obj_pool.browse(cr, uid, context.get('active_id'), context=context), action.wkf_field_id.name)
- if action.wkf_field_id.ttype == 'many2one':
- target_id = value.id
- else:
- target_id = value
- target_pool = self.pool[action.wkf_model_id.model]
- trigger_name = action.wkf_transition_id.signal
+ - `base`: base_model_pool.signal_workflow(cr, uid, context.get('active_id'), <TRIGGER_NAME>)
+ - `relational`: find the related model and object, using the relational
+ field, then target_model_pool.signal_workflow(cr, uid, target_id, <TRIGGER_NAME>)
+ """
+ # weird signature and calling -> no self.env, use action param's
+ record = action.env[action.model_id.model].browse(context['active_id'])
+ if action.use_relational_model == 'relational':
+ record = getattr(record, action.wkf_field_id.name)
+ if not isinstance(record, openerp.models.BaseModel):
+ record = action.env[action.wkf_model_id.model].browse(record)
- workflow.trg_validate(uid, target_pool._name, target_id, trigger_name, cr)
+ record.signal_workflow(action.wkf_transition_id.signal)
def run_action_multi(self, cr, uid, action, eval_context=None, context=None):
- # TDE FIXME: loops are not considered here ^^
- res = []
+ res = False
for act in action.child_ids:
- # context['active_id'] = context['active_ids'][0]
- result = self.run(cr, uid, [act.id], context)
+ result = self.run(cr, uid, [act.id], context=context)
if result:
- res.append(result)
- return res and res[0] or False
+ res = result
+ return res
def run_action_object_write(self, cr, uid, action, eval_context=None, context=None):
+ """ Write server action.
+
+ - 1. evaluate the value mapping
+ - 2. depending on the write configuration:
+
+ - `current`: id = active_id
+ - `other`: id = from reference object
+ - `expression`: id = from expression evaluation
+ """
res = {}
for exp in action.fields_lines:
- if exp.type == 'equation':
- expr = eval(exp.value, eval_context)
- else:
- expr = exp.value
- res[exp.col1.name] = expr
+ res[exp.col1.name] = exp.eval_value(eval_context=eval_context)[exp.id]
if action.use_write == 'current':
model = action.model_id.model
obj_pool.write(cr, uid, [ref_id], res, context=context)
def run_action_object_create(self, cr, uid, action, eval_context=None, context=None):
+ """ Create and Copy server action.
+
+ - 1. evaluate the value mapping
+ - 2. depending on the write configuration:
+
+ - `new`: new record in the base model
+ - `copy_current`: copy the current record (id = active_id) + gives custom values
+ - `new_other`: new record in target model
+ - `copy_other`: copy the current record (id from reference object)
+ + gives custom values
+ """
res = {}
for exp in action.fields_lines:
- if exp.type == 'equation':
- expr = eval(exp.value, eval_context)
- else:
- expr = exp.value
- res[exp.col1.name] = expr
+ res[exp.col1.name] = exp.eval_value(eval_context=eval_context)[exp.id]
if action.use_create in ['new', 'copy_current']:
model = action.model_id.model
if action.link_new_record and action.link_field_id:
self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
+ def _get_eval_context(self, cr, uid, action, context=None):
+ """ Prepare the context used when evaluating python code, like the
+ condition or code server actions.
+
+ :param action: the current server action
+ :type action: browse record
+ :returns: dict -- evaluation context given to (safe_)eval """
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ obj_pool = self.pool[action.model_id.model]
+ obj = None
+ if context.get('active_model') == action.model_id.model and context.get('active_id'):
+ obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
+ return {
+ 'self': obj_pool,
+ 'object': obj,
+ 'obj': obj,
+ 'pool': self.pool,
+ 'time': time,
+ 'datetime': datetime,
+ 'dateutil': dateutil,
+ 'cr': cr,
+ 'uid': uid,
+ 'user': user,
+ 'context': context,
+ 'workflow': workflow,
+ 'Warning': openerp.exceptions.Warning,
+ }
+
def run(self, cr, uid, ids, context=None):
- """ Run the server action, by check the condition and then calling
- run_action_<STATE>, i.e. run_action_code, allowing easy inheritance
- of the server actions.
+ """ Runs the server action. For each server action, the condition is
+ checked. Note that a void (``False``) condition is considered as always
+ valid. If it is verified, the run_action_<STATE> method is called. This
+ allows easy overriding of the server actions.
- A void (aka False) condition is considered as always valid.
+ :param dict context: context should contain following keys
- Note coming from previous implementation: FIXME: refactor all the eval()
- calls in run()!
+ - active_id: id of the current object (single mode)
+ - active_model: current model that should equal the action's model
- :param dict context: context should contain following keys:
- - active_id: current id of the object
- - active_model: current model that should equal the action's model
- - TDE: ?? ids: original ids
+ The following keys are optional:
- :return: False: finished correctly or action_id: action to lanch
+ - active_ids: ids of the current records (mass mode). If active_ids
+ and active_id are present, active_ids is given precedence.
+
+ :return: an action_id to be executed, or False is finished correctly without
+ return action
"""
if context is None:
context = {}
res = False
- user = self.pool.get('res.users').browse(cr, uid, uid)
for action in self.browse(cr, uid, ids, context):
- obj = None
- obj_pool = self.pool[action.model_id.model]
- if context.get('active_model') == action.model_id.model and context.get('active_id'):
- obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
- cxt = {
- 'self': obj_pool,
- 'object': obj,
- 'obj': obj,
- 'pool': self.pool,
- 'time': time,
- 'cr': cr,
- 'context': dict(context), # copy context to prevent side-effects of eval
- 'uid': uid,
- 'user': user
- }
- # evaluate the condition, with the specific case that a void (aka False) condition is considered as True
+ eval_context = self._get_eval_context(cr, uid, action, context=context)
condition = action.condition
- if action.condition is False:
+ if condition is False:
+ # Void (aka False) conditions are considered as True
condition = True
- expr = eval(str(condition), cxt)
- if not expr:
- continue
- # call the method related to the action: run_action_<STATE>
- if hasattr(self, 'run_action_%s' % action.state):
- res = getattr(self, 'run_action_%s' % action.state)(cr, uid, action, eval_context=cxt, context=context)
+ if hasattr(self, 'run_action_%s_multi' % action.state):
+ run_context = eval_context['context']
+ expr = eval(str(condition), eval_context)
+ if not expr:
+ continue
+ # call the multi method
+ func = getattr(self, 'run_action_%s_multi' % action.state)
+ res = func(cr, uid, action, eval_context=eval_context, context=run_context)
+
+ elif hasattr(self, 'run_action_%s' % action.state):
+ func = getattr(self, 'run_action_%s' % action.state)
+ active_id = context.get('active_id')
+ active_ids = context.get('active_ids', [active_id] if active_id else [])
+ for active_id in active_ids:
+ # run context dedicated to a particular active_id
+ run_context = dict(context, active_ids=[active_id], active_id=active_id)
+ eval_context["context"] = run_context
+ expr = eval(str(condition), eval_context)
+ if not expr:
+ continue
+ # call the single method related to the action: run_action_<STATE>
+ res = func(cr, uid, action, eval_context=eval_context, context=run_context)
return res
-class act_window_close(osv.osv):
- _name = 'ir.actions.act_window_close'
- _inherit = 'ir.actions.actions'
- _table = 'ir_actions'
+class ir_server_object_lines(osv.osv):
+ _name = 'ir.server.object.lines'
+ _description = 'Server Action value mapping'
+ _sequence = 'ir_actions_id_seq'
+
+ _columns = {
+ 'server_id': fields.many2one('ir.actions.server', 'Related Server Action'),
+ 'col1': fields.many2one('ir.model.fields', 'Field', required=True),
+ 'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
+ "When Formula type is selected, this field may be a Python expression "
+ " that can use the same values as for the condition field on the server action.\n"
+ "If Value type is selected, the value will be used directly without evaluation."),
+ 'type': fields.selection([
+ ('value', 'Value'),
+ ('equation', 'Python expression')
+ ], 'Evaluation Type', required=True, change_default=True),
+ }
+
_defaults = {
- 'type': 'ir.actions.act_window_close',
+ 'type': 'value',
}
-act_window_close()
-# This model use to register action services.
+ def eval_value(self, cr, uid, ids, eval_context=None, context=None):
+ res = dict.fromkeys(ids, False)
+ for line in self.browse(cr, uid, ids, context=context):
+ expr = line.value
+ if line.type == 'equation':
+ expr = eval(line.value, eval_context)
+ elif line.col1.ttype in ['many2one', 'integer']:
+ try:
+ expr = int(line.value)
+ except Exception:
+ pass
+ res[line.id] = expr
+ return res
+
+
TODO_STATES = [('open', 'To Do'),
('done', 'Done')]
TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'),
'ir.actions.actions', 'Action', select=True, required=True),
'sequence': fields.integer('Sequence'),
'state': fields.selection(TODO_STATES, string='Status', required=True),
- 'name': fields.char('Name', size=64),
+ 'name': fields.char('Name'),
'type': fields.selection(TODO_TYPES, 'Type', required=True,
help="""Manual: Launched manually.
Automatic: Runs whenever the system is reconfigured.
}
_order="sequence,id"
+ def name_get(self, cr, uid, ids, context=None):
+ return [(rec.id, rec.action_id.name) for rec in self.browse(cr, uid, ids, context=context)]
+
+ def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
+ if args is None:
+ args = []
+ if name:
+ ids = self.search(cr, user, [('action_id', operator, name)] + args, limit=limit)
+ return self.name_get(cr, user, ids, context=context)
+ return super(ir_actions_todo, self).name_search(cr, user, name, args=args, operator=operator, context=context, limit=limit)
+
+
def action_launch(self, cr, uid, ids, context=None):
""" Launch Action of Wizard"""
wizard_id = ids and ids[0] or False
wizard.write({'state': 'done'})
# Load action
- act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
+ act_type = wizard.action_id.type
- res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context)
- if act_type['type'] != 'ir.actions.act_window':
+ res = self.pool[act_type].read(cr, uid, [wizard.action_id.id], [], context=context)[0]
+ if act_type != 'ir.actions.act_window':
return res
res.setdefault('context','{}')
res['nodestroy'] = True
'todo': len(total) - len(done)
}
-ir_actions_todo()
-class act_client(osv.osv):
+class ir_actions_act_client(osv.osv):
_name = 'ir.actions.client'
_inherit = 'ir.actions.actions'
_table = 'ir_act_client'
def _get_params(self, cr, uid, ids, field_name, arg, context):
result = {}
+ # Need to remove bin_size from context, to obtains the binary and not the length.
+ context = dict(context, bin_size_params_store=False)
for record in self.browse(cr, uid, ids, context=context):
result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
return result
self.write(cr, uid, id, {'params_store': field_value}, context=context)
_columns = {
- 'name': fields.char('Action Name', required=True, size=64, translate=True),
- 'tag': fields.char('Client action tag', size=64, required=True,
+ 'name': fields.char('Action Name', required=True, translate=True),
+ 'tag': fields.char('Client action tag', required=True,
help="An arbitrary string, interpreted by the client"
" according to its own needs and wishes. There "
"is no central tag repository across clients."),
- 'res_model': fields.char('Destination Model', size=64,
+ 'res_model': fields.char('Destination Model',
help="Optional model, mostly used for needactions."),
- 'context': fields.char('Context Value', size=250, required=True,
+ 'context': fields.char('Context Value', required=True,
help="Context dictionary as Python expression, empty by default (Default: {})"),
'params': fields.function(_get_params, fnct_inv=_set_params,
type='binary',
'context': '{}',
}
-act_client()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: