19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:39 +01:00
parent 38c6088dcc
commit d9452d2060
243 changed files with 30797 additions and 10815 deletions

View file

@ -12,37 +12,14 @@ pip install odoo-bringout-oca-ocb-test_base_automation
## Dependencies
This addon depends on:
- base_automation
## Manifest Information
- **Name**: Test - Base Automation
- **Version**: 1.0
- **Category**: Hidden
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_base_automation`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/test_base_automation
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md
This package preserves the original LGPL-3 license.

View file

@ -1,12 +1,14 @@
[project]
name = "odoo-bringout-oca-ocb-test_base_automation"
version = "16.0.0"
description = "Test - Base Automation - Base Automation Tests: Ensure Flow Robustness"
description = "Test - Base Automation -
Base Automation Tests: Ensure Flow Robustness
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-base_automation>=16.0.0",
"odoo-bringout-oca-ocb-base_automation>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -16,7 +18,7 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]

View file

@ -14,6 +14,12 @@ tests independently to functional aspects of other models.""",
'data': [
'security/ir.model.access.csv',
],
'assets': {
'web.assets_tests': [
'test_base_automation/static/tests/**/*',
],
},
'installable': True,
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -5,8 +5,8 @@ from dateutil import relativedelta
from odoo import fields, models, api
class LeadTest(models.Model):
_name = "base.automation.lead.test"
class BaseAutomationLeadTest(models.Model):
_name = 'base.automation.lead.test'
_description = "Automated Rule Test"
name = fields.Char(string='Subject', required=True)
@ -15,8 +15,9 @@ class LeadTest(models.Model):
('pending', 'Pending'), ('done', 'Closed')],
string="Status", readonly=True, default='draft')
active = fields.Boolean(default=True)
tag_ids = fields.Many2many('test_base_automation.tag')
partner_id = fields.Many2one('res.partner', string='Partner')
date_action_last = fields.Datetime(string='Last Action', readonly=True)
date_automation_last = fields.Datetime(string='Last Automation', readonly=True)
employee = fields.Boolean(compute='_compute_employee_deadline', store=True)
line_ids = fields.One2many('base.automation.line.test', 'lead_id')
@ -24,12 +25,26 @@ class LeadTest(models.Model):
deadline = fields.Boolean(compute='_compute_employee_deadline', store=True)
is_assigned_to_admin = fields.Boolean(string='Assigned to admin user')
stage_id = fields.Many2one(
'test_base_automation.stage', string='Stage',
compute='_compute_stage_id', readonly=False, store=True)
@api.depends('state')
def _compute_stage_id(self):
Test_Base_AutomationStage = self.env['test_base_automation.stage']
for task in self:
if not task.stage_id and task.state == 'draft':
task.stage_id = (
Test_Base_AutomationStage.search([('name', 'ilike', 'new')], limit=1)
or Test_Base_AutomationStage.create({'name': 'New'})
)
@api.depends('partner_id.employee', 'priority')
def _compute_employee_deadline(self):
# this method computes two fields on purpose; don't split it
for record in self:
record.employee = record.partner_id.employee
if not record.priority:
if not record.priority or not record.create_date:
record.deadline = False
else:
record.deadline = record.create_date + relativedelta.relativedelta(days=3)
@ -42,8 +57,15 @@ class LeadTest(models.Model):
return result
class LineTest(models.Model):
_name = "base.automation.line.test"
class BaseAutomationLeadThreadTest(models.Model):
_name = 'base.automation.lead.thread.test'
_description = "Automated Rule Test With Thread"
_inherit = ['base.automation.lead.test', 'mail.thread']
user_id = fields.Many2one("res.users")
class BaseAutomationLineTest(models.Model):
_name = 'base.automation.line.test'
_description = "Automated Rule Line Test"
name = fields.Char()
@ -51,31 +73,37 @@ class LineTest(models.Model):
user_id = fields.Many2one('res.users')
class ModelWithAccess(models.Model):
_name = "base.automation.link.test"
class BaseAutomationLinkTest(models.Model):
_name = 'base.automation.link.test'
_description = "Automated Rule Link Test"
name = fields.Char()
linked_id = fields.Many2one('base.automation.linked.test', ondelete='cascade')
class ModelWithoutAccess(models.Model):
_name = "base.automation.linked.test"
class BaseAutomationLinkedTest(models.Model):
_name = 'base.automation.linked.test'
_description = "Automated Rule Linked Test"
name = fields.Char()
another_field = fields.Char()
class Project(models.Model):
_name = _description = 'test_base_automation.project'
class Test_Base_AutomationProject(models.Model):
_name = 'test_base_automation.project'
_description = 'test_base_automation.project'
name = fields.Char()
task_ids = fields.One2many('test_base_automation.task', 'project_id')
stage_id = fields.Many2one('test_base_automation.stage')
tag_ids = fields.Many2many('test_base_automation.tag')
priority = fields.Selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], default='1')
user_ids = fields.Many2many('res.users')
class Task(models.Model):
_name = _description = 'test_base_automation.task'
class Test_Base_AutomationTask(models.Model):
_name = 'test_base_automation.task'
_description = 'test_base_automation.task'
name = fields.Char()
parent_id = fields.Many2one('test_base_automation.task')
@ -83,9 +111,72 @@ class Task(models.Model):
'test_base_automation.project',
compute='_compute_project_id', recursive=True, store=True, readonly=False,
)
allocated_hours = fields.Float()
trigger_hours = fields.Float("Save time to trigger effective hours")
remaining_hours = fields.Float("Time Remaining", compute='_compute_remaining_hours', store=True, readonly=True, help="Number of allocated hours minus the number of hours spent.")
effective_hours = fields.Float("Time Spent", compute='_compute_effective_hours', compute_sudo=True, store=True)
@api.depends('parent_id.project_id')
def _compute_project_id(self):
for task in self:
if not task.project_id:
task.project_id = task.parent_id.project_id
@api.depends('trigger_hours')
def _compute_effective_hours(self):
for task in self:
task.effective_hours = task.trigger_hours
@api.depends('effective_hours')
def _compute_remaining_hours(self):
for task in self:
if not task.allocated_hours:
task.remaining_hours = 0.0
else:
task.remaining_hours = task.allocated_hours - task.effective_hours
class Test_Base_AutomationStage(models.Model):
_name = 'test_base_automation.stage'
_description = 'test_base_automation.stage'
name = fields.Char()
class Test_Base_AutomationTag(models.Model):
_name = 'test_base_automation.tag'
_description = 'test_base_automation.tag'
name = fields.Char()
# pylint: disable=E0102
class BaseAutomationLeadThreadTest(models.Model): # noqa: F811
_name = 'base.automation.lead.thread.test'
_inherit = ["base.automation.lead.test", "mail.thread"]
_description = "Threaded Lead Test"
class BaseAutomationModelWithRecnameChar(models.Model):
_name = 'base.automation.model.with.recname.char'
_description = "Model with Char as _rec_name"
_rec_name = "description"
description = fields.Char()
user_id = fields.Many2one('res.users', string='Responsible')
class BaseAutomationModelWithRecnameM2o(models.Model):
_name = 'base.automation.model.with.recname.m2o'
_description = "Model with Many2one as _rec_name and name_create"
_rec_name = "user_id"
user_id = fields.Many2one("base.automation.model.with.recname.char", string='Responsible')
@api.model
def name_create(self, name):
name = name.strip()
user = self.env["base.automation.model.with.recname.char"].search([('description', '=ilike', name)], limit=1)
if user:
user_id = user.id
else:
user_id, _user_name = self.env["base.automation.model.with.recname.char"].name_create(name)
record = self.create({'user_id': user_id})
return record.id, record.display_name

View file

@ -1,7 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
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_lead_thread_test,access_base_automation_lead_thread_test,model_base_automation_lead_thread_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_link_test,access_base_automation_link_test,model_base_automation_link_test,,1,1,1,1
access_base_automation_linked_test,access_base_automation_linked_test,model_base_automation_linked_test,,1,1,1,1
access_test_base_automation_project,access_test_base_automation_project,model_test_base_automation_project,,1,1,1,1
access_test_base_automation_task,access_test_base_automation_task,model_test_base_automation_task,,1,1,1,1
access_base_automation_link_test,access_base_automation_link_test,model_base_automation_link_test,base.group_user,1,1,1,1
access_base_automation_linked_test,access_base_automation_linked_test,model_base_automation_linked_test,base.group_user,1,1,1,1
access_base_automation_model_with_recname_char,access_base_automation_model_with_recname_char,model_base_automation_model_with_recname_char,base.group_user,1,1,1,1
access_base_automation_model_with_recname_m2o,access_base_automation_model_with_recname_m2o,model_base_automation_model_with_recname_m2o,base.group_user,1,1,1,1
access_test_base_automation_project,access_test_base_automation_project,model_test_base_automation_project,base.group_user,1,1,1,1
access_test_base_automation_task,access_test_base_automation_task,model_test_base_automation_task,base.group_user,1,1,1,1
access_test_base_automation_stage,access_test_base_automation_stage,model_test_base_automation_stage,base.group_user,1,1,1,1
access_test_base_automation_tag,access_test_base_automation_tag,model_test_base_automation_tag,base.group_user,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_lead_test access_base_automation_lead_test model_base_automation_lead_test base.group_system 1 1 1 1
3 access_base_automation_lead_thread_test access_base_automation_lead_thread_test model_base_automation_lead_thread_test base.group_system 1 1 1 1
4 access_base_automation_line_test access_base_automation_line_test model_base_automation_line_test base.group_system 1 1 1 1
5 access_base_automation_link_test access_base_automation_link_test model_base_automation_link_test base.group_user 1 1 1 1
6 access_base_automation_linked_test access_base_automation_linked_test model_base_automation_linked_test base.group_user 1 1 1 1
7 access_test_base_automation_project access_base_automation_model_with_recname_char access_test_base_automation_project access_base_automation_model_with_recname_char model_test_base_automation_project model_base_automation_model_with_recname_char base.group_user 1 1 1 1
8 access_test_base_automation_task access_base_automation_model_with_recname_m2o access_test_base_automation_task access_base_automation_model_with_recname_m2o model_test_base_automation_task model_base_automation_model_with_recname_m2o base.group_user 1 1 1 1
9 access_test_base_automation_project access_test_base_automation_project model_test_base_automation_project base.group_user 1 1 1 1
10 access_test_base_automation_task access_test_base_automation_task model_test_base_automation_task base.group_user 1 1 1 1
11 access_test_base_automation_stage access_test_base_automation_stage model_test_base_automation_stage base.group_user 1 1 1 1
12 access_test_base_automation_tag access_test_base_automation_tag model_test_base_automation_tag base.group_user 1 1 1 1

View file

@ -0,0 +1,671 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
function assertEqual(actual, expected) {
if (actual !== expected) {
throw new Error(`Assert failed: expected: ${expected} ; got: ${actual}`);
}
}
registry.category("web_tour.tours").add("test_base_automation", {
steps: () => [
stepUtils.showAppsMenuItem(),
{
content: "Create new rule",
trigger: ".o_control_panel button.o-kanban-button-new",
run: "click",
},
{
content: "Enter rule name",
trigger: ".o_form_renderer .oe_title .o_input",
run: "edit Test rule",
},
{
content: "Select model",
trigger: '.o_form_renderer .o_group div[name="model_id"] input',
run: "edit res.partner",
},
{
trigger: ".dropdown-menu:contains(Contact)",
},
{
content: "Select model contact",
trigger: ".dropdown-menu li a:contains(Contact):not(:has(.fa-spin))",
run: "click",
},
{
content: "Open select",
trigger: ".o_form_renderer #trigger_0",
run: "click",
},
{
trigger: ".o_select_menu_item:contains(On create and edit)",
run: "click",
},
{
content: "Add new action",
trigger: '.o_form_renderer div[name="action_server_ids"] button',
run: "click",
},
{
content: "Set new action to update the record",
trigger:
".modal .modal-content .o_form_renderer [name='state'] span[value*='object_write']",
run: "click",
},
{
content: "Focus on the 'update_path' field",
trigger:
".modal .modal-content .o_form_renderer [name='update_path'] .o_model_field_selector",
run: "click",
},
{
content: "Input field name",
trigger: ".o_model_field_selector_popover .o_model_field_selector_popover_search input",
run: "edit Job Position",
},
{
content: "Select field",
trigger:
'.o_model_field_selector_popover .o_model_field_selector_popover_page li[data-name="function"] button',
run: "click",
},
{
content: "Open update select",
trigger:
'.modal .modal-content .o_form_renderer div[name="value"] textarea',
run: "edit Test",
},
{
content: "Open update select",
trigger: ".modal .modal-content .o_form_button_save",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
...stepUtils.saveForm(),
],
});
registry.category("web_tour.tours").add("test_base_automation_on_tag_added", {
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: ".o_control_panel button.o-kanban-button-new",
run: "click",
},
{
trigger: ".o_form_renderer .oe_title .o_input",
run: "edit Test rule",
},
{
trigger: '.o_form_renderer .o_group div[name="model_id"] input',
run: "edit test_base_automation.project",
},
{
trigger:
".dropdown-menu li a:contains(test_base_automation.project):not(:has(.fa-spin))",
run: "click",
},
{
content: "Open select",
trigger: ".o_form_renderer #trigger_0",
run: "click",
},
{
trigger: ".o_select_menu_menu",
run() {
const options = [...this.anchor.querySelectorAll(".o_select_menu_item")].map(
(el) => el.textContent
);
assertEqual(
JSON.stringify(options),
JSON.stringify([
"Stage is set to",
"User is set",
"Tag is added",
"Priority is set to",
"Based on date field",
"After creation",
"After last update",
"On create",
"On create and edit",
"On deletion",
"On UI change",
"On webhook"
])
);
},
},
{
trigger: ".o_select_menu_item:contains(Tag is added)",
run: "click",
},
{
trigger: '.o_form_renderer div[name="trg_field_ref"] input',
run: "edit test",
},
{
trigger: ".dropdown-menu li a:contains(test):not(:has(.fa-spin))",
run: "click",
},
{
trigger: '.o_form_renderer div[name="action_server_ids"] button',
run: "click",
},
{
trigger:
".modal .modal-content .o_form_renderer [name='state'] span[value*='object_write']",
run: "click",
},
{
content: "Focus on the 'update_path' field",
trigger:
".modal .modal-content .o_form_renderer [name='update_path'] .o_model_field_selector",
run: "click",
},
{
content: "Input field name",
trigger:
".o_model_field_selector_popover .o_model_field_selector_popover_search input",
run: "edit Name",
},
{
content: "Select field",
trigger:
'.o_model_field_selector_popover .o_model_field_selector_popover_page li[data-name="name"] button',
run: "click",
},
{
trigger:
'.modal .modal-content .o_form_renderer div[name="value"] textarea',
run: "edit Test",
},
{
trigger: ".modal .modal-content .o_form_button_save",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
trigger: '.o_form_renderer div[name="action_server_ids"] button',
run: "click",
},
{
trigger:
".modal .modal-content .o_form_renderer [name='state'] span[value*='object_write']",
run: "click",
},
{
content: "Focus on the 'update_path' field",
trigger:
".modal .modal-content .o_form_renderer [name='update_path'] .o_model_field_selector",
run: "click",
},
{
content: "Input field name",
trigger:
".o_model_field_selector_popover .o_model_field_selector_popover_search input",
run: "edit Priority",
},
{
content: "Select field",
trigger:
'.o_model_field_selector_popover .o_model_field_selector_popover_page li[data-name="priority"] button',
run: "click",
},
{
trigger:
'.modal .modal-content .o_form_renderer div[name="selection_value"] input',
run: "edit High",
},
{
trigger: ".dropdown-menu li a:contains(High):not(:has(.fa-spin))",
run: "click",
},
{
trigger: ".modal .modal-content .o_form_button_save",
run: "click",
},
{
trigger: "body:not(:has(.modal-content))",
},
...stepUtils.saveForm(),
{
trigger: ".breadcrumb .o_back_button a",
run: "click",
},
{
trigger: ".o_base_automation_kanban_view .o_kanban_record",
run() {
assertEqual(
this.anchor.querySelector(".o_automation_base_info").textContent,
"Test ruletest_base_automation.projectTag is addedtest"
);
assertEqual(
this.anchor.querySelector(".o_automation_actions").textContent,
"Update test_base_automation.projectUpdate test_base_automation.project"
);
},
},
],
});
registry.category("web_tour.tours").add("test_open_automation_from_grouped_kanban", {
steps: () => [
{
trigger: ".o_kanban_header:contains(test tag)",
run: "hover && click .o_kanban_view .o_group_config button.dropdown-toggle",
},
{
trigger: ".dropdown-menu .o_column_automations",
run: "click",
},
{
trigger: ".o_base_automation_kanban_view .o_control_panel button.o-kanban-button-new",
run: "click",
},
{
trigger: ".o_form_view",
run() {
assertEqual(
this.anchor.querySelector(".o_field_widget[name='trigger'] input").value,
"Tag is added"
);
assertEqual(
this.anchor.querySelector(".o_field_widget[name='trg_field_ref'] input").value,
"test tag"
);
},
},
{
trigger: ".o_form_view .o_field_widget[name='name'] input",
run: "edit From Tour",
},
...stepUtils.saveForm(),
],
});
registry.category("web_tour.tours").add("test_kanban_automation_view_stage_trigger", {
steps: () => [
{
trigger: ".o_base_automation_kanban_view",
},
{
trigger: ".o_kanban_record .fs-2:contains(Test Stage)",
},
{
trigger: ".o_kanban_record .o_tag:contains(Stage value)",
},
],
});
registry.category("web_tour.tours").add("test_kanban_automation_view_time_trigger", {
steps: () => [
{
trigger: ".o_base_automation_kanban_view",
},
{
trigger: ".o_automation_base_info > div > div > span:nth-child(1):contains(1)",
},
{
trigger: ".o_automation_base_info .text-lowercase:contains(hours)",
},
{
trigger: `.o_kanban_record .o_tag:contains("Last Automation (Automated Rule Test)")`,
},
],
});
registry.category("web_tour.tours").add("test_kanban_automation_view_time_updated_trigger", {
steps: () => [
{
trigger: ".o_base_automation_kanban_view",
},
{
trigger: ".o_automation_base_info > div > div > span:nth-child(1):contains(1)",
async run() {
const lowercaseTexts = document.querySelectorAll(
".o_automation_base_info .text-lowercase"
);
assertEqual(lowercaseTexts.length, 2);
assertEqual(lowercaseTexts[0].innerText, "hours");
assertEqual(lowercaseTexts[1].innerText, "after last update");
},
},
],
});
registry.category("web_tour.tours").add("test_kanban_automation_view_create_action", {
steps: () => [
{
trigger: ".o_base_automation_kanban_view",
},
{
trigger: "div[name='action_server_ids']:contains(Create Contact with name NameX)",
async run() {
assertEqual(document.querySelectorAll(".fa.fa-plus-square").length, 1);
},
},
],
});
registry.category("web_tour.tours").add("test_resize_kanban", {
steps: () => [
{
trigger: ".o_base_automation_kanban_view",
},
{
trigger:
".o_automation_actions:contains(Set Active To False Set Active To False Set Active To False)",
async run() {
document.body.style.setProperty("width", "500px");
window.dispatchEvent(new Event("resize"));
},
},
{
trigger: ".o_automation_actions:contains(Set Active To False 2 actions)",
},
],
});
registry.category("web_tour.tours").add("test_form_view_resequence_actions", {
steps: () => [
{
trigger:
".o_form_renderer .o_field_widget[name='action_server_ids'] .o_kanban_renderer",
async run() {
assertEqual(
this.anchor.innerText,
"Update Active 0\nUpdate Active 1\nUpdate Active 2"
);
},
},
{
trigger:
".o_form_renderer .o_field_widget[name='action_server_ids'] .o_kanban_record:nth-child(3)",
run: "drag_and_drop(.o_form_renderer .o_field_widget[name='action_server_ids'] .o_kanban_record:nth-child(1))",
},
...stepUtils.saveForm(),
{
trigger:
".o_form_renderer .o_field_widget[name='action_server_ids'] .o_kanban_renderer",
async run() {
assertEqual(
this.anchor.innerText,
"Update Active 2\nUpdate Active 0\nUpdate Active 1"
);
},
},
{
trigger:
".o_form_renderer .o_field_widget[name='action_server_ids'] .o_kanban_view .o_cp_buttons button",
run: "click",
},
{
trigger: ".modal-content .o_form_renderer",
run() {
const allFields = this.anchor.querySelectorAll(".o_field_widget[name]");
assertEqual(
Array.from(allFields)
.map((el) => el.getAttribute("name"))
.includes("model_id"),
false
);
},
},
{
trigger: ".modal-content .o_form_renderer [name='state'] span[value*='followers']",
run: "click",
},
{
trigger:
".modal-content .o_form_renderer [name='state'] span.active[value*='followers']",
},
{
trigger: ".modal-content .o_form_button_cancel",
run: "click",
},
{
trigger: "body:not(:has(.modal-content))",
},
],
});
registry.category("web_tour.tours").add("test_form_view_model_id", {
steps: () => [
{
trigger: ".o_field_widget[name='model_id'] input",
run: "edit base.automation.line.test",
},
{
trigger: ".dropdown-menu li a:contains(Automated Rule Line Test)",
run: "click",
},
{
trigger: ".o_field_widget[name='trigger'] input",
run: "click",
},
{
trigger: ".o_select_menu_menu",
run() {
assertEqual(
Array.from(this.anchor.querySelectorAll(".o_select_menu_group"))
.map((el) => el.textContent)
.join(", "),
"Values Updated, Timing Conditions, Custom, External"
);
assertEqual(
Array.from(this.anchor.querySelectorAll(".o_select_menu_item"))
.map((el) => el.textContent)
.join(", "),
"User is set, Based on date field, After creation, After last update, On create, On create and edit, On deletion, On UI change, On webhook"
);
}
},
{
trigger: ".o_field_widget[name='model_id'] input",
run: "edit test_base_automation.project",
},
{
trigger: ".dropdown-menu li a:contains(test_base_automation.project)",
run: "click",
},
{
trigger: ".o_field_widget[name='trigger'] input",
run: "click",
},
{
trigger: ".o_select_menu_menu",
run() {
assertEqual(
Array.from(this.anchor.querySelectorAll(".o_select_menu_group"))
.map((el) => el.textContent)
.join(", "),
"Values Updated, Timing Conditions, Custom, External"
);
assertEqual(
Array.from(this.anchor.querySelectorAll(".o_select_menu_item"))
.map((el) => el.textContent)
.join(", "),
"Stage is set to, User is set, Tag is added, Priority is set to, Based on date field, After creation, After last update, On create, On create and edit, On deletion, On UI change, On webhook"
);
}
},
{
trigger: ".o_form_button_cancel",
run: "click",
},
{
trigger: ".o_base_automation_kanban_view",
},
],
});
registry.category("web_tour.tours").add("test_form_view_custom_reference_field", {
steps: () => [
{
trigger: ".o_field_widget[name='model_id'] input",
run: "edit test_base_automation.project",
},
{
trigger: ".dropdown-menu li a:contains(test_base_automation.project)",
run: "click",
},
{
trigger: "body:not(:has(.o_field_widget[name='trg_field_ref']))",
},
{
content: "Open select",
trigger: ".o_form_renderer #trigger_0",
run: "click",
},
{
trigger: ".o_select_menu_item:contains(Stage is set to)",
run: "click",
},
{
trigger: ".o_field_widget[name='trg_field_ref'] input",
run: "fill test",
},
{
trigger:
".o_field_widget[name='trg_field_ref'] .o-autocomplete--dropdown-menu:not(:has(a .fa-spin)",
run() {
assertEqual(this.anchor.innerText, "test stage\nSearch more...");
},
},
{
content: "Open select",
trigger: ".o_form_renderer #trigger_0",
run: "click",
},
{
trigger: ".o_select_menu_item:contains(Tag is added)",
run: "click",
},
{
trigger:
".o_field_widget[name='trg_field_ref'] :not(:has(.o-autocomplete--dropdown-menu))",
},
{
trigger: ".o_field_widget[name='trg_field_ref'] input",
run: "fill test",
},
{
trigger:
".o_field_widget[name='trg_field_ref'] .o-autocomplete--dropdown-menu:not(:has(a .fa-spin)",
run() {
assertEqual(this.anchor.innerText, "test tag\nSearch more...");
},
},
{
trigger: ".o_form_button_cancel",
run: "click",
},
{
trigger: ".o_base_automation_kanban_view",
},
],
});
registry.category("web_tour.tours").add("test_form_view_mail_triggers", {
steps: () => [
{
trigger: ".o_field_widget[name='model_id'] input",
run: "edit base.automation.lead.test",
},
{
trigger: ".dropdown-menu li a:contains(Automated Rule Test)",
run: "click",
},
{
trigger: ".o_field_widget[name='trigger'] input",
run: "click",
},
{
trigger: ".o_select_menu_menu",
run() {
assertEqual(
Array.from(this.anchor.querySelectorAll(".o_select_menu_group"))
.map((el) => el.textContent)
.join(", "),
"Values Updated, Timing Conditions, Custom, External"
);
},
},
{
trigger: ".o_field_widget[name='model_id'] input",
run: "edit base.automation.lead.thread.test",
},
{
trigger: ".dropdown-menu li a:contains(Threaded Lead Test)",
run: "click",
},
{
trigger: ".o_field_widget[name='trigger'] input",
run: "click",
},
{
trigger: ".o_select_menu_menu",
run() {
assertEqual(
Array.from(this.anchor.querySelectorAll(".o_select_menu_group "))
.map((el) => el.textContent)
.join(", "),
"Values Updated, Email Events, Timing Conditions, Custom, External"
);
}
},
{
trigger: "button.o_form_button_cancel",
run: "click",
},
{
trigger: "body:not(:has(button.o_form_button_cancel)",
},
],
});
registry.category("web_tour.tours").add("base_automation.on_change_rule_creation", {
url: "/odoo/action-base_automation.base_automation_act",
steps: () => [
{
trigger: ".o-kanban-button-new",
run: "click",
},
{
trigger: ".o_field_widget[name=name] input",
run: "edit Test rule",
},
{
trigger: ".o_field_widget[name=model_id] input",
run: "edit ir.ui.view",
},
{
trigger: ".ui-menu-item > a:text(View)",
run: "click",
},
{
content: "Open select",
trigger: ".o_form_renderer #trigger_0",
run: "click",
},
{
trigger: ".o_select_menu_item:contains(On UI change)",
run: "click",
},
{
trigger: ".o_field_widget[name=on_change_field_ids] input",
run: "edit Active",
},
{
trigger: ".ui-menu-item > a:text(Active)",
run: "click",
},
...stepUtils.saveForm(),
],
});

View file

@ -2,3 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_flow
from . import test_server_actions
from . import test_tour

View file

@ -0,0 +1,77 @@
# # Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.base.models.ir_actions import ServerActionWithWarningsError
from odoo.exceptions import ValidationError
from odoo.addons.base.tests.test_ir_actions import TestServerActionsBase
class TestServerActionsValidation(TestServerActionsBase):
def test_multi_action_children_warnings(self):
self.action.write({
'state': 'multi',
'child_ids': [self.test_server_action.id]
})
self.assertEqual(self.action.model_id.model, "res.partner")
self.assertEqual(self.test_server_action.model_id.model, "ir.actions.server")
self.assertEqual(self.action.warning, "Following child actions should have the same model (Contact): TestDummyServerAction")
new_action = self.action.copy()
with self.assertRaises(ValidationError) as ve:
new_action.write({
'child_ids': [self.action.id]
})
self.assertEqual(ve.exception.args[0], "Following child actions have warnings: TestAction")
def test_webhook_payload_includes_group_restricted_fields(self):
self.test_server_action.write({
'state': 'webhook',
'webhook_field_ids': [self.env['ir.model.fields']._get('ir.actions.server', 'code').id],
})
self.assertEqual(self.test_server_action.warning, "Group-restricted fields cannot be included in "
"webhook payloads, as it could allow any user to "
"accidentally leak sensitive information. You will "
"have to remove the following fields from the webhook payload:\n"
"- Python Code")
def test_recursion_in_child(self):
new_action = self.action.copy()
self.action.write({
'state': 'multi',
'child_ids': [new_action.id]
})
with self.assertRaises(ValidationError) as ve:
new_action.write({
'child_ids': [self.action.id]
})
self.assertEqual(ve.exception.args[0], "Recursion found in child server actions")
def test_non_relational_field_traversal(self):
self.action.write({
'state': 'object_write',
'update_path': 'parent_id.name',
'value': 'TestNew',
})
with self.assertRaises(ValidationError) as ve:
self.action.write({'update_path': 'parent_id.name.something_else'})
self.assertEqual(ve.exception.args[0], "The path contained by the field "
"'Field to Update Path' contains a non-relational field"
" (Name) that is not the last field in the path. You "
"can't traverse non-relational fields (even in the quantum"
" realm). Make sure only the last field in the path is non-relational.")
def test_python_bad_expr(self):
with self.assertRaises(ValidationError) as ve:
self.test_server_action.write({'code': 'this is invalid python code'})
self.assertEqual(
ve.exception.args[0],
"SyntaxError : invalid syntax at line 1\n"
"this is invalid python code\n")
def test_cannot_run_if_warnings(self):
self.action.write({
'state': 'multi',
'child_ids': [self.test_server_action.id]
})
self.assertTrue(self.action.warning)
with self.assertRaises(ServerActionWithWarningsError) as e:
self.action.run()
self.assertEqual(e.exception.args[0], "Server action TestAction has one or more warnings, address them first.")

View file

@ -0,0 +1,304 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from urllib.parse import urlencode
import ast
from odoo import Command
from odoo.tests import HttpCase, tagged
def _urlencode_kwargs(**kwargs):
return urlencode(kwargs)
@tagged("post_install_l10n", "post_install", "-at_install")
class BaseAutomationTestUi(HttpCase):
def _neutralize_preexisting_automations(self, neutralize_action=True):
self.env["base.automation"].with_context(active_test=False).search([]).write({"active": False})
if neutralize_action:
context = ast.literal_eval(self.env.ref("base_automation.base_automation_act").context)
del context["search_default_inactive"]
self.env.ref("base_automation.base_automation_act").context = str(context)
def test_01_base_automation_tour(self):
self._neutralize_preexisting_automations()
self.start_tour("/odoo/action-base_automation.base_automation_act?debug=tests", "test_base_automation", login="admin")
base_automation = self.env["base.automation"].search([])
self.assertEqual(base_automation.model_id.model, "res.partner")
self.assertEqual(base_automation.trigger, "on_create_or_write")
self.assertEqual(base_automation.action_server_ids.state, "object_write") # only one action
self.assertEqual(base_automation.action_server_ids.model_name, "res.partner")
self.assertEqual(base_automation.action_server_ids.update_field_id.name, "function")
self.assertEqual(base_automation.action_server_ids.value, "Test")
def test_base_automation_on_tag_added(self):
self._neutralize_preexisting_automations()
self.env["test_base_automation.tag"].create({"name": "test"})
self.start_tour("/odoo/action-base_automation.base_automation_act?debug=tests", "test_base_automation_on_tag_added", login="admin")
def test_open_automation_from_grouped_kanban(self):
self._neutralize_preexisting_automations()
test_view = self.env["ir.ui.view"].create(
{
"name": "test_view",
"model": "test_base_automation.project",
"type": "kanban",
"arch": """
<kanban default_group_by="tag_ids">
<templates>
<t t-name="card">
<field name="name" />
</t>
</templates>
</kanban>
""",
}
)
test_action = self.env["ir.actions.act_window"].create(
{
"name": "test action",
"res_model": "test_base_automation.project",
"view_ids": [Command.create({"view_id": test_view.id, "view_mode": "kanban"})],
}
)
tag = self.env["test_base_automation.tag"].create({"name": "test tag"})
self.env["test_base_automation.project"].create({"name": "test", "tag_ids": [Command.link(tag.id)]})
self.start_tour(f"/odoo/action-{test_action.id}?debug=0", "test_open_automation_from_grouped_kanban", login="admin")
base_auto = self.env["base.automation"].search([])
self.assertEqual(base_auto.name, "From Tour")
self.assertEqual(base_auto.model_name, "test_base_automation.project")
self.assertEqual(base_auto.trigger_field_ids.name, "tag_ids")
self.assertEqual(base_auto.trigger, "on_tag_set")
self.assertEqual(base_auto.trg_field_ref_model_name, "test_base_automation.tag")
self.assertEqual(base_auto.trg_field_ref, tag.id)
def test_kanban_automation_view_stage_trigger(self):
self._neutralize_preexisting_automations()
project_model = self.env.ref('test_base_automation.model_test_base_automation_project')
stage_field = self.env['ir.model.fields'].search([
('model_id', '=', project_model.id),
('name', '=', 'stage_id'),
])
test_stage = self.env['test_base_automation.stage'].create({'name': 'Stage value'})
automation = self.env["base.automation"].create({
"name": "Test Stage",
"trigger": "on_stage_set",
"model_id": project_model.id,
"trigger_field_ids": [stage_field.id],
"trg_field_ref": test_stage,
})
action = {
"name": "Set Active To False",
"base_automation_id": automation.id,
"state": "object_write",
"update_path": "user_ids.active",
"value": False,
"model_id": project_model.id
}
automation.write({"action_server_ids": [Command.create(action)]})
self.start_tour(
"/odoo/action-base_automation.base_automation_act",
"test_kanban_automation_view_stage_trigger", login="admin"
)
def test_kanban_automation_view_time_trigger(self):
self._neutralize_preexisting_automations()
model = self.env['ir.model']._get("base.automation.lead.test")
date_field = self.env['ir.model.fields'].search([
('model_id', '=', model.id),
('name', '=', 'date_automation_last'),
])
self.env["base.automation"].create({
"name": "Test Date",
"trigger": "on_time",
"model_id": model.id,
"trg_date_range": 1,
"trg_date_range_type": "hour",
"trg_date_id": date_field.id,
})
self.start_tour(
"/odoo/action-base_automation.base_automation_act",
"test_kanban_automation_view_time_trigger", login="admin"
)
def test_kanban_automation_view_time_updated_trigger(self):
self._neutralize_preexisting_automations()
model = self.env.ref("base.model_res_partner")
self.env["base.automation"].create({
"name": "Test Date",
"trigger": "on_time_updated",
"model_id": model.id,
"trg_date_range": 1,
"trg_date_range_type": "hour",
})
self.start_tour(
"/odoo/action-base_automation.base_automation_act",
"test_kanban_automation_view_time_updated_trigger", login="admin"
)
def test_kanban_automation_view_create_action(self):
self._neutralize_preexisting_automations()
model = self.env.ref("base.model_res_partner")
automation = self.env["base.automation"].create({
"name": "Test",
"trigger": "on_create_or_write",
"model_id": model.id,
})
action = {
"name": "Create Contact with name NameX",
"base_automation_id": automation.id,
"state": "object_create",
"value": "NameX",
"model_id": model.id
}
automation.write({"action_server_ids": [Command.create(action)]})
self.start_tour(
"/odoo/action-base_automation.base_automation_act",
"test_kanban_automation_view_create_action", login="admin"
)
def test_resize_kanban(self):
self._neutralize_preexisting_automations()
model = self.env.ref("base.model_res_partner")
automation = self.env["base.automation"].create(
{
"name": "Test",
"trigger": "on_create_or_write",
"model_id": model.id,
}
)
action = {
"name": "Set Active To False",
"base_automation_id": automation.id,
"state": "object_write",
"update_path": "active",
"value": False,
"model_id": model.id,
}
automation.write({"action_server_ids": [Command.create(action) for i in range(3)]})
self.start_tour(
"/odoo/action-base_automation.base_automation_act",
"test_resize_kanban",
login="admin",
)
def test_form_view(self):
model = self.env.ref("base.model_res_partner")
automation = self.env["base.automation"].create(
{
"name": "Test",
"trigger": "on_create_or_write",
"model_id": model.id,
}
)
action = {
"name": "Update Active",
"base_automation_id": automation.id,
"state": "object_write",
"update_path": "active",
"update_boolean_value": "false",
"model_id": model.id,
}
automation.write(
{"action_server_ids": [Command.create(dict(action, name=action["name"] + f" {i}", sequence=i)) for i in range(3)]}
)
self.assertEqual(
automation.action_server_ids.mapped("name"),
["Update Active 0", "Update Active 1", "Update Active 2"],
)
onchange_link_passes = 0
origin_link_onchange = type(self.env["ir.actions.server"]).onchange
def _onchange_base_auto_link(self_model, *args):
nonlocal onchange_link_passes
onchange_link_passes += 1
res = origin_link_onchange(self_model, *args)
if onchange_link_passes == 1:
default_keys = {k: v for k, v in self_model.env.context.items() if k.startswith("default_")}
self.assertEqual(
default_keys,
{"default_model_id": model.id, "default_usage": "base_automation"},
)
if onchange_link_passes == 2:
self.assertEqual(res["value"]["name"], "Add Followers")
return res
self.patch(type(self.env["ir.actions.server"]), "onchange", _onchange_base_auto_link)
self.start_tour(
(
f"/odoo/action-base_automation.base_automation_act/{automation.id}?debug=0"
),
"test_form_view_resequence_actions",
login="admin",
)
self.assertEqual(onchange_link_passes, 2)
self.assertEqual(
automation.action_server_ids.mapped("name"),
["Update Active 2", "Update Active 0", "Update Active 1"],
)
def test_form_view_model_id(self):
self.start_tour(
(
"/odoo/action-base_automation.base_automation_act/new?view_type='form'&amp;debug=0)"
),
"test_form_view_model_id",
login="admin",
)
def test_form_view_custom_reference_field(self):
self.env["test_base_automation.stage"].create({"name": "test stage"})
self.env["test_base_automation.tag"].create({"name": "test tag"})
self.start_tour(
(
"/odoo/action-base_automation.base_automation_act/new?view_type='form'&amp;debug=0)"
),
"test_form_view_custom_reference_field",
login="admin",
)
def test_form_view_mail_triggers(self):
self.start_tour(
(
"/odoo/action-base_automation.base_automation_act/new?view_type='form'&debug=0)"
),
"test_form_view_mail_triggers",
login="admin",
)
def test_on_change_rule_creation(self):
""" test on_change rule creation from the UI """
self.start_tour("/odoo/action-base_automation.base_automation_act", 'base_automation.on_change_rule_creation', login="admin")
rule = self.env['base.automation'].search([], order="create_date desc", limit=1)[0]
view_model = self.env['ir.model']._get("ir.ui.view")
active_field = self.env['ir.model.fields'].search([
('name', '=', 'active'),
('model', '=', 'ir.ui.view'),
])[0]
self.assertEqual(rule.name, "Test rule")
self.assertEqual(rule.model_id, view_model)
self.assertEqual(rule.trigger, 'on_change')
self.assertEqual(len(rule.on_change_field_ids), 1)
self.assertEqual(rule.on_change_field_ids[0], active_field)