[REF] ir_actions: refactor and simplify ir.actions.server

Main idea of this commit is to simplify the various options of
ir.actions.server model. Purpose is to make it a simple tool to
execute some actions or python code. This has two main consequences

 * create and update server actions are simplified to update only the
   current record. Complex options can always be achieved by python code
   instead of error-prone option building mechanism;
 * condition of server actions is removed. If a condition is necessary it
   can either be included in a code server action, or integrated in an
   automated action;

This commit features :

 * remove 'client_action' server action type. Indeed this can be achieved
   with a code server action returning an action;
 * remove 'condition' on ir_actions_server model. From now on conditions
   and triggers are managed in server action code or using automated
   or scheduled action;
 * clean 'create and copy a record'. Create and link are the proposed
   options. Copy of the record or of a selected record is removed. Link
   is simplified by removing the parsed expression that allowed complex
   path update. It is simpler to write code than write python-like
   expression in char fields;
 * remove 'use_write' options. It now simply updates a record;
 * remove 'write_expression' as it is replaced by code actions;
 * remove 'ref_object' not used anymore with the removal of "choose
   a record";
 * remove helper to build an expression not used anymore with the removal
   of "write expression";
 * remove object ID finder;
 * removed template display when choosing an email template; just go
   on the template if you want more details;
 * refactor server action form view to reflect changes. It will be used
   in future commits as base view for automated and scheduled actions;
 * add 'sequence' in list view;
 * add new 'usage' selection field allowing to distinguish which model
   uses the server action. Indeed as server action will soon be used
   in scheduled or automated actions having a clue about the usage
   is always helpful;

