[REF] base_automation: cleaning and refactoring

Purpose of this commit is to continue to clean server actions, scheduled
actions and automated actions. Server actions hold code execution and
automated actions hold triggers and conditions.

This commit make automated action inherits from ir_actions_server.
Automated actions do not hold several server actions anymore. Instead
each new automated action creates its own server action holding the code
or actions to perform. Automated actions that held several server actions
should now simply use a server action of type multi. This commit also
features:

 * rename 'kind' field to trigger
 * remove 'act_user_id' as this should be done by the server action itself
   not the automated action. This can be achieved using a write or code
   server action;
 * remove 'act_followers' as this is replaced by a followers server action;
 * use a primary form view inherited from server action. This way automated
   actions use the same base form as server actions with triggers and
   conditions added;

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:08 +01:00
committed by Thibault Delavallée
parent ab13808949
commit aedde9a46c
6 changed files with 143 additions and 171 deletions
@@ -19,34 +19,46 @@
</record>
<record id="test_rule_on_create" model="base.automation">
<field name="name">Test rule on create</field>
<field name="kind">on_create</field>
<field name="name">Base Automation: test rule on create</field>
<field name="model_id" ref="base_automation.model_base_automation_lead_test"/>
<field name="state">code</field>
<field name="code" eval="'records.write({\'user_id\': %s})' % ref('base.user_demo')"/>
<field name="trigger">on_create</field>
<field name="active" eval="True"/>
<field name="model_id" ref="base_automation.model_base_automation_lead_test"/>
<field name="act_user_id" ref="base.user_demo"/>
<field name="filter_id" ref="test_filter_draft"/>
</record>
<record id="test_rule_on_write" model="base.automation">
<field name="name">Test rule on write</field>
<field name="kind">on_write</field>
<field name="name">Base Automation: test rule on write</field>
<field name="model_id" ref="base_automation.model_base_automation_lead_test"/>
<field name="state">code</field>
<field name="code" eval="'records.write({\'user_id\': %s})' % ref('base.user_demo')"/>
<field name="trigger">on_write</field>
<field name="active" eval="True"/>
<field name="model_id" ref="base_automation.model_base_automation_lead_test"/>
<field name="act_user_id" ref="base.user_demo"/>
<field name="filter_pre_id" ref="test_filter_open"/>
<field name="filter_id" ref="test_filter_done"/>
</record>
<record id="test_rule_on_recompute" model="base.automation">
<field name="name">Test rule on recompute</field>
<field name="kind">on_write</field>
<field name="name">Base Automation: test rule on recompute</field>
<field name="model_id" ref="base_automation.model_base_automation_lead_test"/>
<field name="state">code</field>
<field name="code" eval="'records.write({\'user_id\': %s})' % ref('base.user_demo')"/>
<field name="trigger">on_write</field>
<field name="active" eval="True"/>
<field name="model_id" ref="base_automation.model_base_automation_lead_test"/>
<field name="act_user_id" ref="base.user_demo"/>
<field name="filter_domain">[('customer', '=', True)]</field>
</record>
<record id="test_rule_on_line" model="base.automation">
<field name="name">Test rule on secondary record</field>
<field name="kind">on_create</field>
<field name="name">Base Automation: test rule on secondary model</field>
<field name="model_id" ref="base_automation.model_base_automation_line_test"/>
<field name="state">code</field>
<field name="code" eval="'records.write({\'user_id\': %s})' % ref('base.user_demo')"/>
<field name="trigger">on_create</field>
<field name="active" eval="True"/>
<field name="model_id" ref="base_automation.model_base_automation_line_test"/>
<field name="act_user_id" ref="base.user_demo"/>
</record>
</odoo>
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base_automation
import ir_actions
@@ -30,17 +30,19 @@ class BaseAutomation(models.Model):
_description = 'Automated Action'
_order = 'sequence'
name = fields.Char(string='Rule Name', required=True)
model_id = fields.Many2one('ir.model', string='Related Document Model', required=True, domain=[('transient', '=', False)])
model = fields.Char(related='model_id.model', readonly=True)
action_server_id = fields.Many2one(
'ir.actions.server', 'Server Actions',
domain="[('model_id', '=', model_id)]",
delegate=True, required=True, ondelete='restrict')
active = fields.Boolean(default=True, help="When unchecked, the rule is hidden and will not be executed.")
sequence = fields.Integer(help="Gives the sequence order when displaying a list of rules.")
kind = fields.Selection([('on_create', 'On Creation'),
('on_write', 'On Update'),
('on_create_or_write', 'On Creation & Update'),
('on_unlink', 'On Deletion'),
('on_change', 'Based on Form Modification'),
('on_time', 'Based on Timed Condition')], string='When to Run', required=True)
trigger = fields.Selection([
('on_create', 'On Creation'),
('on_write', 'On Update'),
('on_create_or_write', 'On Creation & Update'),
('on_unlink', 'On Deletion'),
('on_change', 'Based on Form Modification'),
('on_time', 'Based on Timed Condition')
], string='Trigger condition', required=True, oldname="kind")
trg_date_id = fields.Many2one('ir.model.fields', string='Trigger Date',
help="""When should the condition be triggered.
If present, will be checked by the scheduler. If empty, will be checked at creation and update.""",
@@ -53,34 +55,33 @@ class BaseAutomation(models.Model):
string='Delay type', default='day')
trg_date_calendar_id = fields.Many2one("resource.calendar", string='Use Calendar',
help="When calculating a day-based timed condition, it is possible to use a calendar to compute the date based on working days.")
act_user_id = fields.Many2one('res.users', string='Set Responsible')
act_followers = fields.Many2many("res.partner", string="Add Followers")
server_action_ids = fields.Many2many('ir.actions.server', string='Server Actions', domain="[('model_id', '=', model_id)]",
help="Examples: email reminders, call object service, etc.")
filter_pre_id = fields.Many2one("ir.filters", string='Before Update Filter', ondelete='restrict', domain="[('model_id', '=', model_id.model)]",
filter_pre_id = fields.Many2one("ir.filters", string='Before Update Filter', ondelete='restrict',
domain="[('model_id', '=', model_name)]",
help="If present, this condition must be satisfied before the update of the record.")
filter_pre_domain = fields.Char(string='Before Update Domain',
help="If present, this condition must be satisfied before the update of the record.")
filter_id = fields.Many2one("ir.filters", string='Filter', ondelete='restrict', domain="[('model_id', '=', model_id.model)]",
filter_id = fields.Many2one("ir.filters", string='Filter', ondelete='restrict',
domain="[('model_id', '=', model_name)]",
help="If present, this condition must be satisfied before executing the action rule.")
filter_domain = fields.Char(string='Domain', help="If present, this condition must be satisfied before executing the action rule.")
last_run = fields.Datetime(readonly=True, copy=False)
on_change_fields = fields.Char(string="On Change Fields Trigger", help="Comma-separated list of field names that triggers the onchange.")
# which fields have an impact on the registry
CRITICAL_FIELDS = ['model_id', 'active', 'kind', 'on_change_fields']
CRITICAL_FIELDS = ['model_id', 'active', 'trigger', 'on_change_fields']
@api.onchange('model_id')
def onchange_model_id(self):
self.filter_pre_id = self.filter_id = False
self.model_name = self.model_id.model
@api.onchange('kind')
def onchange_kind(self):
if self.kind in ['on_create', 'on_create_or_write', 'on_unlink']:
@api.onchange('trigger')
def onchange_trigger(self):
if self.trigger in ['on_create', 'on_create_or_write', 'on_unlink']:
self.filter_pre_id = self.filter_pre_domain = self.trg_date_id = self.trg_date_range = self.trg_date_range_type = False
elif self.kind in ['on_write', 'on_create_or_write']:
elif self.trigger in ['on_write', 'on_create_or_write']:
self.trg_date_id = self.trg_date_range = self.trg_date_range_type = False
elif self.kind == 'on_time':
elif self.trigger == 'on_time':
self.filter_pre_id = self.filter_pre_domain = False
@api.onchange('filter_pre_id')
@@ -93,6 +94,7 @@ class BaseAutomation(models.Model):
@api.model
def create(self, vals):
vals['usage'] = 'base_automation'
base_automation = super(BaseAutomation, self).create(vals)
self._update_cron()
self._update_registry()
@@ -118,7 +120,7 @@ class BaseAutomation(models.Model):
based on time conditions.
"""
cron = self.env.ref('base_automation.ir_cron_data_base_automation_check', raise_if_not_found=False)
return cron and cron.toggle(model=self._name, domain=[('kind', '=', 'on_time')])
return cron and cron.toggle(model=self._name, domain=[('trigger', '=', 'on_time')])
def _update_registry(self):
""" Update the registry after a modification on action rules. """
@@ -129,13 +131,13 @@ class BaseAutomation(models.Model):
registry = Registry.new(self._cr.dbname)
registry.signal_registry_change()
def _get_actions(self, records, kinds):
""" Return the actions of the given kinds for records' model. The
def _get_actions(self, records, triggers):
""" Return the actions of the given triggers for records' model. The
returned actions' context contain an object to manage processing.
"""
if '__action_done' not in self._context:
self = self.with_context(__action_done={})
domain = [('model', '=', records._name), ('kind', 'in', kinds)]
domain = [('model_name', '=', records._name), ('trigger', 'in', triggers)]
actions = self.with_context(active_test=True).search(domain)
return actions.with_env(self.env)
@@ -189,27 +191,14 @@ class BaseAutomation(models.Model):
values = {}
if 'date_action_last' in records._fields:
values['date_action_last'] = fields.Datetime.now()
if self.act_user_id and 'user_id' in records._fields:
values['user_id'] = self.act_user_id.id
if values:
records.write(values)
# subscribe followers
if self.act_followers and hasattr(records, 'message_subscribe'):
followers = self.env['mail.followers'].sudo().search(
[('res_model', '=', records._name),
('res_id', 'in', records.ids),
('partner_id', 'in', self.act_followers.ids),
]
)
if not len(followers) == len(self.act_followers):
records.message_subscribe(self.act_followers.ids)
# execute server actions
if self.server_action_ids:
if self.action_server_id:
for record in records:
ctx = {'active_model': record._name, 'active_ids': record.ids, 'active_id': record.id}
self.server_action_ids.with_context(**ctx).run()
self.action_server_id.with_context(**ctx).run()
@api.model_cr
def _register_hook(self):
@@ -287,16 +276,16 @@ class BaseAutomation(models.Model):
def base_automation_onchange(self):
action_rule = self.env['base.automation'].browse(action_rule_id)
result = {}
for server_action in action_rule.server_action_ids.with_context(active_model=self._name, onchange_self=self):
res = server_action.run()
if res:
if 'value' in res:
res['value'].pop('id', None)
self.update({key: val for key, val in res['value'].iteritems() if key in self._fields})
if 'domain' in res:
result.setdefault('domain', {}).update(res['domain'])
if 'warning' in res:
result['warning'] = res['warning']
server_action = action_rule.action_server_id.with_context(active_model=self._name, onchange_self=self)
res = server_action.run()
if res:
if 'value' in res:
res['value'].pop('id', None)
self.update({key: val for key, val in res['value'].iteritems() if key in self._fields})
if 'domain' in res:
result.setdefault('domain', {}).update(res['domain'])
if 'warning' in res:
result['warning'] = res['warning']
return result
return base_automation_onchange
@@ -310,21 +299,21 @@ class BaseAutomation(models.Model):
# retrieve all actions, and patch their corresponding model
for action_rule in self.with_context({}).search([]):
Model = self.env[action_rule.model]
if action_rule.kind == 'on_create':
Model = self.env[action_rule.model_name]
if action_rule.trigger == 'on_create':
patch(Model, 'create', make_create())
elif action_rule.kind == 'on_create_or_write':
elif action_rule.trigger == 'on_create_or_write':
patch(Model, 'create', make_create())
patch(Model, '_write', make_write())
elif action_rule.kind == 'on_write':
elif action_rule.trigger == 'on_write':
patch(Model, '_write', make_write())
elif action_rule.kind == 'on_unlink':
elif action_rule.trigger == 'on_unlink':
patch(Model, 'unlink', make_unlink())
elif action_rule.kind == 'on_change':
elif action_rule.trigger == 'on_change':
# register an onchange method for the action_rule
method = make_onchange(action_rule.id)
for field_name in action_rule.on_change_fields.split(","):
@@ -350,7 +339,7 @@ class BaseAutomation(models.Model):
# retrieve all the action rules to run based on a timed condition
eval_context = self._get_eval_context()
for action in self.with_context(active_test=True).search([('kind', '=', 'on_time')]):
for action in self.with_context(active_test=True).search([('trigger', '=', 'on_time')]):
last_run = fields.Datetime.from_string(action.last_run) or datetime.datetime.utcfromtimestamp(0)
# retrieve all the records that satisfy the action's condition
@@ -367,7 +356,7 @@ class BaseAutomation(models.Model):
filter_meta = action.filter_id.get_metadata()[0]
user_id = (filter_meta['write_uid'] or filter_meta['create_uid'])[0]
context['lang'] = self.env['res.users'].browse(user_id).lang
records = self.env[action.model].with_context(context).search(domain)
records = self.env[action.model_name].with_context(context).search(domain)
# determine when action should occur for the records
if action.trg_date_id.name == 'date_action_last' and 'create_date' in records._fields:
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ServerAction(models.Model):
_inherit = "ir.actions.server"
usage = fields.Selection(selection_add=[('base_automation', 'Automated Action')])
@@ -2,4 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_base_automation,base.automation,model_base_automation,,1,0,0,0
access_base_automation_config,base.automation config,model_base_automation,base.group_system,1,1,1,1
access_base_automation_lead_test,access_base_automation_lead_test,model_base_automation_lead_test,base.group_system,1,1,1,1
access_base_automation_line_test,access_base_automation_line_test,model_base_automation_line_test,base.group_system,1,1,1,1
access_base_automation_line_test,access_base_automation_line_test,model_base_automation_line_test,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_base_automation base.automation model_base_automation 1 0 0 0
3 access_base_automation_config base.automation config model_base_automation base.group_system 1 1 1 1
4 access_base_automation_lead_test access_base_automation_lead_test model_base_automation_lead_test base.group_system 1 1 1 1
5 access_base_automation_line_test access_base_automation_line_test model_base_automation_line_test base.group_system 1 1 1 1
@@ -1,118 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Action Rule Form View -->
<!-- Automation Form View -->
<record id="view_base_automation_form" model="ir.ui.view">
<field name="name">base.automation.form</field>
<field name="name">Automations</field>
<field name="model">base.automation</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="base.view_server_action_form"/>
<field name="arch" type="xml">
<form string="Action Rule">
<sheet>
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
<group>
<group>
<field name="model_id"/>
<field name="model" invisible="1"/>
</group>
<group>
<field name="active"/>
<field name="sequence"/>
</group>
</group>
<notebook>
<page string="Conditions">
<group attrs="{'invisible': [('model', '!=', False)]}">
<p><b>Please choose the document type before setting the conditions.</b></p>
</group>
<group attrs="{'invisible': [('model', '=', False)]}">
<field name="kind"/>
<label for="filter_pre_id" attrs="{'invisible': [('kind', '!=', 'on_write')]}"/>
<div attrs="{'invisible': [('kind', '!=', 'on_write')]}">
<field name="filter_pre_domain" widget="domain" options="{'model': 'model'}"
class="o_row"/>
<div class="oe_edit_only o_row">Set selection based on a search filter:
<field name="filter_pre_id" domain="[('model_id', '=', model), '|', ('user_id', '=', uid), ('user_id', '=', False)]"
class="o_row" context="{'default_model_id': model, 'default_active': False, 'active_test': False}"/></div>
</div>
<label for="filter_id"/>
<div>
<field name="filter_domain" widget="domain" options="{'model': 'model'}"
class="o_row"/>
<div class="oe_edit_only o_row">Set selection based on a search filter:
<field name="filter_id" domain="[('model_id', '=', model), '|', ('user_id', '=', uid), ('user_id', '=', False)]"
class="o_row" context="{'default_model_id': model, 'default_active': False, 'active_test': False}"/>
</div>
</div>
<field name="on_change_fields"
attrs="{'invisible': [('kind', '!=', 'on_change')], 'required': [('kind', '=', 'on_change')]}"/>
<field name="trg_date_id"
attrs="{'invisible': [('kind', '!=', 'on_time')], 'required': [('kind', '=', 'on_time')]}"/>
<label for="trg_date_range" string="Delay After Trigger Date"
attrs="{'invisible': [('trg_date_id','=',False)]}"/>
<div attrs="{'invisible': [('trg_date_id','=',False)]}">
<field name="trg_date_range" class="o_row"/>
<field name="trg_date_range_type" class="o_row" attrs="{'required': [('trg_date_id','!=',False)]}"/>
</div>
<field name="trg_date_calendar_id" class="o_row"
attrs="{'invisible': ['|', ('trg_date_id','=',False), ('trg_date_range_type', '!=', 'day')]}"/>
</group>
<div attrs="{'invisible': [('model', '=', False)]}">
<p>Select when the action must be run, and choose records and/or timing conditions.</p>
<p groups="base.group_no_one">You may also use filters instead of choosing records. In order to create a new filter:</p>
<ul groups="base.group_no_one">
<li>Go to your "Related Document Model" page and set the filter parameters in the "Search" view (Example of filter based on Leads/Opportunities: Creation Date "is equal to" 01/01/2012)</li>
<li>In this same "Search" view, select the menu "Save Current Filter", enter the name (Ex: Create the 01/01/2012) and add the option "Share with all users"</li>
</ul>
</div>
</page>
<page string="Actions">
<group name="action_field" col="4" string="Fields to Change">
<field name="act_user_id"/>
<field name="act_followers" widget="many2many_tags"/>
</group>
<group name="action_server" string="Server actions to run">
<field name="server_action_ids" domain="[('model_id','=', model_id)]" nolabel="1" context="{'default_model_id': model_id}">
<tree string="Server Actions">
<field name="sequence"/>
<field name="name"/>
<field name="state"/>
</tree>
</field>
</group>
</page>
</notebook>
</sheet>
</form>
<xpath expr="//button[@name='create_action']" position="replace">
<button name="create_action_for_menu" 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."/>
</xpath>
<xpath expr="//button[@name='unlink_action']" position="replace">
<button name="unlink_action_for_menu" string="Remove Action" type="object"
attrs="{'invisible':[('menu_ir_values_id','=',False)]}"
help="Remove the contextual action related to this server action"/>
</xpath>
<xpath expr="//group[@name='action_wrapper']" position="inside">
<group>
<field name="trigger"/>
<label for="filter_pre_id" attrs="{'invisible': [('trigger', 'not in', ['on_write','on_create_or_write'])]}"/>
<div attrs="{'invisible': [('trigger', 'not in', ['on_write','on_create_or_write'])]}">
<field name="filter_pre_domain" widget="domain" options="{'model': 'model_name'}"
class="o_row"/>
<div class="oe_edit_only o_row">Set selection based on a search filter:
<field name="filter_pre_id" domain="[('model_id', '=', model_name), '|', ('user_id', '=', uid), ('user_id', '=', False)]"
class="o_row" context="{'default_model_id': model_name, 'default_active': False, 'active_test': False}"/></div>
</div>
<label for="filter_id"/>
<div>
<field name="filter_domain" widget="domain" options="{'model': 'model_name'}"
class="o_row"/>
<div class="oe_edit_only o_row">Set selection based on a search filter:
<field name="filter_id" domain="[('model_id', '=', model_name), '|', ('user_id', '=', uid), ('user_id', '=', False)]"
class="o_row" context="{'default_model_id': model_name, 'default_active': False, 'active_test': False}"/>
</div>
</div>
<field name="on_change_fields"
attrs="{'invisible': [('trigger', '!=', 'on_change')], 'required': [('trigger', '=', 'on_change')]}"/>
<field name="trg_date_id"
attrs="{'invisible': [('trigger', '!=', 'on_time')], 'required': [('trigger', '=', 'on_time')]}"/>
<label for="trg_date_range"
attrs="{'invisible': [('trigger', '!=', 'on_time')]}"/>
<div class="o_row" attrs="{'invisible': [('trigger', '!=', 'on_time')]}">
<field name="trg_date_range" attrs="{'required': [('trigger','=','on_time')]}"/>
<field name="trg_date_range_type" attrs="{'required': [('trigger','=','on_time')]}"/>
</div>
<field name="trg_date_calendar_id"
attrs="{'invisible': ['|', ('trg_date_id','=',False), ('trg_date_range_type', '!=', 'day')]}"/>
</group>
</xpath>
</field>
</record>
<!-- Action Rule Tree View -->
<!-- automation Tree View -->
<record id="view_base_automation_tree" model="ir.ui.view">
<field name="name">base.automation.tree</field>
<field name="model">base.automation</field>
<field name="arch" type="xml">
<tree string="Action Rule">
<field name="sequence"/>
<tree string="Automation">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="kind"/>
<field name="trigger"/>
<field name="model_id"/>
<field name="filter_id"/>
</tree>
</field>
</record>
<record id="view_base_automation_search" model="ir.ui.view">
<field name="name">base.automation.search</field>
<field name="model">base.automation</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="model_id"/>
</search>
</field>
</record>
<!-- Action Rule Action -->
<!-- automation Action -->
<record id="base_automation_act" model="ir.actions.act_window">
<field name="name">Automated Actions</field>
<field name="res_model">base.automation</field>
@@ -121,7 +81,7 @@
<field name="view_id" ref="view_base_automation_tree"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to setup a new automated action rule.
Click to setup a new automated automation.
</p><p>
Use automated actions to automatically trigger actions for
various screens. Example: a lead created by a specific user may
@@ -135,4 +95,4 @@
<menuitem id="menu_base_automation_form"
parent="base.menu_automation" action="base_automation_act" sequence="1"/>
</odoo>
</odoo>