Odoo18-Base/addons/test_base_automation/tests/test_flow.py
2025-01-06 10:57:38 +07:00

1247 lines
52 KiB
Python

# # -*- coding: utf-8 -*-
# # Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from unittest.mock import patch
import sys
from odoo.tools import mute_logger
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
from odoo.tests import common, tagged
from odoo.exceptions import AccessError, ValidationError
from odoo import Command
def create_automation(self, **kwargs):
"""
Create a transient automation with the given data and actions
The created automation is cleaned up at the end of the calling test
"""
vals = {'name': 'Automation'}
vals.update(kwargs)
actions_data = vals.pop('_actions', [])
if not isinstance(actions_data, list):
actions_data = [actions_data]
automation_id = self.env['base.automation'].create(vals)
action_ids = self.env['ir.actions.server'].create(
[
{
'name': 'Action',
'base_automation_id': automation_id.id,
'model_id': automation_id.model_id.id,
'usage': 'base_automation',
**action,
}
for action in actions_data
]
)
automation_id.write({'action_server_ids': [Command.set(action_ids.ids)]})
self.addCleanup(automation_id.unlink)
return automation_id
@tagged('post_install', '-at_install')
class BaseAutomationTest(TransactionCaseWithUserDemo):
def setUp(self):
super(BaseAutomationTest, self).setUp()
self.user_root = self.env.ref('base.user_root')
self.user_admin = self.env.ref('base.user_admin')
self.lead_model = self.env.ref('test_base_automation.model_base_automation_lead_test')
self.project_model = self.env.ref('test_base_automation.model_test_base_automation_project')
self.test_mail_template_automation = self.env['mail.template'].create(
{
'name': 'Template Automation',
'model_id': self.env['ir.model']._get_id("base.automation.lead.thread.test"),
'body_html': """<div>Email automation</div>""",
}
)
self.res_partner_1 = self.env['res.partner'].create({'name': 'My Partner'})
def create_lead(self, **kwargs):
vals = {
'name': "Lead Test",
'user_id': self.user_root.id,
}
vals.update(kwargs)
lead = self.env['base.automation.lead.test'].create(vals)
self.addCleanup(lead.unlink)
return lead
def create_line(self, **kwargs):
vals = {
'name': 'Line Test',
'user_id': self.user_root.id,
}
vals.update(kwargs)
line = self.env['base.automation.line.test'].create(vals)
self.addCleanup(line.unlink)
return line
def create_project(self, **kwargs):
vals = {'name': 'Project Test'}
vals.update(kwargs)
project = self.env['test_base_automation.project'].create(vals)
self.addCleanup(project.unlink)
return project
def create_stage(self, **kwargs):
vals = {'name': 'Stage Test'}
vals.update(kwargs)
stage = self.env['test_base_automation.stage'].create(vals)
self.addCleanup(stage.unlink)
return stage
def create_tag(self, **kwargs):
vals = {'name': 'Tag Test'}
vals.update(kwargs)
tag = self.env['test_base_automation.tag'].create(vals)
self.addCleanup(tag.unlink)
return tag
def test_000_on_create_or_write(self):
"""
Test case: on save, simple case
- trigger: on_create_or_write
"""
# --- Without the automation ---
lead = self.create_lead()
self.assertEqual(lead.state, 'draft')
self.assertEqual(lead.user_id, self.user_root)
# --- With the automation ---
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
_actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
)
# Write a lead should trigger the automation
lead.write({'state': 'open'})
self.assertEqual(lead.state, 'open')
self.assertEqual(lead.user_id, self.user_demo)
# Create a lead should trigger the automation
lead2 = self.create_lead()
self.assertEqual(lead2.state, 'draft')
self.assertEqual(lead2.user_id, self.user_demo)
def test_001_on_create_or_write(self):
"""
Test case: on save, with filter_domain
- trigger: on_create_or_write
- apply when: state is 'draft'
"""
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
filter_domain="[('state', '=', 'draft')]",
_actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
)
# Create a lead with state=open should not trigger the automation
lead = self.create_lead(state='open')
self.assertEqual(lead.state, 'open')
self.assertEqual(lead.user_id, self.user_root)
# Write a lead to state=draft should trigger the automation
lead.write({'state': 'draft'})
self.assertEqual(lead.state, 'draft')
self.assertEqual(lead.user_id, self.user_demo)
# Create a lead with state=draft should trigger the automation
lead_2 = self.create_lead()
self.assertEqual(lead_2.state, 'draft')
self.assertEqual(lead_2.user_id, self.user_demo)
def test_002_on_create_or_write(self):
"""
Test case: on save, with filter_pre_domain and filter_domain
- trigger: on_create_or_write
- before update filter: state is 'open'
- apply when: state is 'done'
"""
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
filter_pre_domain="[('state', '=', 'open')]",
filter_domain="[('state', '=', 'done')]",
_actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
)
# Create a lead with state=open should not trigger the automation
lead = self.create_lead(state='open')
self.assertEqual(lead.state, 'open')
self.assertEqual(lead.user_id, self.user_root)
# Write a lead to state=pending THEN to state=done should not trigger the automation
lead.write({'state': 'pending'})
self.assertEqual(lead.state, 'pending')
self.assertEqual(lead.user_id, self.user_root)
lead.write({'state': 'done'})
self.assertEqual(lead.state, 'done')
self.assertEqual(lead.user_id, self.user_root)
# Write a lead from state=open to state=done should trigger the automation
lead.write({'state': 'open'})
self.assertEqual(lead.state, 'open')
self.assertEqual(lead.user_id, self.user_root)
lead.write({'state': 'done'})
self.assertEqual(lead.state, 'done')
self.assertEqual(lead.user_id, self.user_demo)
# Create a lead with state=open then write it to state=done should trigger the automation
lead_2 = self.create_lead(state='open')
self.assertEqual(lead_2.state, 'open')
self.assertEqual(lead_2.user_id, self.user_root)
lead_2.write({'state': 'done'})
self.assertEqual(lead_2.state, 'done')
self.assertEqual(lead_2.user_id, self.user_demo)
# Create a lead with state=done should trigger the automation,
# as verifying the filter_pre_domain does not make sense on create
lead_3 = self.create_lead(state='done')
self.assertEqual(lead_3.state, 'done')
self.assertEqual(lead_3.user_id, self.user_demo)
def test_003_on_create_or_write(self):
""" Check that the on_create_or_write trigger works as expected with trigger fields. """
lead_state_field = self.env.ref('test_base_automation.field_base_automation_lead_test__state')
automation = create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
trigger_field_ids=[Command.link(lead_state_field.id)],
_actions={
'state': 'code',
'code': """
if env.context.get('old_values', None): # on write only
record = model.browse(env.context['active_id'])
record['name'] = record.name + 'X'""",
},
)
lead = self.create_lead(name="X")
lead.priority = True
partner1 = self.res_partner_1
lead.partner_id = partner1
self.assertEqual(lead.name, 'X', "No update until now.")
lead.state = 'open'
self.assertEqual(lead.name, 'XX', "One update should have happened.")
lead.state = 'done'
self.assertEqual(lead.name, 'XXX', "One update should have happened.")
lead.state = 'done'
self.assertEqual(lead.name, 'XXX', "No update should have happened.")
lead.state = 'cancel'
self.assertEqual(lead.name, 'XXXX', "One update should have happened.")
# change the rule to trigger on partner_id
lead_partner_id_field = self.env.ref('test_base_automation.field_base_automation_lead_test__partner_id')
automation.write({'trigger_field_ids': [Command.set([lead_partner_id_field.id])]})
partner2 = self.env['res.partner'].create({'name': 'A new partner'})
lead.name = 'X'
lead.state = 'open'
self.assertEqual(lead.name, 'X', "No update should have happened.")
lead.partner_id = partner2
self.assertEqual(lead.name, 'XX', "One update should have happened.")
lead.partner_id = partner2
self.assertEqual(lead.name, 'XX', "No update should have happened.")
lead.partner_id = partner1
self.assertEqual(lead.name, 'XXX', "One update should have happened.")
lead.partner_id = partner1
self.assertEqual(lead.name, 'XXX', "No update should have happened.")
def test_010_recompute(self):
"""
Test case: automation is applied whenever a field is recomputed
after a change on another model.
- trigger: on_create_or_write
- apply when: employee is True
"""
partner = self.res_partner_1
partner.write({'employee': False})
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
filter_domain="[('employee', '=', True)]",
_actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
)
lead = self.create_lead(partner_id=partner.id)
self.assertEqual(lead.partner_id, partner)
self.assertEqual(lead.employee, False)
self.assertEqual(lead.user_id, self.user_root)
# change partner, recompute on lead should trigger the rule
partner.write({'employee': True})
self.env.flush_all() # ensures the recomputation is done
self.assertEqual(lead.partner_id, partner)
self.assertEqual(lead.employee, True)
self.assertEqual(lead.user_id, self.user_demo)
def test_011_recompute(self):
"""
Test case: automation is applied whenever a field is recomputed.
The context contains the target field.
- trigger: on_create_or_write
"""
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
_actions={
'state': 'code',
'code': """
if env.context.get('old_values', None): # on write
if 'user_id' in env.context['old_values'][record.id]:
record.write({'is_assigned_to_admin': (record.user_id.id == 1)})""",
},
)
partner = self.res_partner_1
lead = self.create_lead(state='draft', partner_id=partner.id)
self.assertEqual(lead.deadline, False)
self.assertEqual(lead.is_assigned_to_admin, False)
# change priority and user; this triggers deadline recomputation, and
# the server action should set is_assigned_to_admin field to True
lead.write({'priority': True, 'user_id': self.user_root.id})
self.assertNotEqual(lead.deadline, False)
self.assertEqual(lead.is_assigned_to_admin, True)
def test_012_recompute(self):
"""
Test case: automation is applied whenever a field is recomputed.
- trigger: on_create_or_write
- if updating fields: [deadline]
"""
active_field = self.env.ref("test_base_automation.field_base_automation_lead_test__active")
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
trigger_field_ids=[Command.link(active_field.id)],
_actions={
'state': 'code',
'code': """
if not env.context.get('old_values', None): # on create
record.write({'state': 'open'})
else:
record.write({'priority': not record.priority})""",
},
)
lead = self.create_lead(state='draft', priority=False)
self.assertEqual(lead.state, 'open') # the rule has set the state to open on create
self.assertEqual(lead.priority, False)
# change state; the rule should not be triggered
lead.write({'state': 'pending'})
self.assertEqual(lead.state, 'pending')
self.assertEqual(lead.priority, False)
# change active; the rule should be triggered
lead.write({'active': False})
self.assertEqual(lead.state, 'pending')
self.assertEqual(lead.priority, True)
# change active again; the rule should still be triggered
lead.write({'active': True})
self.assertEqual(lead.state, 'pending')
self.assertEqual(lead.priority, False)
def test_013_recompute(self):
"""
Test case: automation is applied whenever a field is recomputed
- trigger: on_create_or_write
- if updating fields: [deadline]
- before update filter: deadline is not set
- apply when: deadline is set
"""
deadline_field = self.env.ref("test_base_automation.field_base_automation_lead_test__deadline")
create_automation(
self,
model_id=self.env['ir.model']._get_id('base.automation.lead.thread.test'),
trigger='on_create_or_write',
trigger_field_ids=[Command.link(deadline_field.id)],
filter_pre_domain="[('deadline', '=', False)]",
filter_domain="[('deadline', '!=', False)]",
_actions={
'state': 'mail_post',
'mail_post_method': 'email',
'template_id': self.test_mail_template_automation.id,
},
)
send_mail_count = 0
def _patched_send_mail(*args, **kwargs):
nonlocal send_mail_count
send_mail_count += 1
patcher = patch('odoo.addons.mail.models.mail_template.MailTemplate.send_mail', _patched_send_mail)
self.startPatcher(patcher)
lead = self.env['base.automation.lead.thread.test'].create({
'name': "Lead Test",
'user_id': self.user_root.id,
})
self.addCleanup(lead.unlink)
self.assertEqual(lead.priority, False)
self.assertEqual(lead.deadline, False)
self.assertEqual(send_mail_count, 0)
lead.write({'priority': True})
self.assertEqual(lead.priority, True)
self.assertNotEqual(lead.deadline, False)
self.assertEqual(send_mail_count, 1)
def test_020_recursive(self):
""" Check that a rule is executed recursively by a secondary change. """
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
_actions={
'state': 'code',
'code': """
if env.context.get('old_values', None): # on write
if 'partner_id' in env.context['old_values'][record.id]:
record.write({'state': 'draft'})""",
},
)
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
filter_domain="[('state', '=', 'draft')]",
_actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
)
lead = self.create_lead(state='open')
self.assertEqual(lead.state, 'open')
self.assertEqual(lead.user_id, self.user_root)
# change partner; this should trigger the rule that modifies the state
# and then the rule that modifies the user
partner = self.res_partner_1
lead.write({'partner_id': partner.id})
self.assertEqual(lead.state, 'draft')
self.assertEqual(lead.user_id, self.user_demo)
def test_021_recursive(self):
""" Check what it does with a recursive infinite loop """
automations = [
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
filter_domain="[('state', '=', 'draft')]",
_actions={'state': 'code', 'code': "record.write({'state': 'pending'})"},
),
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
filter_domain="[('state', '=', 'pending')]",
_actions={'state': 'code', 'code': "record.write({'state': 'open'})"},
),
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
filter_domain="[('state', '=', 'open')]",
_actions={'state': 'code', 'code': "record.write({'state': 'done'})"},
),
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
filter_domain="[('state', '=', 'done')]",
_actions={'state': 'code', 'code': "record.write({'state': 'draft'})"},
),
]
def _patch(*args, **kwargs):
self.assertEqual(args[0], automations.pop(0))
patcher = patch('odoo.addons.base_automation.models.base_automation.BaseAutomation._process', _patch)
self.startPatcher(patcher)
lead = self.create_lead(state='draft')
self.assertEqual(lead.state, 'draft')
self.assertEqual(len(automations), 0) # all automations have been processed # CHECK if proper assertion ?
def test_030_submodel(self):
""" Check that a rule on a submodel is executed when the parent is modified. """
# --- Without the automations ---
line = self.create_line()
self.assertEqual(line.user_id, self.user_root)
lead = self.create_lead(line_ids=[(0, 0, {'name': 'Line', 'user_id': self.user_root.id})])
self.assertEqual(lead.user_id, self.user_root)
self.assertEqual(lead.line_ids.user_id, self.user_root)
# --- With the automations ---
comodel = self.env.ref('test_base_automation.model_base_automation_line_test')
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
_actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
)
create_automation(
self,
model_id=comodel.id,
trigger='on_create_or_write',
_actions={'state': 'code', 'code': "record.write({'user_id': %s})" % (self.user_demo.id)},
)
line = self.create_line(user_id=self.user_root.id)
self.assertEqual(line.user_id, self.user_demo) # rule on secondary model
lead = self.create_lead(line_ids=[(0, 0, {'name': 'Line', 'user_id': self.user_root.id})])
self.assertEqual(lead.user_id, self.user_demo) # rule on primary model
self.assertEqual(lead.line_ids.user_id, self.user_demo) # rule on secondary model
def test_040_modelwithoutaccess(self):
"""
Ensure a domain on a M2O without user access doesn't fail.
We create a base automation with a filter on a model the user haven't access to
- create a group
- restrict acl to this group and set only admin in it
- create base.automation with a filter
- create a record in the restricted model in admin
- create a record in the non restricted model in demo
"""
Model = self.env['base.automation.link.test']
model_id = self.env.ref('test_base_automation.model_base_automation_link_test')
Comodel = self.env['base.automation.linked.test']
comodel_access = self.env.ref('test_base_automation.access_base_automation_linked_test')
comodel_access.group_id = self.env['res.groups'].create({
'name': "Access to base.automation.linked.test",
"users": [Command.link(self.user_admin.id)],
})
# sanity check: user demo has no access to the comodel of 'linked_id'
with self.assertRaises(AccessError):
Comodel.with_user(self.user_demo).check_access('read')
# check base automation with filter that performs Comodel.search()
create_automation(
self,
model_id=model_id.id,
trigger='on_create_or_write',
filter_pre_domain="[('linked_id.another_field', '=', 'something')]",
_actions={'state': 'code', 'code': 'action = [rec.name for rec in records]'},
)
Comodel.create([
{'name': 'a first record', 'another_field': 'something'},
{'name': 'another record', 'another_field': 'something different'},
])
rec1 = Model.create({'name': 'a record'})
rec1.write({'name': 'a first record'})
rec2 = Model.with_user(self.user_demo).create({'name': 'another record'})
rec2.write({'name': 'another value'})
# check base automation with filter that performs Comodel.name_search()
create_automation(
self,
model_id=model_id.id,
trigger='on_create_or_write',
filter_pre_domain="[('linked_id', '=', 'whatever')]",
_actions={'state': 'code', 'code': 'action = [rec.name for rec in records]'},
)
rec3 = Model.create({'name': 'a random record'})
rec3.write({'name': 'a first record'})
rec4 = Model.with_user(self.user_demo).create({'name': 'again another record'})
rec4.write({'name': 'another value'})
def test_050_on_create_or_write_with_create_record(self):
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_create_or_write',
_actions={
'state': 'object_create',
'crud_model_id': self.project_model.id,
'value': 'foo',
},
)
lead = self.create_lead()
search_result = self.env['test_base_automation.project'].name_search('foo')
self.assertEqual(len(search_result), 1, 'One record on the project model should have been created')
lead.write({'name': 'renamed lead'})
search_result = self.env['test_base_automation.project'].name_search('foo')
self.assertEqual(len(search_result), 2, 'Another record on the project model should have been created')
# ----------------------------
# The following does not work properly as it is a known
# limitation of the implementation since at least 14.0
# -> AssertionError: 4 != 3 : Another record on the secondary model should have been created
# # write on a field that is a dependency of another computed field
# lead.write({'priority': True})
# search_result = self.env['test_base_automation.project'].name_search('foo')
# self.assertEqual(len(search_result), 3, 'Another record on the secondary model should have been created')
# ----------------------------
def test_060_on_stage_set(self):
stage_field = self.env['ir.model.fields'].search([
('model_id', '=', self.project_model.id),
('name', '=', 'stage_id'),
])
stage1 = self.create_stage()
stage2 = self.create_stage()
create_automation(
self,
model_id=self.project_model.id,
trigger='on_stage_set',
trigger_field_ids=[stage_field.id],
filter_domain="[('stage_id', '=', %s)]" % stage1.id,
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
project = self.create_project()
self.assertEqual(project.name, 'Project Test')
project.write({'stage_id': stage1.id})
self.assertEqual(project.name, 'Project Test!')
project.write({'stage_id': stage1.id})
self.assertEqual(project.name, 'Project Test!')
project.write({'stage_id': stage2.id})
self.assertEqual(project.name, 'Project Test!')
project.write({'stage_id': False})
self.assertEqual(project.name, 'Project Test!')
project.write({'stage_id': stage1.id})
self.assertEqual(project.name, 'Project Test!!')
def test_070_on_user_set(self):
user_field = self.env['ir.model.fields'].search([
('model_id', '=', self.lead_model.id),
('name', '=', 'user_id'),
])
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_user_set',
trigger_field_ids=[user_field.id],
filter_domain="[('user_id', '!=', False)]",
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
lead = self.create_lead()
self.assertEqual(lead.name, 'Lead Test!')
lead.write({'user_id': self.user_demo.id})
self.assertEqual(lead.name, 'Lead Test!!')
lead.write({'user_id': self.user_demo.id})
self.assertEqual(lead.name, 'Lead Test!!')
lead.write({'user_id': self.user_admin.id})
self.assertEqual(lead.name, 'Lead Test!!!')
lead.write({'user_id': False})
self.assertEqual(lead.name, 'Lead Test!!!')
lead.write({'user_id': self.user_demo.id})
self.assertEqual(lead.name, 'Lead Test!!!!')
def test_071_on_user_set(self):
# same test as above but with the user_ids many2many on a project
user_field = self.env['ir.model.fields'].search([
('model_id', '=', self.project_model.id),
('name', '=', 'user_ids'),
])
create_automation(
self,
model_id=self.project_model.id,
trigger='on_user_set',
trigger_field_ids=[user_field.id],
filter_domain="[('user_ids', '!=', False)]",
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
project = self.create_project()
self.assertEqual(project.name, 'Project Test')
project.write({'user_ids': [Command.set([self.user_demo.id])]})
self.assertEqual(project.name, 'Project Test!')
project.write({'user_ids': [Command.set([self.user_demo.id])]})
self.assertEqual(project.name, 'Project Test!')
project.write({'user_ids': [Command.link(self.user_admin.id)]})
self.assertEqual(project.name, 'Project Test!!')
# Unlinking a user while there are still other users does trigger the automation
# This behavior could be changed in the future but needs a bit of investigation
project.write({'user_ids': [Command.unlink(self.user_admin.id)]})
self.assertEqual(project.name, 'Project Test!!!')
project.write({'user_ids': [Command.set([])]})
self.assertEqual(project.name, 'Project Test!!!')
project.write({'user_ids': [Command.set([self.user_demo.id])]})
self.assertEqual(project.name, 'Project Test!!!!')
def test_080_on_tag_set(self):
tag_field = self.env['ir.model.fields'].search([
('model_id', '=', self.project_model.id),
('name', '=', 'tag_ids'),
])
tag1 = self.create_tag()
create_automation(
self,
model_id=self.project_model.id,
trigger='on_tag_set',
trigger_field_ids=[tag_field.id],
filter_pre_domain="[('tag_ids', 'not in', [%s])]" % tag1.id,
filter_domain="[('tag_ids', 'in', [%s])]" % tag1.id,
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
project = self.create_project()
self.assertEqual(project.name, 'Project Test')
project.write({'tag_ids': [Command.set([tag1.id])]})
self.assertEqual(project.name, 'Project Test!')
project.write({'tag_ids': [Command.set([tag1.id])]})
self.assertEqual(project.name, 'Project Test!')
tag2 = self.create_tag()
project.write({'tag_ids': [Command.link(tag2.id)]})
self.assertEqual(project.name, 'Project Test!')
project.write({'tag_ids': [Command.clear()]})
self.assertEqual(project.name, 'Project Test!')
project.write({'tag_ids': [Command.set([tag2.id])]})
self.assertEqual(project.name, 'Project Test!')
project.write({'tag_ids': [Command.link(tag1.id)]})
self.assertEqual(project.name, 'Project Test!!')
def test_090_on_state_set(self):
state_field = self.env['ir.model.fields'].search([
('model_id', '=', self.lead_model.id),
('name', '=', 'state'),
])
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_state_set',
trigger_field_ids=[state_field.id],
filter_domain="[('state', '=', 'done')]",
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
lead = self.create_lead()
self.assertEqual(lead.name, 'Lead Test')
lead.write({'state': 'open'})
self.assertEqual(lead.name, 'Lead Test')
lead.write({'state': 'done'})
self.assertEqual(lead.name, 'Lead Test!')
lead.write({'state': 'done'})
self.assertEqual(lead.name, 'Lead Test!')
lead.write({'state': 'open'})
self.assertEqual(lead.name, 'Lead Test!')
lead.write({'state': 'done'})
self.assertEqual(lead.name, 'Lead Test!!')
def test_100_on_priority_set(self):
priority_field = self.env['ir.model.fields'].search([
('model_id', '=', self.project_model.id),
('name', '=', 'priority'),
])
create_automation(
self,
model_id=self.project_model.id,
trigger='on_priority_set',
trigger_field_ids=[priority_field.id],
filter_domain="[('priority', '=', '2')]",
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
project = self.create_project()
self.assertEqual(project.name, 'Project Test')
self.assertEqual(project.priority, '1')
project.write({'priority': '0'})
self.assertEqual(project.name, 'Project Test')
project.write({'priority': '2'})
self.assertEqual(project.name, 'Project Test!')
project.write({'priority': '2'})
self.assertEqual(project.name, 'Project Test!')
project.write({'priority': '0'})
self.assertEqual(project.name, 'Project Test!')
project.write({'priority': '2'})
self.assertEqual(project.name, 'Project Test!!')
def test_110_on_archive(self):
active_field = self.env['ir.model.fields'].search([
('model_id', '=', self.lead_model.id),
('name', '=', 'active'),
])
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_archive',
trigger_field_ids=[active_field.id],
filter_domain="[('active', '=', False)]",
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
lead = self.create_lead()
self.assertEqual(lead.name, 'Lead Test')
lead.write({'active': False})
self.assertEqual(lead.name, 'Lead Test!')
lead.write({'active': True})
self.assertEqual(lead.name, 'Lead Test!')
lead.write({'active': False})
self.assertEqual(lead.name, 'Lead Test!!')
lead.write({'active': False})
self.assertEqual(lead.name, 'Lead Test!!')
def test_110_on_unarchive(self):
active_field = self.env['ir.model.fields'].search([
('model_id', '=', self.lead_model.id),
('name', '=', 'active'),
])
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_unarchive',
trigger_field_ids=[active_field.id],
filter_domain="[('active', '=', True)]",
_actions={'state': 'object_write', 'evaluation_type': 'equation', 'update_path': 'name', 'value': "record.name + '!'"},
)
lead = self.create_lead()
self.assertEqual(lead.name, 'Lead Test')
lead.write({'active': False})
self.assertEqual(lead.name, 'Lead Test')
lead.write({'active': True})
self.assertEqual(lead.name, 'Lead Test!')
lead.write({'active': False})
self.assertEqual(lead.name, 'Lead Test!')
lead.write({'active': True})
self.assertEqual(lead.name, 'Lead Test!!')
lead.write({'active': True})
self.assertEqual(lead.name, 'Lead Test!!')
def test_120_on_change(self):
Model = self.env.get(self.lead_model.model)
lead_name_field = self.env['ir.model.fields'].search([
('model_id', '=', self.lead_model.id),
('name', '=', 'name'),
])
self.assertEqual(lead_name_field.name in Model._onchange_methods, False)
create_automation(
self,
model_id=self.lead_model.id,
trigger='on_change',
on_change_field_ids=[lead_name_field.id],
_actions={'state': 'code', 'code': ""},
)
self.assertEqual(lead_name_field.name in Model._onchange_methods, True)
def test_130_on_unlink(self):
automation = create_automation(
self,
model_id=self.lead_model.id,
trigger='on_unlink',
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
called_count = 0
def _patch(*args, **kwargs):
nonlocal called_count
called_count += 1
self.assertEqual(args[0], automation)
patcher = patch('odoo.addons.base_automation.models.base_automation.BaseAutomation._process', _patch)
self.startPatcher(patcher)
lead = self.create_lead()
self.assertEqual(called_count, 0)
lead.unlink()
self.assertEqual(called_count, 1)
def test_004_check_method(self):
model = self.env["ir.model"]._get("base.automation.lead.test")
TIME_TRIGGERS = [
'on_time',
'on_time_created',
'on_time_updated',
]
self.env["base.automation"].search([('trigger', 'in', TIME_TRIGGERS)]).active = False
automation = self.env["base.automation"].create({
"name": "Cron BaseAuto",
"trigger": "on_time",
"model_id": model.id,
})
self.assertFalse(automation.last_run)
self.env["base.automation"]._check(False)
self.assertTrue(automation.last_run)
def test_005_check_model_with_different_rec_name_char(self):
model = self.env["ir.model"]._get("base.automation.model.with.recname.char")
create_automation(
self,
model_id=self.project_model.id,
trigger='on_create_or_write',
_actions={
'state': 'object_create',
'crud_model_id': model.id,
'value': "Test _rec_name Automation",
},
)
self.create_project()
record_count = self.env[model.model].search_count([('description', '=', 'Test _rec_name Automation')])
self.assertEqual(record_count, 1, "Only one record should have been created")
def test_006_check_model_with_different_m2o_name_create(self):
model = self.env["ir.model"]._get("base.automation.model.with.recname.m2o")
create_automation(
self,
model_id=self.project_model.id,
trigger='on_create_or_write',
_actions={
'state': 'object_create',
'crud_model_id': model.id,
'value': "Test _rec_name Automation",
},
)
self.create_project()
record_count = self.env[model.model].search_count([('user_id', '=', 'Test _rec_name Automation')])
self.assertEqual(record_count, 1, "Only one record should have been created")
def test_140_copy_should_copy_actions(self):
""" Copying an automation should copy its actions. """
automation = create_automation(
self,
model_id=self.lead_model.id,
trigger='on_change',
_actions={'state': 'code', 'code': "record.write({'name': record.name + '!'})"},
)
action_ids = automation.action_server_ids
copy_automation = automation.copy()
copy_action_ids = copy_automation.action_server_ids
# Same number of actions but id should be different
self.assertEqual(len(action_ids), 1)
self.assertEqual(len(copy_action_ids), len(action_ids))
self.assertNotEqual(copy_action_ids, action_ids)
@common.tagged('post_install', '-at_install')
class TestCompute(common.TransactionCase):
def test_inversion(self):
""" If a stored field B depends on A, an update to the trigger for A
should trigger the recomputaton of A, then B.
However if a search() is performed during the computation of A
??? and _order is affected ??? a flush will be triggered, forcing the
computation of B, based on the previous A.
This happens if a rule has a non-empty filter_pre_domain, even if
it's an empty list (``'[]'`` as opposed to ``False``).
"""
company1 = self.env['res.partner'].create({
'name': "Gorofy",
'is_company': True,
})
company2 = self.env['res.partner'].create({
'name': "Awiclo",
'is_company': True
})
r = self.env['res.partner'].create({
'name': 'Bob',
'is_company': False,
'parent_id': company1.id
})
self.assertEqual(r.display_name, 'Gorofy, Bob')
r.parent_id = company2
self.assertEqual(r.display_name, 'Awiclo, Bob')
create_automation(
self,
model_id=self.env.ref('base.model_res_partner').id,
filter_pre_domain=False,
trigger='on_create_or_write',
_actions={'state': 'code'}, # no-op action
)
r.parent_id = company1
self.assertEqual(r.display_name, 'Gorofy, Bob')
create_automation(
self,
model_id=self.env.ref('base.model_res_partner').id,
filter_pre_domain='[]',
trigger='on_create_or_write',
_actions={'state': 'code'}, # no-op action
)
r.parent_id = company2
self.assertEqual(r.display_name, 'Awiclo, Bob')
def test_recursion(self):
project = self.env['test_base_automation.project'].create({})
# this action is executed every time a task is assigned to project
create_automation(
self,
model_id=self.env.ref('test_base_automation.model_test_base_automation_task').id,
trigger='on_create_or_write',
filter_domain=repr([('project_id', '=', project.id)]),
_actions={'state': 'code'}, # no-op action
)
# create one task in project with 10 subtasks; all the subtasks are
# automatically assigned to project, too
task = self.env['test_base_automation.task'].create({'project_id': project.id})
subtasks = task.create([{'parent_id': task.id} for _ in range(10)])
subtasks.flush_model()
# This test checks what happens when a stored recursive computed field
# is marked to compute on many records, and automation rules are
# triggered depending on that field. In this case, we trigger the
# recomputation of 'project_id' on 'subtasks' by deleting their parent
# task.
#
# An issue occurs when the domain of automation rules is evaluated by
# method search(), because the latter flushes the fields to search on,
# which are also the ones being recomputed. Combined with the fact
# that recursive fields are not computed in batch, this leads to a huge
# amount of recursive calls between the automation rule and flush().
#
# The execution of task.unlink() looks like this:
# - mark 'project_id' to compute on subtasks
# - delete task
# - flush()
# - recompute 'project_id' on subtask1
# - call compute on subtask1
# - in action, search([('id', 'in', subtask1.ids), ('project_id', '=', pid)])
# - flush(['id', 'project_id'])
# - recompute 'project_id' on subtask2
# - call compute on subtask2
# - in action search([('id', 'in', subtask2.ids), ('project_id', '=', pid)])
# - flush(['id', 'project_id'])
# - recompute 'project_id' on subtask3
# - call compute on subtask3
# - in action, search([('id', 'in', subtask3.ids), ('project_id', '=', pid)])
# - flush(['id', 'project_id'])
# - recompute 'project_id' on subtask4
# ...
limit = sys.getrecursionlimit()
try:
sys.setrecursionlimit(100)
task.unlink()
finally:
sys.setrecursionlimit(limit)
def test_mail_triggers(self):
lead_model = self.env["ir.model"]._get("base.automation.lead.test")
with self.assertRaises(ValidationError):
create_automation(self, trigger="on_message_sent", model_id=lead_model.id)
lead_thread_model = self.env["ir.model"]._get("base.automation.lead.thread.test")
automation = create_automation(self, trigger="on_message_sent", model_id=lead_thread_model.id, _actions={
"state": "object_write",
"update_path": "active",
"update_boolean_value": "false"
})
ext_partner = self.env["res.partner"].create({"name": "ext", "email": "email@server.com"})
internal_partner = self.env["res.users"].browse(2).partner_id
obj = self.env["base.automation.lead.thread.test"].create({"name": "test"})
obj.message_subscribe([ext_partner.id, internal_partner.id])
obj.message_post(author_id=internal_partner.id, message_type="comment", subtype_xmlid="mail.mt_comment")
self.assertFalse(obj.active)
obj.active = True
obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment")
self.assertTrue(obj.active)
obj.message_post(author_id=ext_partner.id, message_type="comment")
self.assertTrue(obj.active)
obj.message_post(author_id=internal_partner.id, message_type="comment")
self.assertTrue(obj.active)
obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
self.assertFalse(obj.active)
obj.active = True
# message doesn't have author_id, so it should be considered as external the automation should't be triggered
obj.message_post(author_id=False, email_from="test_abla@test.test", message_type="email", subtype_xmlid="mail.mt_comment")
self.assertTrue(obj.active)
automation.trigger = "on_message_received"
obj.active = True
obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
self.assertTrue(obj.active)
obj.message_post(author_id=ext_partner.id, message_type="comment")
self.assertTrue(obj.active)
obj.message_post(author_id=ext_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
self.assertFalse(obj.active)
obj.active = True
obj.message_post(author_id=ext_partner.id, subtype_xmlid="mail.mt_comment")
self.assertTrue(obj.active)
obj.message_post(author_id=False, email_from="test_abla@test.test", message_type="email", subtype_xmlid="mail.mt_comment")
self.assertFalse(obj.active)
def test_multiple_mail_triggers(self):
lead_model = self.env["ir.model"]._get("base.automation.lead.test")
with self.assertRaises(ValidationError):
create_automation(self, trigger="on_message_sent", model_id=lead_model.id)
lead_thread_model = self.env["ir.model"]._get("base.automation.lead.thread.test")
create_automation(self, trigger="on_message_sent", model_id=lead_thread_model.id, _actions={
"state": "object_write",
"update_path": "active",
"update_boolean_value": "false"
})
create_automation(self, trigger="on_message_sent", model_id=lead_thread_model.id, _actions={
"state": "object_write",
"evaluation_type": "equation",
"update_path": "name",
"value": "record.name + '!'"
})
ext_partner = self.env["res.partner"].create({"name": "ext", "email": "email@server.com"})
internal_partner = self.env["res.users"].browse(2).partner_id
obj = self.env["base.automation.lead.thread.test"].create({"name": "test"})
obj.message_subscribe([ext_partner.id, internal_partner.id])
obj.message_post(author_id=internal_partner.id, message_type="comment", subtype_xmlid="mail.mt_comment")
self.assertFalse(obj.active)
self.assertEqual(obj.name, "test!")
def test_compute_on_create(self):
lead_model = self.env['ir.model']._get('base.automation.lead.test')
stage_field = self.env['ir.model.fields']._get('base.automation.lead.test', 'stage_id')
new_stage = self.env['test_base_automation.stage'].create({'name': 'New'})
create_automation(
self,
model_id=lead_model.id,
trigger='on_stage_set',
trigger_field_ids=[stage_field.id],
_actions={
'state': 'object_create',
'crud_model_id': self.env['ir.model']._get('res.partner').id,
'value': "Test Partner Automation",
},
filter_domain=repr([('stage_id', '=', new_stage.id)]),
)
# Tricky case: the record is created with 'stage_id' being false, and
# the field is marked for recomputation. The field is then recomputed
# while evaluating 'filter_domain', which causes the execution of the
# automation. And as the domain is satisfied, the automation is
# processed again, but it must detect that it has just been run!
self.env['base.automation.lead.test'].create({
'name': 'Test Lead',
})
# check that the automation has been run once
partner_count = self.env['res.partner'].search_count([('name', '=', 'Test Partner Automation')])
self.assertEqual(partner_count, 1, "Only one partner should have been created")
@common.tagged("post_install", "-at_install")
class TestHttp(common.HttpCase):
def test_webhook_trigger(self):
model = self.env["ir.model"]._get("base.automation.linked.test")
record_getter = "model.search([('name', '=', payload['name'])]) if payload.get('name') else None"
automation = create_automation(self, trigger="on_webhook", model_id=model.id, record_getter=record_getter, _actions={
"state": "object_write",
"update_path": "another_field",
"value": "written"
})
obj = self.env[model.model].create({"name": "some name"})
response = self.url_open(automation.url, data=json.dumps({"name": "some name"}))
self.assertEqual(response.json(), {"status": "ok"})
self.assertEqual(response.status_code, 200)
self.assertEqual(obj.another_field, "written")
obj.another_field = False
with mute_logger("odoo.addons.base_automation.models.base_automation"):
response = self.url_open(automation.url, data=json.dumps({}))
self.assertEqual(response.json(), {"status": "error"})
self.assertEqual(response.status_code, 500)
self.assertEqual(obj.another_field, False)
response = self.url_open("/web/hook/0123456789", data=json.dumps({"name": "some name"}))
self.assertEqual(response.json(), {"status": "error"})
self.assertEqual(response.status_code, 404)
def test_payload_in_action_server(self):
model = self.env["ir.model"]._get("base.automation.linked.test")
record_getter = "model.search([('name', '=', payload['name'])]) if payload.get('name') else None"
automation = create_automation(self, trigger="on_webhook", model_id=model.id, record_getter=record_getter, _actions={
"state": "code",
"code": "record.write({'another_field': json.dumps(payload)})"
})
obj = self.env[model.model].create({"name": "some name"})
self.url_open(automation.url, data=json.dumps({"name": "some name", "test_key": "test_value"}), headers={"Content-Type": "application/json"})
self.assertEqual(json.loads(obj.another_field), {
"name": "some name",
"test_key": "test_value",
})
obj.another_field = ""
self.url_open(automation.url + "?test_param=test_value&name=some%20name")
self.assertEqual(json.loads(obj.another_field), {
"name": "some name",
"test_param": "test_value",
})
def test_webhook_send_and_receive(self):
model = self.env["ir.model"]._get("base.automation.linked.test")
obj = self.env[model.model].create({"name": "some name"})
automation_receiver = create_automation(self, trigger="on_webhook", model_id=model.id, _actions={
"state": "code",
"code": "record.write({'another_field': json.dumps(payload)})"
})
name_field_id = self.env.ref("test_base_automation.field_base_automation_linked_test__name")
automation_sender = create_automation(self, trigger="on_write", model_id=model.id, trigger_field_ids=[(6, 0, [name_field_id.id])], _actions={
"state": "webhook",
"webhook_url": automation_receiver.url,
})
obj.name = "new_name"
self.cr.flush()
self.cr.clear()
self.assertEqual(json.loads(obj.another_field), {
'_action': f'Send Webhook Notification(#{automation_sender.action_server_ids[0].id})',
"_id": obj.id,
"_model": obj._name,
})
def test_on_change_get_views_cache(self):
model_name = "base.automation.lead.test"
my_view = self.env["ir.ui.view"].create({
"name": "My View",
"model": model_name,
"type": "form",
"arch": "<form><field name='active'/></form>",
})
self.assertEqual(
self.env[model_name].get_view(my_view.id)["arch"],
'<form><field name="active"/></form>'
)
model = self.env["ir.model"]._get(model_name)
active_field = self.env["ir.model.fields"]._get(model_name, "active")
create_automation(self, trigger="on_change", model_id=model.id, on_change_field_ids=[Command.set([active_field.id])], _actions={
"state": "code",
"code": "",
})
self.assertEqual(
self.env[model_name].get_view(my_view.id)["arch"],
'<form><field name="active" on_change="1"/></form>'
)