Thanks to @fpodoo for the original idea and preliminary work. Thanks to
@jpr-odoo for first developments. Thanks to @jem-odoo and @rco-odoo
for reviewing.
This commit is contained in:
Yannick Tivisse
2017-01-03 19:11:06 +01:00
committed by Thibault Delavallée
parent 452a97d9d4
commit 8fa9f7d6d4
15 changed files with 70 additions and 517 deletions
@@ -11,7 +11,6 @@
<record id="action_email_reminder_lead" model="ir.actions.server">
<field name="name">Reminder to User</field>
<field name="model_id" ref="model_crm_lead"/>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="state">email</field>
<field name="template_id" ref="email_template_opportunity_reminder_mail"/>
@@ -38,7 +37,6 @@
<record id="action_set_team_sales_department" model="ir.actions.server">
<field name="name">Set team to Sales Department</field>
<field name="model_id" ref="model_crm_lead"/>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="state">code</field>
<field name="code">record.write({'team_id': env.ref('sales_team.team_sales_department').id})</field>
-1
View File
@@ -774,7 +774,6 @@ if record:
<field name="model_id" ref="crm.model_crm_team"/>
<field name="state">code</field>
<field name="code">action = model.action_your_pipeline()</field>
<field eval="True" name="condition"/>
</record>
<record model="ir.actions.act_window" id="crm_lead_opportunities_tree_view">
@@ -51,7 +51,6 @@
<record id="contract_set_as_pending" model="ir.actions.server">
<field name="name">HR Contract: set as pending</field>
<field name="model_id" ref="model_hr_contract"/>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="state">code</field>
<field name="code">record.set_as_pending()</field>
@@ -82,7 +81,6 @@
<record id="contract_set_as_close" model="ir.actions.server">
<field name="name">HR Contract: set as close</field>
<field name="model_id" ref="model_hr_contract"/>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="state">code</field>
<field name="code">record.set_as_close()</field>
@@ -8,7 +8,6 @@
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_hr_timesheet_current_open"/>
<field name="code">action = model.open_timesheet()</field>
<field name="condition">True</field>
<field name="name">My Timesheet</field>
</record>
-1
View File
@@ -415,7 +415,6 @@
<record id="action_server_lunch_archive_product" model="ir.actions.server">
<field name="name">Archive/Restore</field>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_lunch_product" />
<field name="state">code</field>
-5
View File
@@ -11,11 +11,6 @@ class ServerActions(models.Model):
_inherit = ['ir.actions.server']
state = fields.Selection(selection_add=[('email', 'Send Email')])
email_from = fields.Char('From', related='template_id.email_from', readonly=True)
email_to = fields.Char('To (Emails)', related='template_id.email_to', readonly=True)
partner_to = fields.Char('To (Partners)', related='template_id.partner_to', readonly=True)
subject = fields.Char('Subject', related='template_id.subject', readonly=True)
body_html = fields.Html('Body', related='template_id.body_html', readonly=True)
template_id = fields.Many2one(
'mail.template', 'Email Template', ondelete='set null',
domain="[('model_id', '=', model_id)]",
-21
View File
@@ -16,27 +16,6 @@
<group attrs="{'invisible': [('model_id', '=', False)]}">
<field name="template_id"
attrs="{'required': [('state', '=', 'email')]}"/>
<p colspan="2" attrs="{'invisible': [('template_id', '!=', False)]}">
Choose a template to display its values.
</p>
<p colspan="2" attrs="{'invisible': [('template_id', '=', False)]}">
The values displayed hereunder are informative. When sending the email, the values
will be taken from the email template.
</p>
</group>
<group attrs="{'invisible': ['|', ('model_id', '=', False), ('template_id', '=', False)]}">
<label for="email_from"/>
<div>
<field name="email_from" nolabel="1'" readonly="1"
attrs="{'required': [('state', '=', 'email')]}"/>
<p attrs="{'invisible': [('email_from', '!=', False)]}">
Your template does not defined the email address of the sender. Please update your template.
</p>
</div>
<field name="email_to" readonly="1"/>
<field name="partner_to" readonly="1"/>
<field name="subject" readonly="1" attrs="{'required': [('state', '=', 'email')]}"/>
<field name="body_html" readonly="1" attrs="{'required': [('state', '=', 'email')]}"/>
</group>
</page>
</xpath>
@@ -5,7 +5,6 @@
<field name="state">code</field>
<field name="code">True</field>
<field name="sequence" eval="5"/>
<field name="condition" eval="True"/>
</record>
<!-- Email template -->
-1
View File
@@ -635,7 +635,6 @@
<record id="survey_action_server_clean_test_answers" model="ir.actions.server">
<field name="name">Clean test answers</field>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_survey_survey" />
<field name="state">code</field>
-2
View File
@@ -36,7 +36,6 @@
<record id="action_partner_post" model="ir.actions.server">
<field name="name">Website Partner Post and Thanks Demo</field>
<field name="condition">True</field>
<field name="website_published" eval="True"/>
<field name="website_path">partner_thanks</field>
<field name="model_id" ref="base.model_res_partner"/>
@@ -112,7 +111,6 @@ response = request.website.render("website.template_partner_post", {'partner': p
<record id="action_partner_comment" model="ir.actions.server">
<field name="name">Website Partners Comment Form</field>
<field name="condition">True</field>
<field name="website_published" eval="True"/>
<field name="website_path">partner_comment</field>
<field name="model_id" ref="base.model_res_partner"/>
@@ -11,8 +11,6 @@
<xpath expr="//field[@name='state']" position="after">
<field name="website_published"
attrs="{'invisible': [('state', '!=', 'code')]}"/>
</xpath>
<xpath expr="//field[@name='sequence']" position="after">
<field name="xml_id" invisible="1"/>
<field name="website_path"
attrs="{'invisible': ['|', ('website_published', '!=', True), ('state', '!=', 'code')]}"/>
+21 -260
View File
@@ -457,23 +457,17 @@ class IrActionsServer(models.Model):
name = fields.Char(string='Action Name', translate=True)
type = fields.Char(default='ir.actions.server')
condition = fields.Char(default="True", groups='base.group_system',
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 'record.list_price > 5000'. A void "
"condition is considered as always True. Help about python expression "
"is given in the help tab.")
usage = fields.Selection([
('ir_actions_server', 'Server Action')], string='Usage',
default='ir_actions_server', required=True)
state = fields.Selection([
('code', 'Execute Python Code'),
('client_action', 'Run a Client Action'),
('object_create', 'Create or Copy a new Record'),
('object_write', 'Write on a Record'),
('object_create', 'Create a new Record'),
('object_write', 'Update the Record'),
('multi', 'Execute several actions')], string='Action To Do',
default='code', required=True, # set default to write ?
help="Type of server action. The following values are available:\n"
"- 'Execute Python Code': a block of python code that will be executed\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"
"- 'Write on a Record': update the values of a record\n"
"- 'Execute several actions': define an action that triggers several other server actions\n"
@@ -488,9 +482,6 @@ class IrActionsServer(models.Model):
menu_ir_values_id = fields.Many2one('ir.values', string='Action on Object',
copy=False, readonly=True,
help='IrValues entry of the related more menu entry action')
# Client Action
action_id = fields.Many2one('ir.actions.actions', string='Client Action',
help="Select the client action that has to be executed.")
# Python code
code = fields.Text(string='Python Code', groups='base.group_system',
default=DEFAULT_PYTHON_CODE,
@@ -499,79 +490,15 @@ class IrActionsServer(models.Model):
# Multi
child_ids = fields.Many2many('ir.actions.server', 'rel_server_actions', 'server_id', 'action_id',
string='Child Actions', help='Child server actions that will be executed. Note that the last return returned action value will be used as global return value.')
# Create/Copy/Write
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', 'Choose and copy a record in the database')],
string="Creation Policy", default='new', required=True)
# Create
crud_model_id = fields.Many2one('ir.model', string='Create/Write 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.Char(string='Create/Write Target Model Name', related='crud_model_id.model', store=True, readonly=True)
ref_object = fields.Reference(string='Reference record', selection='_select_objects', oldname='copy_object')
link_new_record = fields.Boolean(string='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.")
crud_model_name = fields.Char(related='crud_model_id.name', readonly=True)
link_field_id = fields.Many2one('ir.model.fields', string='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'),
('expression', 'Update a record linked to the current record using python'),
('other', 'Choose and Update a record in the database')],
string='Update Policy', default='current', required=True)
write_expression = fields.Char(string='Expression', oldname='write_id',
help="Provide an expression that, applied on the current record, gives the field to update.")
help="Provide the field used to link the newly created record"
"on the record on used by the server action.")
fields_lines = fields.One2many('ir.server.object.lines', 'server_id', string='Value Mapping', copy=True)
# Fake fields used to implement the placeholder assistant
model_object_field = fields.Many2one('ir.model.fields', string="Field",
help="Select target field from the related document model.\n"
"If it is a relationship field you will be able to select "
"a target field at the destination of the relationship.")
sub_object = fields.Many2one('ir.model', string='Sub-model', readonly=True,
help="fWhen a relationship field is selected as first field, "
"this field shows the document model the relationship goes to.")
sub_model_object_field = fields.Many2one('ir.model.fields', string='Sub-field',
help="When a relationship field is selected as first field, "
"this field lets you select the target field within the "
"destination document model (sub-model).")
copyvalue = fields.Char(string='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(string='Record', selection='_select_objects')
id_value = fields.Char(string='Record ID')
def _check_expression(self, expression, model):
""" 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 model: a record of the model 'ir.model'
:returns tuple: (is_valid, target_model_name, error_msg)
"""
if not model:
return (False, None, 'Your expression cannot be validated because the Base Model is not set.')
# fetch current model
model_name = model.model
# transform expression into a path that should look like 'object.many2onefield.many2onefield'
path = expression.split('.')
initial = path.pop(0)
if initial not in ['obj', 'object', 'record']:
return (False, None, 'Your expression should begin with obj, object, record.\nAn expression builder is available in the help tab.')
# analyze path
while path:
step = path.pop(0)
field = self.env[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, model_name))
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':
model_name = field.comodel_name
return (True, model_name, None)
@api.constrains('code')
def _check_python_code(self):
for action in self.sudo().filtered('code'):
@@ -579,142 +506,19 @@ class IrActionsServer(models.Model):
if msg:
raise ValidationError(msg)
@api.constrains('write_expression', 'model_id')
def _check_write_expression(self):
for record in self:
if record.write_expression and record.model_id:
correct, model_name, message = self._check_expression(record.write_expression, record.model_id)
if not correct:
_logger.warning('Invalid expression: %s' % message)
raise ValidationError(_('Incorrect Write Record Expression'))
@api.constrains('child_ids')
def _check_recursion(self):
if not self._check_m2m_recursion('child_ids'):
raise ValidationError(_('Recursion found in child server actions'))
@api.onchange('model_id')
def _onchange_model_id(self):
""" When changing the action base model, reset crud config
to ease value coherence. """
self.use_create = 'new'
self.use_write = 'current'
self.crud_model_id = self.model_id
@api.onchange('use_create', 'use_write', 'ref_object')
def _onchange_crud_config(self):
""" Wrapper on CRUD-type (create or write) on_change """
if self.state == 'object_create':
self._onchange_create_config()
elif self.state == 'object_write':
self._onchange_write_config()
def _onchange_create_config(self):
""" 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
"""
crud_model_id = self.crud_model_id
if self.use_create == 'new':
self.crud_model_id = self.model_id
elif self.use_create == 'new_other':
pass
elif self.use_create == 'copy_current':
self.crud_model_id = self.model_id
elif self.use_create == 'copy_other' and self.ref_object:
ref_model = self.ref_object._name
self.crud_model_id = self.env['ir.model'].search([('model', '=', ref_model)])
if self.crud_model_id != crud_model_id:
self.link_field_id = False
def _onchange_write_config(self):
""" 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
"""
crud_model_id = self.crud_model_id
if self.use_write == 'current':
self.crud_model_id = self.model_id
elif self.use_write == 'other' and self.ref_object:
ref_model = self.ref_object._name
self.crud_model_id = self.env['ir.model'].search([('model', '=', ref_model)])
elif self.use_write == 'expression':
pass
if self.crud_model_id != crud_model_id:
self.link_field_id = False
@api.onchange('crud_model_id')
def _onchange_crud_model_id(self):
""" When changing the CRUD model, update its stored name also """
self.link_field_id = False
self.crud_model_name = self.crud_model_id.model
@api.onchange('write_expression')
def _onchange_write_expression(self):
""" Check the write_expression and update crud_model_id accordingly """
values = {}
if self.write_expression:
valid, model_name, message = self._check_expression(self.write_expression, self.model_id)
else:
valid, model_name, message = True, None, False
if self.model_id:
model_name = self.model_id.model
if not valid:
return {
'warning': {
'title': _('Incorrect expression'),
'message': message or _('Invalid expression'),
}
}
if model_name:
self.crud_model_id = self.env['ir.model'].search([('model', '=', model_name)])
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
"""
expression = ''
if field_name:
expression = "object." + field_name
if sub_field_name:
expression += "." + sub_field_name
return expression
@api.onchange('model_object_field', 'sub_model_object_field')
def _onchange_model_object_field(self):
field = self.model_object_field
sub_field = self.sub_model_object_field
self.sub_object = False
self.sub_model_object_field = False
self.copyvalue = False
if field:
if field.ttype in ['many2one', 'one2many', 'many2many']:
comodel = self.env['ir.model'].search([('model', '=', field.relation)])
if comodel:
self.sub_object = comodel
self.copyvalue = self._build_expression(field.name, sub_field.name)
self.sub_model_object_field = sub_field
else:
self.copyvalue = self._build_expression(field.name, False)
@api.onchange('id_object')
def _onchange_id_object(self):
self.id_value = self.id_object.id if self.id_object else False
@api.onchange('model_id')
def _onchange_model_id(self):
self.model_name = self.model_id.model
@api.multi
def create_action(self):
@@ -741,13 +545,6 @@ class IrActionsServer(models.Model):
raise UserError(_('Deletion of the action record failed.'))
return True
@api.model
def run_action_client_action(self, action, eval_context=None):
if not action.action_id:
raise UserError(_("Please specify an action to launch!"))
record = self.env[action.action_id.type].browse(action.action_id.id)
return record.read()[0]
@api.model
def run_action_code_multi(self, action, eval_context=None):
safe_eval(action.sudo().code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action'
@@ -778,21 +575,7 @@ class IrActionsServer(models.Model):
for exp in action.fields_lines:
res[exp.col1.name] = exp.eval_value(eval_context=eval_context)[exp.id]
if action.use_write == 'current':
model = action.model_id.model
ref_id = self._context.get('active_id')
elif action.use_write == 'other':
model = action.crud_model_id.model
ref_id = action.ref_object.id
elif action.use_write == 'expression':
model = action.crud_model_id.model
ref = safe_eval(action.write_expression, eval_context)
if isinstance(ref, models.BaseModel):
ref_id = ref.id
else:
ref_id = int(ref)
self.env[model].browse(ref_id).write(res)
self.env[action.model_id.model].browse(self._context.get('active_id')).write(res)
@api.model
def run_action_object_create(self, action, eval_context=None):
@@ -811,27 +594,16 @@ class IrActionsServer(models.Model):
for exp in action.fields_lines:
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
elif action.use_create in ['new_other', 'copy_other']:
model = action.crud_model_id.model
res = self.env[action.crud_model_id.model].create(res)
if action.use_create == 'copy_current':
ref_id = self._context.get('active_id')
res = self.env[model].browse(ref_id).copy(res)
elif action.use_create == 'copy_other':
res = action.ref_object.copy(res)
else:
res = self.env[model].create(res)
if action.link_new_record and action.link_field_id:
if action.link_field_id:
record = self.env[action.model_id.model].browse(self._context.get('active_id'))
record.write({action.link_field_id.name: res.id})
@api.model
def _get_eval_context(self, action=None):
""" Prepare the context used when evaluating python code, like the
condition or code server actions.
python formulas or code server actions.
:param action: the current server action
:type action: browse record
@@ -868,10 +640,9 @@ class IrActionsServer(models.Model):
@api.multi
def run(self):
""" 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.
""" Runs the server action. For each server action, the
run_action_<STATE> method is called. This allows easy overriding
of the server actions.
:param dict context: context should contain following keys
@@ -888,14 +659,7 @@ class IrActionsServer(models.Model):
"""
for action in self:
eval_context = self._get_eval_context(action)
condition = action.sudo().condition
if condition is False:
# Void (aka False) conditions are considered as True
condition = True
if hasattr(self, 'run_action_%s_multi' % action.state):
expr = safe_eval(str(condition), eval_context)
if not expr:
continue
# call the multi method
run_self = self.with_context(eval_context['env'].context)
func = getattr(run_self, 'run_action_%s_multi' % action.state)
@@ -908,9 +672,6 @@ class IrActionsServer(models.Model):
# run context dedicated to a particular active_id
run_self = self.with_context(active_ids=[active_id], active_id=active_id)
eval_context["env"].context = run_self._context
expr = safe_eval(str(condition), eval_context)
if not expr:
continue
# call the single method related to the action: run_action_<STATE>
func = getattr(run_self, 'run_action_%s' % action.state)
res = func(action, eval_context=eval_context)
@@ -934,7 +695,7 @@ class IrServerObjectLines(models.Model):
col1 = fields.Many2one('ir.model.fields', string='Field', required=True)
value = fields.Text(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"
" that can use the same values as for the code 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', default='value', required=True, change_default=True)
+37 -123
View File
@@ -262,128 +262,66 @@
<field name="model">ir.actions.server</field>
<field name="arch" type="xml">
<form string="Server Action">
<header>
<field name="menu_ir_values_id" invisible="1"/>
<button name="create_action" string="Create Action" type="object"
class="btn-primary"
attrs="{'invisible':[('menu_ir_values_id','!=',False)]}"
help="Display an option in the 'More' top-menu or related document, to run this action."/>
<button name="unlink_action" string="Remove Action" type="object"
attrs="{'invisible':[('menu_ir_values_id','=',False)]}"
help="Remove the contextual action related to this server action"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<field name="menu_ir_values_id" invisible="1"/>
<button name="create_action" string="Add in the 'More' menu" type="object"
attrs="{'invisible':[('menu_ir_values_id','!=',False)]}"
help="Display an option on related documents to run this sever action"/>
<button name="unlink_action" string="Remove from the 'More' menu" type="object"
attrs="{'invisible':[('menu_ir_values_id','=',False)]}"
help="Remove the contextual action related to this server action"/>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name" placeholder="e.g. Update order quantity"/></h1>
</div>
<group>
<group>
<field name="type" invisible="1"/>
<group name="action_wrapper">
<group name="action_content">
<field name="model_id"/>
<field name="model_name" invisible="1"/>
<field name="state"/>
</group>
<group>
<field name="condition"/>
<field name="sequence"/>
<field name="type" invisible="1"/>
<field name="crud_model_id"
attrs="{'invisible': [('state', '!=', 'object_create')], 'required': [('state', '=', 'object_create')]}"/>
<field name="crud_model_name" invisible="1"/>
<field name="link_field_id"
domain="[('model_id', '=', model_id), ('relation', '=', crud_model_name), ('ttype', 'in', ['many2one'])]"
attrs="{'invisible': [('state', '!=', 'object_create')]}"/>
</group>
</group>
<notebook colspan="4">
<notebook>
<page string="Python Code" name='code' autofocus="autofocus"
attrs="{'invisible': [('state', '!=', 'code')]}">
<field name="code" widget="ace" options="{'mode': 'python'}" placeholder="Enter Python code here. Help about Python expression is available in the help tab of this document."/>
</page>
<page string="Client" autofocus="autofocus"
attrs="{'invisible': [('state', '!=', 'client_action')]}">
<group>
<field name="action_id" attrs="{'required':[('state', '=', 'client_action')]}"/>
</group>
</page>
<page string="Create / Write / Copy" autofocus="autofocus"
<page string="Data to Write" autofocus="autofocus"
attrs="{'invisible':[('state', 'not in', ['object_create', 'object_write'])]}">
<p attrs="{'invisible': [('model_id', '!=', False)]}">
Please set the Base Model before setting the action details.
Please set the Model to Create before choosing values
</p>
<group attrs="{'invisible': [('model_id', '=', False)]}">
<field name="use_create" widget="radio"
attrs="{'invisible': [('state', '!=', 'object_create')]}"/>
<field name="use_write" widget="radio"
attrs="{'invisible': [('state', '!=', 'object_write')]}"/>
<label for="ref_object" string=" "
attrs="{'invisible': ['&amp;',
'|', ('state', '!=', 'object_write'), ('use_write', '!=', 'other'),
'|', ('state', '!=', 'object_create'), ('use_create', '!=', 'copy_other')]}"/>
<div style="margin-left: 24px;"
attrs="{'invisible': ['&amp;',
'|', ('state', '!=', 'object_write'), ('use_write', '!=', 'other'),
'|', ('state', '!=', 'object_create'), ('use_create', '!=', 'copy_other')]}">
<field name="ref_object" nolabel="1"/>
</div>
<field name="crud_model_id"
attrs="{'invisible': ['|', ('state', '!=', 'object_create'), ('use_create', '!=', 'new_other')]}"/>
<field name="crud_model_name" invisible="1"/>
<label for="link_new_record" attrs="{'invisible': [('state', '!=', 'object_create')]}"/>
<div attrs="{'invisible': [('state', '!=', 'object_create')]}">
<field name="link_new_record" nolabel="1" style="display: inline-block;"/>
<p class="oe_grey oe_edit_only" style="display: inline-block; margin: 0px 0px 0px 8px;">
Check to attach the newly created record to the record on which the server action runs.
</p>
<group>
<field name="model_name" invisible="1"/>
<field name="link_field_id"
domain="[('model_id', '=', crud_model_id), ('relation', '=', model_name), ('ttype', 'in', ['many2one'])]"
attrs="{'required': [('state', '=', 'object_create'), ('link_new_record', '=', True)],
'invisible': ['|', ('state', '!=', 'object_create'), ('link_new_record', '=', False)]}"/>
</group>
</div>
<label for="link_new_record" attrs="{'invisible': ['|', ('state', '!=', 'object_write'), ('use_write', '!=', 'expression')]}"/>
<div attrs="{'invisible': ['|', ('state', '!=', 'object_write'), ('use_write', '!=', 'expression')]}">
<p class="oe_grey oe_edit_only" style="margin: 0px;">
Write a python expression, beginning with record, that gives the record to update. An expression builder is available in the help tab. Examples:
</p>
<ul class="oe_grey oe_edit_only">
<li>record.partner_id</li>
<li>record.partner_id.currency_id</li>
</ul>
<field name="write_expression"
attrs="{'required': [('state', '=', 'object_write'), ('use_write', '=', 'expression')]}"/>
</div>
<field name="fields_lines">
<tree string="Field Mappings" editable="top">
<field name="col1" domain="[('model_id', '=', parent.crud_model_id)]"/>
<field name="type"/>
<field name="value"/>
</tree>
<form string="Field Mapping">
<group >
<field name="col1" domain="[('model_id', '=', parent.crud_model_id)]"/>
<field name="type"/>
<field name="value"/>
</group>
</form>
</field>
</group>
<field name="fields_lines">
<tree string="Field Mappings" editable="bottom">
<field name="col1" domain="['|', ('model_id', '=', parent.crud_model_id), ('model_id', '=', parent.model_id)]"/>
<field name="type"/>
<field name="value"/>
</tree>
</field>
</page>
<page string="Execute several actions" autofocus="autofocus"
<page string="Actions" autofocus="autofocus"
attrs="{'invisible': [('state', '!=', 'multi')]}">
<p class="oe_grey">
If several child actions return an action, only the last one will be executed.
This may happen when having server actions executing code that returns an action, or server actions returning a client action.
</p>
<field name="child_ids"
domain="[('model_id', '=', model_id)]"/>
<field name="child_ids" domain="[('model_id', '=', model_id)]"/>
</page>
<page string="Help">
<page string="Help"
attrs="{'invisible': [('state', '!=', 'code')]}">
<group>
<div style="margin-top: 4px;">
<h3>Help with Python expressions.</h3>
@@ -394,13 +332,6 @@
<li>time, datetime, timezone: Python module</li>
<li>log(message, level='info'): a logging function to create ir_logging records</li>
</ul>
<div>
<p>Example of condition expression using Python</p>
<ul>
<li>condition: True</li>
<li>condition: record.list_price > 5000</li>
</ul>
</div>
<div attrs="{'invisible': [('state', '!=', 'code')]}">
<p>Example of python code</p>
<code style='white-space: pre-wrap'>
@@ -409,25 +340,6 @@ env["res.partner"].create({"name": partner_name})
</code>
</div>
</div>
<group>
<h3 colspan="2">Dynamic expression builder</h3>
<p colspan="2" attrs="{'invisible': [('model_id', '!=', False)]}">
Please set the Base Model of the action to enable the dynamic expression buidler.
</p>
<field name="model_object_field"
attrs="{'invisible': [('model_id', '=', False)]}"
domain="[('model_id', '=', model_id), ('ttype', '!=', 'one2many'), ('ttype', '!=', 'many2many')]"/>
<field name="sub_object" readonly="1" attrs="{'invisible': [('model_id', '=', False)]}"/>
<field name="sub_model_object_field"
domain="[('model_id', '=', sub_object), ('ttype', '!=', 'one2many'), ('ttype', '!=', 'many2many')]"
attrs="{'readonly':[('sub_object','=',False)],
'required':[('sub_object','!=',False)],
'invisible': [('model_id', '=', False)]}"/>
<field name="copyvalue" attrs="{'invisible': [('model_id', '=', False)]}"/>
<h3 colspan="2">Find the ID of a record in the database</h3>
<field name="id_object"/>
<field name="id_value" />
</group>
</group>
</page>
</notebook>
@@ -440,9 +352,11 @@ env["res.partner"].create({"name": partner_name})
<field name="model">ir.actions.server</field>
<field name="arch" type="xml">
<tree string="Server Actions">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="state"/>
<field name="model_id"/>
<field name="usage"/>
</tree>
</field>
</record>
@@ -457,6 +371,7 @@ env["res.partner"].create({"name": partner_name})
<group expand="0" string="Group By" colspan="4" col="4">
<filter string="Action Type" domain="[]" context="{'group_by':'state'}"/>
<filter string="Model" domain="[]" context="{'group_by':'model_id'}"/>
<filter string="Usage" domain="[]" context="{'group_by':'usage'}"/>
</group>
</search>
</field>
@@ -540,7 +455,6 @@ env["res.partner"].create({"name": partner_name})
<menuitem id="menu_ir_actions_todo_form" action="act_ir_actions_todo_form" parent="base.next_id_6"/>
<record id="action_run_ir_action_todo" model="ir.actions.server">
<field name="name">Run Remaining Action Todo</field>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_res_config"/>
<field name="state">code</field>
@@ -3,7 +3,6 @@
<data>
<record id="action_server_module_immediate_install" model="ir.actions.server">
<field name="name">Module Immediate Install</field>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_ir_module_module" />
<field name="state">code</field>
+12 -94
View File
@@ -42,7 +42,6 @@ class TestServerActionsBase(common.TransactionCase):
# create server action to
self.action = self.env['ir.actions.server'].create({
'name': 'TestAction',
'condition': 'True',
'model_id': self.res_partner_model.id,
'state': 'code',
'code': 'record.write({"comment": "MyComment"})',
@@ -52,16 +51,10 @@ class TestServerActionsBase(common.TransactionCase):
class TestServerActions(TestServerActionsBase):
def test_00_action(self):
# Do: eval 'True' condition
self.action.with_context(self.context).run()
self.assertEqual(self.test_partner.comment, 'MyComment', 'ir_actions_server: invalid condition check')
self.test_partner.write({'comment': False})
# Do: eval False condition, that should be considered as True (void = True)
self.action.write({'condition': False})
self.action.with_context(self.context).run()
self.assertEqual(self.test_partner.comment, 'MyComment', 'ir_actions_server: invalid condition check')
# Do: create contextual action
self.action.create_action()
@@ -90,27 +83,14 @@ class TestServerActions(TestServerActionsBase):
partners = self.test_partner.search([('name', 'ilike', 'TestingPartner_code')])
self.assertEqual(len(partners), 1, 'ir_actions_server: 1 new partner should have been created')
def test_30_client(self):
client_action = self.env['ir.actions.client'].create({
'name': 'TestAction2',
'tag': 'Test',
})
self.action.write({
'state': 'client_action',
'action_id': client_action.id,
})
res = self.action.with_context(self.context).run()
self.assertEqual(res['name'], 'TestAction2', 'ir_actions_server: incorrect return result for a client action')
def test_40_crud_create(self):
def test_20_crud_create(self):
_city = 'TestCity'
_name = 'TestNew'
# Do: create a new record in the same model and link it
self.action.write({
'state': 'object_create',
'use_create': 'new',
'link_new_record': True,
'crud_model_id': self.action.model_id.id,
'link_field_id': self.res_partner_parent_field.id,
'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': _name}),
(0, 0, {'col1': self.res_partner_city_field.id, 'value': _city})],
@@ -124,29 +104,11 @@ class TestServerActions(TestServerActionsBase):
# Test: new partner linked
self.assertEqual(self.test_partner.parent_id, partner, 'ir_actions_server: TODO')
# Do: copy current record
self.action.write({
'state': 'object_create',
'use_create': 'copy_current',
'link_new_record': False,
'fields_lines': [(5,),
(0, 0, {'col1': self.res_partner_name_field.id, 'value': 'TestCopyCurrent'}),
(0, 0, {'col1': self.res_partner_city_field.id, 'value': 'TestCity'})],
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
# Test: new partner created
partner = self.test_partner.search([('name', 'ilike', 'TestingPartner (copy)')]) # currently res_partner overrides default['name'] whatever its value
self.assertEqual(len(partner), 1, 'ir_actions_server: TODO')
self.assertEqual(partner.city, 'TestCity', 'ir_actions_server: TODO')
self.assertEqual(partner.country_id, self.test_partner.country_id, 'ir_actions_server: TODO')
# Do: create a new record in another model
self.action.write({
'state': 'object_create',
'use_create': 'new_other',
'crud_model_id': self.res_country_model.id,
'link_new_record': False,
'link_field_id': False,
'fields_lines': [(5,),
(0, 0, {'col1': self.res_country_name_field.id, 'value': 'record.name', 'type': 'equation'}),
(0, 0, {'col1': self.res_country_code_field.id, 'value': 'record.name[0:2]', 'type': 'equation'})],
@@ -158,69 +120,23 @@ class TestServerActions(TestServerActionsBase):
self.assertEqual(len(country), 1, 'ir_actions_server: TODO')
self.assertEqual(country.code, 'TE', 'ir_actions_server: TODO')
# Do: copy a record in another model
self.action.write({
'state': 'object_create',
'use_create': 'copy_other',
'crud_model_id': self.res_country_model.id,
'link_new_record': False,
'ref_object': 'res.country,%s' % self.test_country.id,
'fields_lines': [(5,),
(0, 0, {'col1': self.res_country_name_field.id, 'value': 'NewCountry', 'type': 'value'}),
(0, 0, {'col1': self.res_country_code_field.id, 'value': 'NY', 'type': 'value'})],
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
# Test: new country created
country = self.test_country.search([('name', 'ilike', 'NewCountry')])
self.assertEqual(len(country), 1, 'ir_actions_server: TODO')
self.assertEqual(country.code, 'NY', 'ir_actions_server: TODO')
self.assertEqual(country.address_format, 'SuperFormat', 'ir_actions_server: TODO')
def test_50_crud_write(self):
def test_30_crud_write(self):
_name = 'TestNew'
# Do: create a new record in the same model and link it
# Do: update partner name
self.action.write({
'state': 'object_write',
'use_write': 'current',
'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': _name})],
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
# Test: new partner created
# Test: partner updated
partner = self.test_partner.search([('name', 'ilike', _name)])
self.assertEqual(len(partner), 1, 'ir_actions_server: TODO')
self.assertEqual(partner.city, 'OrigCity', 'ir_actions_server: TODO')
# Do: copy current record
self.action.write({
'use_write': 'other',
'crud_model_id': self.res_country_model.id,
'ref_object': 'res.country,%s' % self.test_country.id,
'fields_lines': [(5,), (0, 0, {'col1': self.res_country_name_field.id, 'value': 'record.name', 'type': 'equation'})],
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
# Test: new country created
country = self.test_country.search([('name', 'ilike', 'TestNew')])
self.assertEqual(len(country), 1, 'ir_actions_server: TODO')
# Do: copy a record in another model
self.action.write({
'use_write': 'expression',
'crud_model_id': self.res_country_model.id,
'write_expression': 'record.country_id',
'fields_lines': [(5,), (0, 0, {'col1': self.res_country_name_field.id, 'value': 'NewCountry', 'type': 'value'})],
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
# Test: new country created
country = self.test_country.search([('name', 'ilike', 'NewCountry')])
self.assertEqual(len(country), 1, 'ir_actions_server: TODO')
@mute_logger('odoo.addons.base.ir.ir_model', 'odoo.models')
def test_60_multi(self):
def test_40_multi(self):
# Data: 2 server actions that will be nested
action1 = self.action.create({
'name': 'Subaction1',
@@ -233,8 +149,10 @@ class TestServerActions(TestServerActionsBase):
'name': 'Subaction2',
'sequence': 2,
'model_id': self.res_partner_model.id,
'crud_model_id': self.res_partner_model.id,
'state': 'object_create',
'use_create': 'copy_current',
'fields_lines': [(0, 0, {'col1': self.res_partner_name_field.id, 'value': 'RaoulettePoiluchette'}),
(0, 0, {'col1': self.res_partner_city_field.id, 'value': 'TestingCity'})],
})
action3 = self.action.create({
'name': 'Subaction3',
@@ -253,8 +171,8 @@ class TestServerActions(TestServerActionsBase):
# Test: new partner created
# currently res_partner overrides default['name'] whatever its value
partner = self.test_partner.search([('name', 'ilike', 'TestingPartner (copy)')])
self.assertEqual(len(partner), 1, 'ir_actions_server: TODO')
partner = self.test_partner.search([('name', 'ilike', 'RaoulettePoiluchette')])
self.assertEqual(len(partner), 1)
# Test: action returned
self.assertEqual(res.get('type'), 'ir.actions.act_url')