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)

View file

@ -12,7 +12,6 @@ pip install odoo-bringout-oca-ocb-test_crm_full
## Dependencies
This addon depends on:
- crm
- crm_iap_enrich
- crm_iap_mine
@ -24,34 +23,12 @@ This addon depends on:
- website_crm_partner_assign
- website_crm_livechat
## Manifest Information
- **Name**: Test Full Crm Flow
- **Version**: 1.0
- **Category**: Hidden/Tests
- **License**: LGPL-3
- **Installable**: False
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_crm_full`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/test_crm_full
## 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,21 +1,23 @@
[project]
name = "odoo-bringout-oca-ocb-test_crm_full"
version = "16.0.0"
description = "Test Full Crm Flow - Odoo addon"
description = "Test Full Crm Flow -
Odoo addon
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-crm>=16.0.0",
"odoo-bringout-oca-ocb-crm_iap_enrich>=16.0.0",
"odoo-bringout-oca-ocb-crm_iap_mine>=16.0.0",
"odoo-bringout-oca-ocb-crm_sms>=16.0.0",
"odoo-bringout-oca-ocb-event_crm>=16.0.0",
"odoo-bringout-oca-ocb-sale_crm>=16.0.0",
"odoo-bringout-oca-ocb-website_crm>=16.0.0",
"odoo-bringout-oca-ocb-website_crm_iap_reveal>=16.0.0",
"odoo-bringout-oca-ocb-website_crm_partner_assign>=16.0.0",
"odoo-bringout-oca-ocb-website_crm_livechat>=16.0.0",
"odoo-bringout-oca-ocb-crm>=19.0.0",
"odoo-bringout-oca-ocb-crm_iap_enrich>=19.0.0",
"odoo-bringout-oca-ocb-crm_iap_mine>=19.0.0",
"odoo-bringout-oca-ocb-crm_sms>=19.0.0",
"odoo-bringout-oca-ocb-event_crm>=19.0.0",
"odoo-bringout-oca-ocb-sale_crm>=19.0.0",
"odoo-bringout-oca-ocb-website_crm>=19.0.0",
"odoo-bringout-oca-ocb-website_crm_iap_reveal>=19.0.0",
"odoo-bringout-oca-ocb-website_crm_partner_assign>=19.0.0",
"odoo-bringout-oca-ocb-website_crm_livechat>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -25,7 +27,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

@ -20,5 +20,6 @@ backend. It notably includes IAP bridges modules to test their impact. """,
'website_crm_partner_assign',
'website_crm_livechat',
],
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -13,7 +13,6 @@ class TestCrmFullCommon(TestCrmCommon, MockIAPReveal, MockVisitor):
@classmethod
def setUpClass(cls):
super(TestCrmFullCommon, cls).setUpClass()
cls._init_mail_gateway()
cls._activate_multi_company()
# Context data: dates
@ -34,7 +33,6 @@ class TestCrmFullCommon(TestCrmCommon, MockIAPReveal, MockVisitor):
'email': 'partner.email.%02d@test.example.com' % idx,
'function': 'Noisy Customer',
'lang': 'fr_BE',
'mobile': '04569999%02d' % idx,
'name': 'PartnerCustomer',
'phone': '04560000%02d' % idx,
'street': 'Super Street, %092d' % idx,

View file

@ -4,8 +4,7 @@
from freezegun import freeze_time
from odoo.addons.test_crm_full.tests.common import TestCrmFullCommon
from odoo.tests.common import users, warmup, Form
from odoo.tests import tagged
from odoo.tests import Form, users, warmup, tagged
@tagged('crm_performance', 'post_install', '-at_install', '-standard')
@ -15,10 +14,13 @@ class CrmPerformanceCase(TestCrmFullCommon):
super(CrmPerformanceCase, self).setUp()
# patch registry to simulate a ready environment
self.patch(self.env.registry, 'ready', True)
# we don't use mock_mail_gateway thus want to mock smtp to test the stack
self._mock_smtplib_connection()
self._flush_tracking()
self.user_sales_leads.write({
'groups_id': [
'group_ids': [
(4, self.env.ref('event.group_event_user').id),
(4, self.env.ref('im_livechat.im_livechat_group_user').id),
]
@ -40,16 +42,15 @@ class TestCrmPerformance(CrmPerformanceCase):
""" Test multiple lead creation (import) """
batch_size = 10
country_be = self.env.ref('base.be')
lang_be_id = self.env['res.lang']._lang_get_id('fr_BE')
lang_be_id = self.env['res.lang']._get_data(code='fr_BE').id
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=194): # tcf 193 / com 194
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=192): # tcf 191
self.env.cr._now = self.reference_now # force create_date to check schedulers
crm_values = [
{'country_id': country_be.id,
'email_from': 'address.email.%02d@test.example.com' % idx,
'function': 'Noisy Customer',
'lang_id': lang_be_id,
'mobile': '04551111%02d' % idx,
'name': 'Test Lead %02d' % idx,
'phone': '04550000%02d' % idx,
'street': 'Super Street, %092d' % idx,
@ -70,14 +71,13 @@ class TestCrmPerformance(CrmPerformanceCase):
country_be = self.env.ref('base.be')
lang_be = self.env['res.lang']._lang_get('fr_BE')
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=189): # tcf only: 173 - com runbot: 174/175
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=145): # tcf 142 / com 144
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['crm.lead']) as lead_form:
lead_form.country_id = country_be
lead_form.email_from = 'address.email@test.example.com'
lead_form.function = 'Noisy Customer'
lead_form.lang_id = lang_be
lead_form.mobile = '0455111100'
lead_form.name = 'Test Lead'
lead_form.phone = '0455000011'
lead_form.street = 'Super Street, 00'
@ -89,7 +89,7 @@ class TestCrmPerformance(CrmPerformanceCase):
@warmup
def test_lead_create_form_partner(self):
""" Test a single lead creation using Form with a partner """
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=199): # tcf 186 / com 188
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=144): # tcf 141 / com 143
self.env.cr._now = self.reference_now # force create_date to check schedulers
with self.debug_mode():
# {'invisible': ['|', ('type', '=', 'opportunity'), ('is_partner_visible', '=', False)]}
@ -105,16 +105,15 @@ class TestCrmPerformance(CrmPerformanceCase):
def test_lead_create_single_address(self):
""" Test multiple lead creation (import) """
country_be = self.env.ref('base.be')
lang_be_id = self.env['res.lang']._lang_get_id('fr_BE')
lang_be_id = self.env['res.lang']._get_data(code='fr_BE').id
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=43): # tcf only: 41 - com runbot: 42
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=30): # tcf 29
self.env.cr._now = self.reference_now # force create_date to check schedulers
crm_values = [
{'country_id': country_be.id,
'email_from': 'address.email.00@test.example.com',
'function': 'Noisy Customer',
'lang_id': lang_be_id,
'mobile': '0455111100',
'name': 'Test Lead',
'phone': '0455000000',
'street': 'Super Street, 00',
@ -127,7 +126,7 @@ class TestCrmPerformance(CrmPerformanceCase):
@warmup
def test_lead_create_single_partner(self):
""" Test multiple lead creation (import) """
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=49): # tcf only: 47 - com runbot: 48
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=30): # tcf 29
self.env.cr._now = self.reference_now # force create_date to check schedulers
crm_values = [
{'partner_id': self.partners[0].id,

View file

@ -10,45 +10,27 @@ pip install odoo-bringout-oca-ocb-test_discuss_full
## Dependencies
This addon depends on:
- calendar
- crm
- crm_livechat
- hr_attendance
- hr_fleet
- hr_holidays
- hr_homeworking
- im_livechat
- mail
- mail_bot
- note
- project_todo
- website_livechat
## Manifest Information
- **Name**: Test Discuss (full)
- **Version**: 1.0
- **Category**: Hidden
- **License**: LGPL-3
- **Installable**: True
- website_sale
- website_slides
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_discuss_full`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/test_discuss_full
## 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,20 +1,27 @@
[project]
name = "odoo-bringout-oca-ocb-test_discuss_full"
version = "16.0.0"
description = "Test Discuss (full) - Test of Discuss with all possible overrides installed."
description = "Test Discuss (full) -
Test of Discuss with all possible overrides installed.
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-calendar>=16.0.0",
"odoo-bringout-oca-ocb-crm>=16.0.0",
"odoo-bringout-oca-ocb-crm_livechat>=16.0.0",
"odoo-bringout-oca-ocb-hr_holidays>=16.0.0",
"odoo-bringout-oca-ocb-im_livechat>=16.0.0",
"odoo-bringout-oca-ocb-mail>=16.0.0",
"odoo-bringout-oca-ocb-mail_bot>=16.0.0",
"odoo-bringout-oca-ocb-note>=16.0.0",
"odoo-bringout-oca-ocb-website_livechat>=16.0.0",
"odoo-bringout-oca-ocb-calendar>=19.0.0",
"odoo-bringout-oca-ocb-crm>=19.0.0",
"odoo-bringout-oca-ocb-crm_livechat>=19.0.0",
"odoo-bringout-oca-ocb-hr_attendance>=19.0.0",
"odoo-bringout-oca-ocb-hr_fleet>=19.0.0",
"odoo-bringout-oca-ocb-hr_holidays>=19.0.0",
"TODO_MAP-hr_homeworking>=19.0.0",
"odoo-bringout-oca-ocb-im_livechat>=19.0.0",
"odoo-bringout-oca-ocb-mail>=19.0.0",
"odoo-bringout-oca-ocb-mail_bot>=19.0.0",
"TODO_MAP-project_todo>=19.0.0",
"odoo-bringout-oca-ocb-website_livechat>=19.0.0",
"odoo-bringout-oca-ocb-website_sale>=19.0.0",
"odoo-bringout-oca-ocb-website_slides>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -24,7 +31,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

@ -2,23 +2,34 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Test Discuss (full)',
'version': '1.0',
'category': 'Hidden',
'sequence': 9877,
'summary': 'Test of Discuss with all possible overrides installed.',
'description': """Test of Discuss with all possible overrides installed, including feature and performance tests.""",
'depends': [
'calendar',
'crm',
'crm_livechat',
'hr_holidays',
'im_livechat',
'mail',
'mail_bot',
'note',
'website_livechat',
"name": "Test Discuss (full)",
"version": "1.0",
"category": "Productivity/Discuss",
"sequence": 9877,
"summary": "Test of Discuss with all possible overrides installed.",
"description": """Test of Discuss with all possible overrides installed, including feature and performance tests.""",
"depends": [
"calendar",
"crm",
"crm_livechat",
"hr_attendance",
"hr_fleet",
"hr_holidays",
"hr_homeworking",
"im_livechat",
"mail",
"mail_bot",
"project_todo",
"website_livechat",
"website_sale",
"website_slides",
],
'installable': True,
'license': 'LGPL-3',
"installable": True,
"assets": {
"web.assets_tests": [
"test_discuss_full/static/tests/tours/**/*",
],
},
"author": "Odoo S.A.",
"license": "LGPL-3",
}

View file

@ -0,0 +1,60 @@
import { registry } from "@web/core/registry";
const steps = [
{
content: "Open the avatar card popover",
trigger: ".o-mail-Message-avatar",
run: "click",
},
{
content: "Check that the employee's work email is displayed",
trigger: ".o_avatar_card:contains(test_employee@test.com)",
},
{
content: "Check that the employee's department is displayed",
trigger: ".o_avatar_card:contains(Test Department)",
},
{
content: "Check that the employee's work phone is displayed",
trigger: ".o_avatar_card:contains(123456789)",
},
{
content: "Check that the employee's holiday status is displayed",
trigger: ".o_avatar_card:contains(Back on)",
},
];
registry.category("web_tour.tours").add("avatar_card_tour", {
steps: () => [
...steps,
{
content: "Check that the employee's job title is displayed",
trigger: ".o_avatar_card:contains(Test Job Title)",
},
{
trigger: ".o-mail-ActivityMenu-counter:text('2')",
},
{
trigger: ".o_switch_company_menu button",
run: "click",
},
{
trigger: `[role=button][title='Switch to Company 2']`,
run: "click",
expectUnloadPage: true,
},
{
trigger: ".o-mail-ActivityMenu-counter:text('1')",
},
],
});
registry.category("web_tour.tours").add("avatar_card_tour_no_hr_access", {
steps: () => [
...steps,
{
content: "Check that the employee's job title is displayed",
trigger: ":not(.o_avatar_card:contains(Test Job Title))",
},
],
});

View file

@ -0,0 +1,28 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("chatbot_redirect_to_portal", {
url: "/contactus",
steps: () => [
{
trigger: ".o-livechat-root:shadow .o-livechat-LivechatButton",
run: "click",
},
{
trigger:
".o-livechat-root:shadow .o-mail-Message:contains(Hello, were do you want to go?)",
run: "click",
},
{
trigger: ".o-livechat-root:shadow li button:contains(Go to the portal page)",
run: "click",
expectUnloadPage: true,
},
{
trigger: ".o-livechat-root:shadow .o-mail-Message:contains('Go to the portal page')",
},
{ trigger: "#chatterRoot:shadow .o-mail-Chatter" },
{
trigger: ".o-livechat-root:shadow .o-mail-Message:last:contains('Tadam')",
},
],
});

View file

@ -0,0 +1,17 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("im_livechat_session_open", {
steps: () => [
{
trigger: "button.o_switch_view.o_list",
run: "click",
},
{
trigger: ".o_data_cell:contains(Visitor)",
run: "click",
},
{
trigger: ".o-mail-Thread:contains('The conversation is empty.')",
},
],
});

View file

@ -1,4 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_avatar_card_tour
from . import test_livechat_hr_holidays
from . import test_im_livechat_portal
from . import test_performance
from . import test_performance_inbox
from . import test_livechat_session_open
from . import test_res_partner

View file

@ -0,0 +1,138 @@
from datetime import date, timedelta
from odoo import Command
from odoo.tests import tagged, users
from odoo.tests.common import HttpCase, new_test_user
from odoo.addons.mail.tests.common import MailCommon
@tagged("post_install", "-at_install")
class TestAvatarCardTour(MailCommon, HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
new_test_user(
cls.env,
login="hr_user",
company_ids=[Command.link(cls.env.company.id), Command.link(cls.company_2.id)],
groups="hr.group_hr_user",
)
# hr setup for multi-company
department = (
cls.env["hr.department"]
.with_company(cls.company_2)
.create({"name": "Test Department", "company_id": cls.company_2.id})
)
job = (
cls.env["hr.job"]
.with_company(cls.company_2)
.create({"name": "Test Job Title", "company_id": cls.company_2.id})
)
other_partner = (
cls.env["res.partner"]
.with_company(cls.company_2)
.create({
"name": "Test Other Partner",
"company_id": cls.company_2.id,
"phone": "987654321",
})
)
test_employee = (
cls.env["hr.employee"]
.with_company(cls.company_2)
.create({
"name": "Test Employee",
"user_id": cls.user_employee_c2.id,
"company_id": cls.company_2.id,
"department_id": department.id,
"job_id": job.id,
"address_id": other_partner.id,
"work_email": "test_employee@test.com",
"work_phone": "123456789",
})
)
cls.test_employee = test_employee
cls.user_employee_c2.write({"employee_ids": [Command.link(test_employee.id)]})
new_test_user(
cls.env,
login="base_user",
company_ids=[Command.link(cls.env.company.id), Command.link(cls.company_2.id)],
)
# hr_holidays setup for multi-company
leave_type = (
cls.env["hr.leave.type"]
.with_company(cls.company_2)
.create(
{
"name": "Time Off multi company",
"company_id": cls.company_2.id,
"time_type": "leave",
"requires_allocation": False,
}
)
)
cls.env["hr.leave"].with_company(cls.company_2).with_context(
leave_skip_state_check=True
).create(
{
"name": "Test Leave",
"company_id": cls.company_2.id,
"holiday_status_id": leave_type.id,
"employee_id": cls.test_employee.id,
"request_date_from": (date.today() - timedelta(days=1)),
"request_date_to": (date.today() + timedelta(days=1)),
"state": "validate",
}
)
cls.test_record = cls.env["hr.department"].create(
[
{"name": "Test", "company_id": cls.env.company.id},
{"name": "Test 2", "company_id": cls.env.company.id},
{"name": "Test 3", "company_id": cls.company_2.id},
]
)
def _setup_channel(self, user):
self.user_employee_c2.partner_id.sudo().with_user(self.user_employee_c2).message_post(
body="Test message in chatter",
message_type="comment",
subtype_xmlid="mail.mt_comment",
)
activity_type_todo = "mail.mail_activity_data_todo"
self.test_record[0].activity_schedule(
activity_type_todo,
summary="Test Activity for Company 2",
user_id=user.id,
)
self.test_record[1].activity_schedule(
activity_type_todo,
summary="Another Test Activity for Company 2",
user_id=user.id,
)
self.test_record[2].activity_schedule(
activity_type_todo,
summary="Test Activity for Company 3",
user_id=user.id,
)
@users("admin", "hr_user")
def test_avatar_card_tour_multi_company(self):
# Clear existing activities to avoid interference with the test
self.env["mail.activity"].with_user(self.env.user).search([]).unlink()
self._setup_channel(self.env.user)
self.start_tour(
f"/odoo/res.partner/{self.user_employee_c2.partner_id.id}",
"avatar_card_tour",
login=self.env.user.login,
)
@users("base_user")
def test_avatar_card_tour_multi_company_no_hr_access(self):
self._setup_channel(self.env.user)
self.start_tour(
f"/odoo/res.partner/{self.user_employee_c2.partner_id.id}",
"avatar_card_tour_no_hr_access",
login=self.env.user.login,
)

View file

@ -0,0 +1,52 @@
from odoo import Command, tests
from odoo.addons.website_livechat.tests.test_chatbot_ui import TestLivechatChatbotUI
@tests.common.tagged("post_install", "-at_install")
class TestImLivechatPortal(TestLivechatChatbotUI):
def test_chatbot_redirect_to_portal(self):
project = self.env["project.project"].create({"name": "Portal Project"})
task = self.env["project.task"].create(
{"name": "Test Task Name Match", "project_id": project.id}
)
chatbot_redirect_script = self.env["chatbot.script"].create({"title": "Redirection Bot"})
question_step = self.env["chatbot.script.step"].create(
[
{
"chatbot_script_id": chatbot_redirect_script.id,
"message": "Hello, were do you want to go?",
"step_type": "question_selection",
},
{
"chatbot_script_id": chatbot_redirect_script.id,
"message": "Tadam, we are on the page you asked for!",
"step_type": "text",
},
]
)[0]
self.env["chatbot.script.answer"].create(
[
{
"name": "Go to the portal page",
"redirect_link": f"/my/tasks/{task.id}?access_token={task.access_token}",
"script_step_id": question_step.id,
},
]
)
livechat_channel = self.env["im_livechat.channel"].create(
{
"name": "Redirection Channel",
"rule_ids": [
Command.create(
{
"regex_url": "/",
"chatbot_script_id": chatbot_redirect_script.id,
}
)
],
}
)
default_website = self.env.ref("website.default_website")
default_website.channel_id = livechat_channel.id
self.env.ref("website.default_website").channel_id = livechat_channel.id
self.start_tour("/contactus", "chatbot_redirect_to_portal")

View file

@ -0,0 +1,52 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import Command, fields
from odoo.tests.common import HttpCase, tagged
from odoo.addons.mail.tests.common import MailCommon
@tagged("post_install", "-at_install")
class TestLivechatHrHolidays(HttpCase, MailCommon):
"""Tests for bridge between im_livechat and hr_holidays modules."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env["mail.presence"]._update_presence(cls.user_employee)
leave_type = cls.env["hr.leave.type"].create(
{"name": "Legal Leaves", "requires_allocation": False, "time_type": "leave"}
)
employee = cls.env["hr.employee"].create({"user_id": cls.user_employee.id})
cls.env["hr.leave"].with_context(leave_skip_state_check=True).create(
{
"employee_id": employee.id,
"holiday_status_id": leave_type.id,
"request_date_from": fields.Datetime.today() + relativedelta(days=-2),
"request_date_to": fields.Datetime.today() + relativedelta(days=2),
"state": "validate",
}
)
def test_operator_available_on_leave(self):
"""Test operator is available on leave when they are online."""
livechat_channel = self.env["im_livechat.channel"].create(
{"name": "support", "user_ids": [Command.link(self.user_employee.id)]}
)
self.assertEqual(self.user_employee.im_status, "leave_online")
self.assertEqual(livechat_channel.available_operator_ids, self.user_employee)
def test_operator_limit_on_leave(self):
"""Test livechat limit is correctly applied when operator is on leave and online."""
livechat_channel = self.env["im_livechat.channel"].create(
{
"max_sessions_mode": "limited",
"max_sessions": 1,
"name": "support",
"user_ids": [Command.link(self.user_employee.id)],
}
)
self.make_jsonrpc_request("/im_livechat/get_session", {"channel_id": livechat_channel.id})
self.assertEqual(self.user_employee.im_status, "leave_online")
self.assertFalse(livechat_channel.available_operator_ids)

View file

@ -0,0 +1,21 @@
import odoo
from odoo.addons.im_livechat.tests.common import TestImLivechatCommon
from odoo.tests import new_test_user
@odoo.tests.tagged("-at_install", "post_install")
class TestImLivechatSessions(TestImLivechatCommon):
def test_livechat_session_open(self):
new_test_user(
self.env,
login="operator",
groups="base.group_user,im_livechat.im_livechat_group_manager",
)
self.make_jsonrpc_request(
"/im_livechat/get_session", {"channel_id": self.livechat_channel.id}
)
action = self.env.ref("im_livechat.discuss_channel_action_from_livechat_channel")
self.start_tour(
f"/odoo/livechat/{self.livechat_channel.id}/action-{action.id}", "im_livechat_session_open",
login="operator"
)

View file

@ -0,0 +1,82 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from itertools import chain
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests.common import HttpCase, tagged, warmup
@tagged("post_install", "-at_install", "is_query_count")
class TestInboxPerformance(HttpCase, MailCommon):
@warmup
def test_fetch_with_rating_stats_enabled(self):
"""
Computation of rating_stats should run a single query per model with rating_stats enabled.
"""
# Queries (in order):
# - search website (get_current_website by domain)
# - search website (get_current_website default)
# - search website_rewrite (_get_rewrites) sometimes occurs depending on the routing cache
# - insert res_device_log
# - _xmlid_lookup (_get_public_users)
# - fetch website (_get_cached_values)
# - get_param ir_config_parameter (_pre_dispatch website_sale)
# 4 _message_fetch:
# 2 _search_needaction:
# - fetch res_users (current user)
# - search ir_rule (_get_rules for mail.notification)
# - search ir_rule (_get_rules)
# - search mail_message
# 30 message _to_store:
# - search mail_message_schedule
# - fetch mail_message
# - search mail_followers
# 2 thread _to_store:
# - fetch slide_channel
# - fetch product_template
# - search mail_message_res_partner_starred_rel (_compute_starred)
# - search message_attachment_rel
# - search mail_link_preview
# - search mail_message_reaction
# - search mail_message_res_partner_rel
# - fetch mail_message_subtype
# - search mail_notification
# 7 _filtered_for_web_client:
# - fetch mail_notification
# 4 _compute_domain:
# - search ir_rule (_get_rules for res.partner)
# - search res_groups_users_rel
# - search rule_group_rel
# - fetch ir_rule
# - fetch res_company
# - fetch res_partner
# 2 _compute_rating_id:
# - search rating_rating
# - fetch rating_rating
# - search mail_tracking_value
# 3 _author_to_store:
# - fetch res_partner
# - search res_users
# - fetch res_users
# - search ir_rule (_get_rules for rating.rating)
# - read group rating_rating (_rating_get_stats_per_record for slide.channel)
# - read group rating_rating (_compute_rating_stats for slide.channel)
# - read group rating_rating (_rating_get_stats_per_record for product.template)
# - read group rating_rating (_compute_rating_stats for product.template)
# - get_param ir_config_parameter (_save_session)
first_model_records = self.env["product.template"].create(
[{"name": "Product A1"}, {"name": "Product A2"}]
)
second_model_records = self.env["slide.channel"].create(
[{"name": "Course B1"}, {"name": "Course B2"}]
)
for record in chain(first_model_records, second_model_records):
record.message_post(
body=f"<p>Test message for {record.name}</p>",
message_type="comment",
partner_ids=[self.user_employee.partner_id.id],
rating_value="4",
)
self.authenticate(self.user_employee.login, self.user_employee.password)
with self.assertQueryCount(43):
self.make_jsonrpc_request("/mail/inbox/messages")

View file

@ -0,0 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.mail.tools.discuss import Store
class TestResPartner(MailCommon):
def test_portal_user_store_data_access(self):
portal_user = mail_new_test_user(self.env, login="portal-user", groups="base.group_portal")
Store().add(portal_user.partner_id.with_user(self.user_employee_c2))

View file

@ -14,7 +14,6 @@ pip install odoo-bringout-oca-ocb-test_event_full
## Dependencies
This addon depends on:
- event
- event_booth
- event_crm
@ -23,43 +22,18 @@ This addon depends on:
- event_sms
- payment_demo
- website_event_booth_sale_exhibitor
- website_event_crm_questions
- website_event_exhibitor
- website_event_questions
- website_event_meet
- website_event_sale
- website_event_track
- website_event_track_live
- website_event_track_quiz
## Manifest Information
- **Name**: Test Full Event Flow
- **Version**: 1.0
- **Category**: Hidden/Tests
- **License**: LGPL-3
- **Installable**: False
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_event_full`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/test_event_full
## 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,27 +1,26 @@
[project]
name = "odoo-bringout-oca-ocb-test_event_full"
version = "16.0.0"
description = "Test Full Event Flow - Odoo addon"
description = "Test Full Event Flow -
Odoo addon
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-event>=16.0.0",
"odoo-bringout-oca-ocb-event_booth>=16.0.0",
"odoo-bringout-oca-ocb-event_crm>=16.0.0",
"odoo-bringout-oca-ocb-event_crm_sale>=16.0.0",
"odoo-bringout-oca-ocb-event_sale>=16.0.0",
"odoo-bringout-oca-ocb-event_sms>=16.0.0",
"odoo-bringout-oca-ocb-payment_demo>=16.0.0",
"odoo-bringout-oca-ocb-website_event_booth_sale_exhibitor>=16.0.0",
"odoo-bringout-oca-ocb-website_event_crm_questions>=16.0.0",
"odoo-bringout-oca-ocb-website_event_exhibitor>=16.0.0",
"odoo-bringout-oca-ocb-website_event_questions>=16.0.0",
"odoo-bringout-oca-ocb-website_event_meet>=16.0.0",
"odoo-bringout-oca-ocb-website_event_sale>=16.0.0",
"odoo-bringout-oca-ocb-website_event_track>=16.0.0",
"odoo-bringout-oca-ocb-website_event_track_live>=16.0.0",
"odoo-bringout-oca-ocb-website_event_track_quiz>=16.0.0",
"odoo-bringout-oca-ocb-event>=19.0.0",
"odoo-bringout-oca-ocb-event_booth>=19.0.0",
"odoo-bringout-oca-ocb-event_crm>=19.0.0",
"odoo-bringout-oca-ocb-event_crm_sale>=19.0.0",
"odoo-bringout-oca-ocb-event_sale>=19.0.0",
"odoo-bringout-oca-ocb-event_sms>=19.0.0",
"TODO_MAP-payment_demo>=19.0.0",
"odoo-bringout-oca-ocb-website_event_booth_sale_exhibitor>=19.0.0",
"odoo-bringout-oca-ocb-website_event_exhibitor>=19.0.0",
"odoo-bringout-oca-ocb-website_event_sale>=19.0.0",
"odoo-bringout-oca-ocb-website_event_track>=19.0.0",
"odoo-bringout-oca-ocb-website_event_track_live>=19.0.0",
"odoo-bringout-oca-ocb-website_event_track_quiz>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -31,7 +30,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

@ -19,10 +19,7 @@ automatic lead generation, full Online support, ...
'event_sms',
'payment_demo',
'website_event_booth_sale_exhibitor',
'website_event_crm_questions',
'website_event_exhibitor',
'website_event_questions',
'website_event_meet',
'website_event_sale',
'website_event_track',
'website_event_track_live',
@ -35,8 +32,12 @@ automatic lead generation, full Online support, ...
],
'assets': {
'web.assets_tests': [
'test_event_full/static/**/*',
'test_event_full/static/src/js/tours/*',
],
'web.assets_unit_tests': [
'test_event_full/static/src/js/tests/*',
],
},
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -1,122 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="0">
<record id="event_booth_category_data_1" model="event.booth.category">
<field name="description" type="html"><p>Standard</p></field>
<field name="name">Standard</field>
<field name="product_id" ref="event_booth_sale.product_product_event_booth"/>
</record>
<record id="event_booth_category_data_2" model="event.booth.category">
<field name="description" type="html"><p>Premium</p></field>
<field name="name">Premium</field>
<field name="product_id" ref="event_booth_sale.product_product_event_booth"/>
<field name="price">90</field>
</record>
<record id="event_type_data_full" model="event.type">
<field name="auto_confirm" eval="True"/>
<field name="default_timezone">Europe/Paris</field>
<field name="event_type_booth_ids" eval="[
(5, 0),
(0, 0, {'booth_category_id': ref('test_event_full.event_booth_category_data_1'),
'name': 'Standard Booth',
}
),
(0, 0, {'booth_category_id': ref('test_event_full.event_booth_category_data_1'),
'name': 'Standard Booth 2',
}
),
(0, 0, {'booth_category_id': ref('test_event_full.event_booth_category_data_2'),
'name': 'Premium Booth',
}
),
(0, 0, {'booth_category_id': ref('test_event_full.event_booth_category_data_2'),
'name': 'Premium Booth 2',
}
)]"/>
<field name="event_type_mail_ids" eval="[
(5, 0),
(0, 0, {'interval_unit': 'now',
'interval_type': 'after_sub',
'notification_type': 'mail',
'template_ref': 'mail.template,%i' % ref('event.event_subscription'),
}
),
(0, 0, {'interval_nbr': 1,
'interval_unit': 'days',
'interval_type': 'before_event',
'notification_type': 'mail',
'template_ref': 'mail.template,%i' % ref('event.event_reminder'),
}
),
(0, 0, {'interval_nbr': 1,
'interval_unit': 'days',
'interval_type': 'after_event',
'notification_type': 'sms',
'template_ref': 'sms.template,%i' % ref('event_sms.sms_template_data_event_reminder'),
}
)]"/>
<field name="event_type_ticket_ids" eval="[
(5, 0),
(0, 0, {'description': 'Ticket1 Description',
'name': 'Ticket1',
'product_id': ref('event_sale.product_product_event'),
'seats_max': 10,
}
),
(0, 0, {'description': 'Ticket2 Description',
'name': 'Ticket2',
'product_id': ref('event_sale.product_product_event'),
'price': 45,
}
)]"/>
<field name="has_seats_limitation" eval="True"/>
<field name="name">Test Type</field>
<field name="note" type="html"><p>Template note</p></field>
<field name="question_ids" eval="[(5, 0)]"/>
<field name="seats_max">30</field>
<field name="tag_ids" eval="[(5, 0)]"/>
<field name="ticket_instructions" type="html"><p>Ticket Instructions</p></field>
<field name="website_menu" eval="True"/>
</record>
<record id="event_question_type_full_1" model="event.question">
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="False"/>
<field name="event_type_id" ref="test_event_full.event_type_data_full"/>
<field name="title">Question1</field>
</record>
<record id="event_question_type_full_1_answer_1" model="event.question.answer">
<field name="name">Q1-Answer1</field>
<field name="sequence">1</field>
<field name="question_id" ref="test_event_full.event_question_type_full_1"/>
</record>
<record id="event_question_type_full_1_answer_2" model="event.question.answer">
<field name="name">Q1-Answer2</field>
<field name="sequence">2</field>
<field name="question_id" ref="test_event_full.event_question_type_full_1"/>
</record>
<record id="event_question_type_full_2" model="event.question">
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="False"/>
<field name="event_type_id" ref="test_event_full.event_type_data_full"/>
<field name="title">Question2</field>
</record>
<record id="event_question_type_full_2_answer_1" model="event.question.answer">
<field name="name">Q2-Answer1</field>
<field name="sequence">1</field>
<field name="question_id" ref="test_event_full.event_question_type_full_2"/>
</record>
<record id="event_question_type_full_2_answer_2" model="event.question.answer">
<field name="name">Q2-Answer2</field>
<field name="sequence">2</field>
<field name="question_id" ref="test_event_full.event_question_type_full_2"/>
</record>
<record id="event_question_type_full_3" model="event.question">
<field name="question_type">text_box</field>
<field name="once_per_order" eval="True"/>
<field name="event_type_id" ref="test_event_full.event_type_data_full"/>
<field name="title">Question3</field>
</record>
</data></odoo>

View file

@ -0,0 +1,124 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { click, select } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
class EventMail extends models.Model {
_name = "event.mail";
template_ref = fields.Reference({
selection: [
["mail.template", "Mail Template"],
["sms.template", "SMS Template"],
["some.template", "Some Template"],
],
});
_records = [
{
id: 1,
template_ref: "mail.template,1",
},
{
id: 2,
template_ref: "sms.template,1",
},
{
id: 3,
template_ref: "some.template,1",
},
];
}
class MailTemplate extends models.Model {
_name = "mail.template";
name = fields.Char();
_records = [{ id: 1, name: "Mail Template 1" }];
}
class SmsTemplate extends models.Model {
_name = "sms.template";
name = fields.Char();
_records = [{ id: 1, name: "SMS template 1" }];
}
class SomeTemplate extends models.Model {
_name = "some.template";
name = fields.Char();
_records = [{ id: 1, name: "Some Template 1" }];
}
defineMailModels();
defineModels([EventMail, MailTemplate, SmsTemplate, SomeTemplate]);
describe.current.tags("desktop");
test("Reference field displays right icons", async () => {
// bypass list controller check
onRpc("has_group", () => true);
await mountView({
type: "list",
resModel: "event.mail",
arch: `
<list editable="top">
<field name="template_ref" widget="EventMailTemplateReferenceField"/>
</list>`,
});
// each field cell will be the field of a different record (1 field/line)
expect(".o_field_cell").toHaveCount(3);
expect(".o_field_cell.o_EventMailTemplateReferenceField_cell").toHaveCount(3);
expect(".o_field_cell:eq(0) .fa-envelope").toHaveCount(1);
expect(".o_field_cell:eq(1) .fa-mobile").toHaveCount(1);
expect(".o_field_cell:eq(2) .fa-envelope").toHaveCount(0);
expect(".o_field_cell:eq(2) .fa-mobile").toHaveCount(0);
// select a sms.template instead of mail.template
await click(".o_field_cell:eq(0)");
await animationFrame();
await click(".o_field_cell:eq(0) select.o_input");
await select("sms.template");
await animationFrame();
await click(".o_field_cell:eq(0) .o_field_many2one_selection input");
await animationFrame();
await click(".o_field_cell:eq(0) .o-autocomplete--dropdown-item");
// click out
await click(".o_list_renderer");
await animationFrame();
expect(".o_field_cell:eq(0) .fa-mobile").toHaveCount(1);
expect(".o_field_cell:eq(0) .fa-envelope").toHaveCount(0);
// select a some other model to check it has no icon
await click(".o_field_cell:eq(0)");
await animationFrame();
await click(".o_field_cell:eq(0) select.o_input");
await select("some.template");
await animationFrame();
await click(".o_field_cell:eq(0) .o_field_many2one_selection input");
await animationFrame();
await click(".o_field_cell:eq(0) .o-autocomplete--dropdown-item");
await click(".o_list_renderer");
await animationFrame();
expect(".o_field_cell:eq(0) .fa-mobile").toHaveCount(0);
expect(".o_field_cell:eq(0) .fa-envelope").toHaveCount(0);
// select no record for the model
await click(".o_field_cell:eq(1)");
await animationFrame();
await click(".o_field_cell:eq(1) select.o_input");
await select("mail.template");
await click(".o_list_renderer");
await animationFrame();
expect(".o_field_cell:eq(1) .fa-mobile").toHaveCount(0);
expect(".o_field_cell:eq(1) .fa-envelope").toHaveCount(0);
});

View file

@ -1,85 +1,76 @@
odoo.define('test_event_full.tour.performance', function (require) {
"use strict";
var tour = require('web_tour.tour');
import { registry } from "@web/core/registry";
import * as wsTourUtils from '@website_sale/js/tours/tour_utils';
var registerSteps = [{
content: "Select 2 units of 'Ticket1' ticket type",
trigger: '#o_wevent_tickets_collapse .row.o_wevent_ticket_selector[name="Ticket1"] select',
run: 'text 2',
content: "Open ticket modal",
trigger: 'button.btn-primary:contains("Register")',
run: "click",
}, {
content: "Select 1 unit of 'Ticket2' ticket type",
trigger: '#o_wevent_tickets_collapse .row.o_wevent_ticket_selector[name="Ticket2"] select',
run: 'text 1',
content: "Add 2 units of 'Ticket1' ticket type by clicking the '+' button",
trigger: 'button[data-increment-type*="plus"]',
run: "dblclick",
}, {
content: "Edit 1 unit of 'Ticket2' ticket type",
trigger: '.modal input:eq(2)',
run: "edit 1",
}, {
content: "Click on 'Register' button",
trigger: '#o_wevent_tickets .btn-primary:contains("Register"):not(:disabled)',
run: 'click',
}, {
content: "Fill attendees details",
trigger: 'form[id="attendee_registration"] .btn:contains("Continue")',
run: function () {
$("input[name='1-name']").val("Raoulette Poiluchette");
$("input[name='1-phone']").val("0456112233");
$("input[name='1-email']").val("raoulette@example.com");
$("div[name*='Question1'] select[name*='question_answer-1']").val($("select[name*='question_answer-1'] option:contains('Q1-Answer2')").val());
$("div[name*='Question2'] select[name*='question_answer-1']").val($("select[name*='question_answer-1'] option:contains('Q2-Answer1')").val());
$("input[name='2-name']").val("Michel Tractopelle");
$("input[name='2-phone']").val("0456332211");
$("input[name='2-email']").val("michel@example.com");
$("div[name*='Question1'] select[name*='question_answer-2']").val($("select[name*='question_answer-2'] option:contains('Q1-Answer1')").val());
$("div[name*='Question2'] select[name*='question_answer-2']").val($("select[name*='question_answer-2'] option:contains('Q2-Answer2')").val());
$("input[name='3-name']").val("Hubert Boitaclous");
$("input[name='3-phone']").val("0456995511");
$("input[name='3-email']").val("hubert@example.com");
$("div[name*='Question1'] select[name*='question_answer-3']").val($("select[name*='question_answer-3'] option:contains('Q1-Answer2')").val());
$("div[name*='Question2'] select[name*='question_answer-3']").val($("select[name*='question_answer-3'] option:contains('Q2-Answer2')").val());
$("textarea[name*='question_answer']").text("Random answer from random guy");
},
content: "Choose the 'Q1-Answer2' answer of the 'Question1' for the first ticket",
trigger: 'select[name*="1-simple_choice"]',
run: "selectByIndex 2",
}, {
content: "Choose the 'Q2-Answer1' answer of the 'Question2' for the first ticket",
trigger: 'select[name*="1-simple_choice"]:last',
run: "selectByIndex 1",
}, {
content: "Choose the 'Q1-Answer1' answer of the 'Question1' for the second ticket",
trigger: 'select[name*="2-simple_choice"]',
run: "selectByIndex 1",
}, {
content: "Choose the 'Q2-Answer2' answer of the 'Question2' for the second ticket",
trigger: 'select[name*="2-simple_choice"]:last',
run: "selectByIndex 2",
}, {
content: "Choose the 'Q1-Answer2' answer of the 'Question1' for the third ticket",
trigger: 'select[name*="3-simple_choice"]',
run: "selectByIndex 2",
}, {
content: "Choose the 'Q2-Answer2' answer of the 'Question2' for the third ticket",
trigger: 'select[name*="3-simple_choice"]:last',
run: "selectByIndex 2",
}, {
content: "Fill the text content of the 'Question3' for the third ticket",
trigger: 'textarea[name*="text_box"]',
run: "edit Random answer from random guy",
}, {
content: "Validate attendees details",
extra_trigger: "input[name='1-name'], input[name='2-name'], input[name='3-name']",
trigger: 'button:contains("Continue")',
trigger: 'button[type=submit]:last',
run: 'click',
}, {
content: "Address filling",
trigger: 'select[name="country_id"]',
run: function () {
$('input[name="name"]').val('Raoulette Poiluchette');
$('input[name="phone"]').val('0456112233');
$('input[name="email"]').val('raoulette@example.com');
$('input[name="street"]').val('Cheesy Crust Street, 42');
$('input[name="city"]').val('CheeseCity');
$('input[name="zip"]').val('8888');
$('#country_id option:eq(1)').attr('selected', true);
},
}, {
content: "Next",
trigger: '.oe_cart .btn:contains("Next")',
}, {
content: 'Select Test payment provider',
trigger: '.o_payment_option_card:contains("Demo")'
}, {
content: 'Add card number',
trigger: 'input[name="customer_input"]',
run: 'text 4242424242424242'
}, {
content: "Pay now",
extra_trigger: "#cart_products:contains(Ticket1):contains(Ticket2)",
trigger: 'button:contains(Pay Now)',
run: 'click',
}, {
content: 'Payment is successful',
trigger: '.oe_website_sale_tx_status:contains("Your payment has been successfully processed.")',
run: function () {}
}];
expectUnloadPage: true,
},
...wsTourUtils.fillAdressForm({
name: "Raoulette Poiluchette",
phone: "0456112233",
email: "raoulette@example.com",
street: "Cheesy Crust Street, 42",
city: "CheeseCity",
zip: "8888",
}),
{
content: "Confirm address",
trigger: 'a[name="website_sale_main_button"]',
run: "click",
expectUnloadPage: true,
},
...wsTourUtils.payWithDemo(),
];
tour.register('wevent_performance_register', {
test: true
}, [].concat(
registry.category("web_tour.tours").add('wevent_performance_register', {
steps: () => [].concat(
registerSteps,
)
);
});

View file

@ -1,18 +1,57 @@
odoo.define('test_event_full.tour.register', function (require) {
"use strict";
var tour = require('web_tour.tour');
import { registry } from "@web/core/registry";
import { session } from "@web/session";
/**
* TALKS STEPS
*/
const reminderToggleSteps = function (talkName, reminderOn, toggleReminder) {
let steps = [];
if (reminderOn) {
steps = steps.concat([{
content: `Check Favorite for ${talkName} was already on`,
trigger: "div.o_wetrack_js_reminder i.fa-bell",
}]);
}
else {
steps = steps.concat([{
content: `Check Favorite for ${talkName} was off`,
trigger: "div.o_wetrack_js_reminder i.fa-bell-o",
}]);
if (toggleReminder) {
steps = steps.concat([{
content: "Set Favorite",
trigger: "i[title='Set Favorite']",
run: "click",
}]);
if (session.is_public){
steps = steps.concat([{
content: "The form of the email reminder modal is filled",
trigger: "#o_wetrack_email_reminder_form input[name='email']",
run: "fill visitor@odoo.com",
},
{
content: "The form is submit",
trigger: "#o_wetrack_email_reminder_form button[type='submit']",
run: "click",
}]);
}
steps = steps.concat([{
content: `Check Favorite for ${talkName} is now on`,
trigger: "div.o_wetrack_js_reminder i.fa-bell",
}]);
}
}
return steps;
};
var discoverTalkSteps = function (talkName, fromList, reminderOn, toggleReminder) {
const discoverTalkSteps = function (talkName, fromList, checkToggleReminder, reminderOn, toggleReminder) {
var steps;
if (fromList) {
steps = [{
content: 'Go on "' + talkName + '" talk in List',
trigger: 'a:contains("' + talkName + '")',
run: "click",
expectUnloadPage: true,
}];
}
else {
@ -20,109 +59,98 @@ var discoverTalkSteps = function (talkName, fromList, reminderOn, toggleReminder
content: 'Click on Live Track',
trigger: 'article span:contains("' + talkName + '")',
run: 'click',
expectUnloadPage: true,
}];
}
steps = steps.concat([{
content: `Check we are on the "${talkName}" talk page`,
trigger: 'div.o_wesession_track_main',
run: function () {}, // it's a check
}]);
if (reminderOn) {
steps = steps.concat([{
content: `Check Favorite for ${talkName} was already on`,
trigger: 'div.o_wetrack_js_reminder i.fa-bell',
extra_trigger: 'span.o_wetrack_js_reminder_text:contains("Favorite On")',
run: function () {}, // it's a check
}]);
}
else {
steps = steps.concat([{
content: `Check Favorite for ${talkName} was off`,
trigger: 'span.o_wetrack_js_reminder_text:contains("Set Favorite")',
run: function () {}, // it's a check
}]);
if (toggleReminder) {
steps = steps.concat([{
content: "Set Favorite",
trigger: 'span.o_wetrack_js_reminder_text',
run: 'click',
}, {
content: `Check Favorite for ${talkName} is now on`,
trigger: 'div.o_wetrack_js_reminder i.fa-bell',
extra_trigger: 'span.o_wetrack_js_reminder_text:contains("Favorite On")',
run: function () {}, // it's a check
}]);
}
if (checkToggleReminder){
steps = steps.concat(reminderToggleSteps(talkName, reminderOn, toggleReminder));
}
return steps;
};
/**
* ROOMS STEPS
*/
var discoverRoomSteps = function (roomName) {
var steps = [{
content: 'Go on "' + roomName + '" room in List',
trigger: 'a.o_wevent_meeting_room_card h4:contains("' + roomName + '")',
run: function() {
// can't click on it, it will try to launch Jitsi and fail on chrome headless
},
}];
return steps;
};
/**
* REGISTER STEPS
*/
var registerSteps = [{
content: 'Go on Register',
trigger: 'a.btn-primary:contains("Register")',
}, {
content: "Select 2 units of 'Standard' ticket type",
trigger: '#o_wevent_tickets_collapse .row:has(.o_wevent_registration_multi_select:contains("Free")) select',
run: 'text 2',
}, {
content: "Click on 'Register' button",
trigger: '#o_wevent_tickets .btn-primary:contains("Register"):not(:disabled)',
run: 'click',
}, {
content: "Fill attendees details",
trigger: 'form[id="attendee_registration"] .btn:contains("Continue")',
run: function () {
$("input[name='1-name']").val("Raoulette Poiluchette");
$("input[name='1-phone']").val("0456112233");
$("input[name='1-email']").val("raoulette@example.com");
$("select[name*='question_answer-1']").val($("select[name*='question_answer-1'] option:contains('Consumers')").val());
$("input[name='2-name']").val("Michel Tractopelle");
$("input[name='2-phone']").val("0456332211");
$("input[name='2-email']").val("michel@example.com");
$("select[name*='question_answer-2']").val($("select[name*='question_answer-1'] option:contains('Research')").val());
$("textarea[name*='question_answer']").text("An unicorn told me about you. I ate it afterwards.");
const registerSteps = [
{
content: "Open ticket modal",
trigger: "button.btn-primary:contains(Register):enabled",
run: "click",
},
}, {
content: "Validate attendees details",
extra_trigger: "input[name='1-name'], input[name='2-name'], input[name='3-name']",
trigger: 'button:contains("Continue")',
run: 'click',
}, {
trigger: 'div.o_wereg_confirmed_attendees span:contains("Raoulette Poiluchette")',
run: function () {} // check
}, {
trigger: 'div.o_wereg_confirmed_attendees span:contains("Michel Tractopelle")',
run: function () {} // check
}, {
content: "Click on 'register favorites talks' button",
trigger: 'a:contains("register to your favorites talks now")',
run: 'click',
}, {
trigger: 'h1:contains("Book your talks")',
run: function() {},
}];
{
content: "Edit 2 units of 'Standard' ticket type",
trigger: ".modal .o_wevent_ticket_selector input",
run: "edit 2",
},
{
content: "Click on 'Register' button",
trigger: ".modal #o_wevent_tickets .btn-primary:contains(Register):enabled",
run: "click",
},
{
content: "Wait the modal is shown before continue",
trigger: ".modal.modal_shown.show form[id=attendee_registration]",
},
{
trigger: ".modal input[name*='1-name']",
run: "edit Raoulette Poiluchette",
},
{
trigger: ".modal input[name*='1-phone']",
run: "edit 0456112233",
},
{
trigger: ".modal input[name*='1-email']",
run: "edit raoulette@example.com",
},
{
trigger: ".modal select[name*='1-simple_choice']",
run: "selectByLabel Consumers",
},
{
trigger: ".modal input[name*='2-name']",
run: "edit Michel Tractopelle",
},
{
trigger: ".modal input[name*='2-phone']",
run: "edit 0456332211",
},
{
trigger: ".modal input[name*='2-email']",
run: "edit michel@example.com",
},
{
trigger: ".modal select[name*='2-simple_choice']",
run: "selectByLabel Research",
},
{
trigger: ".modal textarea[name*='text_box']",
run: "edit An unicorn told me about you. I ate it afterwards.",
},
{
trigger: ".modal input[name*='1-name'], input[name*='2-name'], input[name*='3-name']",
},
{
content: "Validate attendees details",
trigger: ".modal button[type=submit]:enabled",
run: "click",
expectUnloadPage: true,
},
{
content: "Click on 'register favorites talks' button",
trigger: "a:contains(register to your favorites talks now)",
run: "click",
expectUnloadPage: true,
},
{
trigger: "h5:contains(Book your talks)",
},
];
/**
* MAIN STEPS
@ -132,43 +160,46 @@ var initTourSteps = function (eventName) {
return [{
content: 'Go on "' + eventName + '" page',
trigger: 'a[href*="/event"]:contains("' + eventName + '"):first',
run: "click",
expectUnloadPage: true,
}];
};
var browseTalksSteps = [{
content: 'Browse Talks',
trigger: 'a:contains("Talks")',
content: 'Browse Talks Menu',
trigger: 'a[href*="#"]:contains("Talks")',
run: "click",
}, {
content: 'Browse Talks Submenu',
trigger: 'a.dropdown-item span:contains("Talks")',
run: "click",
expectUnloadPage: true,
}, {
content: 'Check we are on the talk list page',
trigger: 'h1:contains("Book your talks")',
run: function () {} // check
trigger: 'h5:contains("Book your talks")',
}];
var browseMeetSteps = [{
content: 'Browse Meet',
trigger: 'a:contains("Community")',
var browseBackSteps = [{
content: 'Browse Back',
trigger: 'a:contains("All Talks")',
run: "click",
expectUnloadPage: true,
}, {
content: 'Check we are on the community page',
trigger: 'span:contains("Join a room")',
run: function () {} // check
content: 'Check we are back on the talk list page',
trigger: 'h5:contains("Book your talks")',
}];
tour.register('wevent_register', {
registry.category("web_tour.tours").add('wevent_register', {
url: '/event',
test: true
}, [].concat(
steps: () => [].concat(
initTourSteps('Online Reveal'),
browseTalksSteps,
discoverTalkSteps('What This Event Is All About', true, true),
browseTalksSteps,
discoverTalkSteps('Live Testimonial', false, false, false),
browseTalksSteps,
discoverTalkSteps('Our Last Day Together !', true, false, true),
browseMeetSteps,
discoverRoomSteps('Best wood for furniture'),
discoverTalkSteps('What This Event Is All About', true, true, true),
browseBackSteps,
discoverTalkSteps('Live Testimonial', false, false, false, false),
browseBackSteps,
discoverTalkSteps('Our Last Day Together!', true, true, false, true),
browseBackSteps,
registerSteps,
)
);
});

View file

@ -7,5 +7,5 @@ from . import test_event_event
from . import test_event_mail
from . import test_event_security
from . import test_performance
from . import test_wevent_menu
from . import test_wevent_register
from . import test_event_discount

View file

@ -3,10 +3,14 @@
from datetime import datetime, timedelta, time
from odoo import Command
from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
from odoo.addons.event.tests.common import EventCase
from odoo.addons.event_crm.tests.common import EventCrmCase
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common import mail_new_test_user, MailCase
from odoo.addons.sales_team.tests.common import TestSalesCommon
from odoo.addons.sms.tests.common import SMSCase
from odoo.addons.website.tests.test_website_visitor import MockVisitor
@ -15,7 +19,6 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
@classmethod
def setUpClass(cls):
super(TestEventFullCommon, cls).setUpClass()
cls._init_mail_gateway()
# Context data: dates
# ------------------------------------------------------------
@ -37,6 +40,7 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
# set country in order to format Belgian numbers
cls.company_admin.write({
'country_id': cls.env.ref('base.be').id,
'email': 'info@yourcompany.com',
})
cls.event_user = mail_new_test_user(
cls.env,
@ -55,7 +59,6 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
'country_id': cls.env.ref('base.be').id,
'email': 'customer.test@example.com',
'name': 'Test Customer',
'mobile': '0456123456',
'phone': '0456123456',
})
# make a SO for a customer, selling some tickets
@ -68,14 +71,16 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
cls.ticket_product = cls.env['product.product'].create({
'description_sale': 'Ticket Product Description',
'detailed_type': 'event',
'type': 'service',
'service_tracking': 'event',
'list_price': 10,
'name': 'Test Registration Product',
'standard_price': 30.0,
})
cls.booth_product = cls.env['product.product'].create({
'description_sale': 'Booth Product Description',
'detailed_type': 'event_booth',
'type': 'service',
'service_tracking': 'event_booth',
'list_price': 20,
'name': 'Test Booth Product',
'standard_price': 60.0,
@ -120,9 +125,8 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
# ------------------------------------------------------------
test_registration_report = cls.env.ref('test_event_full.event_registration_report_test')
subscription_template = cls.env.ref('event.event_subscription')
subscription_template.write({'report_template': test_registration_report.id})
subscription_template.write({'report_template_ids': [(6, 0, test_registration_report.ids)]})
cls.test_event_type = cls.env['event.type'].create({
'auto_confirm': True,
'default_timezone': 'Europe/Paris',
'event_type_booth_ids': [
(0, 0, {'booth_category_id': cls.event_booth_categories[0].id,
@ -145,21 +149,18 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
'event_type_mail_ids': [
(0, 0, {'interval_unit': 'now', # right at subscription
'interval_type': 'after_sub',
'notification_type': 'mail',
'template_ref': 'mail.template,%i' % subscription_template.id,
}
),
(0, 0, {'interval_nbr': 1, # 1 days before event
'interval_unit': 'days',
'interval_type': 'before_event',
'notification_type': 'mail',
'template_ref': 'mail.template,%i' % cls.env['ir.model.data']._xmlid_to_res_id('event.event_reminder'),
}
),
(0, 0, {'interval_nbr': 1, # 1 days after event
'interval_unit': 'days',
'interval_type': 'after_event',
'notification_type': 'sms',
'template_ref': 'sms.template,%i' % cls.env['ir.model.data']._xmlid_to_res_id('event_sms.sms_template_data_event_reminder'),
}
),
@ -228,15 +229,15 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
'is_published': True,
}
cls.test_event = cls.env['event.event'].create({
'name': 'Test Event',
'auto_confirm': True,
'date_begin': datetime.now() + timedelta(days=1),
'date_end': datetime.now() + timedelta(days=5),
'date_tz': 'Europe/Brussels',
'event_type_id': cls.test_event_type.id,
'is_published': True,
})
with cls.mock_datetime_and_now(cls, cls.reference_now):
cls.test_event = cls.env['event.event'].create({
'name': 'Test Event',
'date_begin': datetime.now() + timedelta(days=1),
'date_end': datetime.now() + timedelta(days=5),
'date_tz': 'Europe/Brussels',
'event_type_id': cls.test_event_type.id,
'is_published': True,
})
# update post-synchronize data
ticket_1 = cls.test_event.event_ticket_ids.filtered(lambda t: t.name == 'Ticket1')
ticket_2 = cls.test_event.event_ticket_ids.filtered(lambda t: t.name == 'Ticket2')
@ -251,39 +252,36 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
], limit=1)
cls.customer_data = [
{'email': 'customer.email.%02d@test.example.com' % x,
'name': 'My Customer %02d' % x,
'mobile': '04569999%02d' % x,
{'email': f'customer.email.{idx:02d}@test.example.com',
'name': f'My Customer {idx:02d}',
'partner_id': False,
'phone': '04560000%02d' % x,
} for x in range(0, 10)
'phone': f'04560000{idx:02d}',
} for idx in range(0, 10)
]
cls.website_customer_data = [
{'email': 'website.email.%02d@test.example.com' % x,
'name': 'My Customer %02d' % x,
'mobile': '04569999%02d' % x,
{'email': f'website.email.{idx:02d}@test.example.com',
'name': f'My Customer {idx:02d}',
'partner_id': cls.env.ref('base.public_partner').id,
'phone': '04560000%02d' % x,
'phone': f'04560000{idx:02d}',
'registration_answer_ids': [
(0, 0, {
'question_id': cls.test_event.question_ids[0].id,
'value_answer_id': cls.test_event.question_ids[0].answer_ids[(x % 2)].id,
'value_answer_id': cls.test_event.question_ids[0].answer_ids[(idx % 2)].id,
}), (0, 0, {
'question_id': cls.test_event.question_ids[1].id,
'value_answer_id': cls.test_event.question_ids[1].answer_ids[(x % 2)].id,
'value_answer_id': cls.test_event.question_ids[1].answer_ids[(idx % 2)].id,
}), (0, 0, {
'question_id': cls.test_event.question_ids[2].id,
'value_text_box': 'CustomerAnswer%s' % x,
'value_text_box': f'CustomerAnswer{idx}',
})
],
} for x in range(0, 10)
} for idx in range(0, 10)
]
cls.partners = cls.env['res.partner'].create([
{'email': 'partner.email.%02d@test.example.com' % x,
'name': 'PartnerCustomer',
'mobile': '04569999%02d' % x,
'phone': '04560000%02d' % x,
} for x in range(0, 10)
{'email': f'partner.email.{idx:02d}@test.example.com',
'name': f'PartnerCustomer {idx:02d}',
'phone': f'04560000{idx:02d}',
} for idx in range(0, 10)
])
def assertLeadConvertion(self, rule, registrations, partner=None, **expected):
@ -304,6 +302,98 @@ class TestEventFullCommon(EventCrmCase, TestSalesCommon, MockVisitor):
self.assertIn(answer.value_text_box, lead.description) # better: check multi line
class TestEventMailCommon(EventCase, SMSCase, MailCase, CronMixinCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.event_cron_id = cls.env.ref('event.event_mail_scheduler')
# deactivate other schedulers to avoid messing with crons
cls.env['event.mail'].search([]).unlink()
# consider asynchronous sending as default sending
cls.env["ir.config_parameter"].set_param("event.event_mail_async", False)
cls.env.company.write({
'email': 'info@yourcompany.example.com',
'name': 'YourCompany',
})
# prepare SMS templates
cls.sms_template_sub = cls.env['sms.template'].create({
'name': 'Test SMS Subscription',
'model_id': cls.env.ref('event.model_event_registration').id,
'body': '{{ object.event_id.organizer_id.name }} registration confirmation.',
'lang': '{{ object.partner_id.lang }}'
})
cls.sms_template_rem = cls.env['sms.template'].create({
'name': 'Test SMS Reminder',
'model_id': cls.env.ref('event.model_event_registration').id,
'body': '{{ object.event_id.organizer_id.name }} reminder',
'lang': '{{ object.partner_id.lang }}'
})
# freeze some datetimes, and ensure more than 1D+1H before event starts
# to ease time-based scheduler check
# Since `now` is used to set the `create_date` of an event and create_date
# has often microseconds, we set it to ensure that the scheduler we still be
# launched if scheduled_date == create_date - microseconds
cls.reference_now = datetime(2021, 3, 20, 14, 30, 15, 123456)
cls.event_date_begin = datetime(2021, 3, 25, 8, 0, 0)
cls.event_date_end = datetime(2021, 3, 28, 18, 0, 0)
cls._setup_test_reports()
with cls.mock_datetime_and_now(cls, cls.reference_now):
cls.test_event = cls.env['event.event'].create({
'name': 'TestEventMail',
'user_id': cls.user_eventmanager.id,
'date_begin': cls.event_date_begin,
'date_end': cls.event_date_end,
'event_mail_ids': [
(0, 0, { # right at subscription: mail
'interval_unit': 'now',
'interval_type': 'after_sub',
'notification_type': 'mail',
'template_ref': f'mail.template,{cls.template_subscription.id}',
}),
(0, 0, { # right at subscription: sms
'interval_unit': 'now',
'interval_type': 'after_sub',
'notification_type': 'sms',
'template_ref': f'sms.template,{cls.sms_template_sub.id}',
}),
(0, 0, { # 3 days before event: mail
'interval_nbr': 3,
'interval_unit': 'days',
'interval_type': 'before_event',
'notification_type': 'mail',
'template_ref': f'mail.template,{cls.template_reminder.id}',
}),
(0, 0, { # 3 days before event: SMS
'interval_nbr': 3,
'interval_unit': 'days',
'interval_type': 'before_event',
'notification_type': 'sms',
'template_ref': f'sms.template,{cls.sms_template_rem.id}',
}),
(0, 0, { # 1h after event: mail
'interval_nbr': 1,
'interval_unit': 'hours',
'interval_type': 'after_event',
'notification_type': 'mail',
'template_ref': f'mail.template,{cls.template_reminder.id}',
}),
(0, 0, { # 1h after event: SMS
'interval_nbr': 1,
'interval_unit': 'hours',
'interval_type': 'after_event',
'notification_type': 'sms',
'template_ref': f'sms.template,{cls.sms_template_rem.id}',
}),
],
})
class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor):
def setUp(self):
@ -322,7 +412,8 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
'description_sale': 'Mighty Description',
'list_price': 10,
'standard_price': 30.0,
'detailed_type': 'event',
'type': 'service',
'service_tracking': 'event',
})
self.event_tag_category_1 = self.env['event.tag.category'].create({
@ -336,13 +427,12 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
'color': 8,
})
self.env['event.event'].search(
[('name', 'like', '%Online Reveal%')]
[('name', 'like', 'Online Reveal')]
).write(
{'name': 'Do not click on me'}
)
self.event = self.env['event.event'].create({
'name': 'Online Reveal TestEvent',
'auto_confirm': True,
'stage_id': self.env.ref('event.event_stage_booked').id,
'address_id': False,
'user_id': self.user_demo.id,
@ -377,7 +467,6 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
'email': 'constantin@test.example.com',
'country_id': self.env.ref('base.be').id,
'phone': '0485112233',
'mobile': False,
})
self.event_speaker = self.env['res.partner'].create({
'name': 'Brandon Freeman',
@ -392,7 +481,7 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
self.event_question_1 = self.env['event.question'].create({
'title': 'Which field are you working in',
'question_type': 'simple_choice',
'event_id': self.event.id,
'event_ids': [Command.set(self.event.ids)],
'once_per_order': False,
'answer_ids': [
(0, 0, {'name': 'Consumers'}),
@ -403,7 +492,7 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
self.event_question_2 = self.env['event.question'].create({
'title': 'How did you hear about us ?',
'question_type': 'text_box',
'event_id': self.event.id,
'event_ids': [Command.set(self.event.ids)],
'once_per_order': True,
})
@ -421,6 +510,7 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
'wishlisted_by_default': True,
'user_id': self.user_admin.id,
'partner_id': self.event_speaker.id,
'description': 'Performance of Raoul Grosbedon.'
})
self.track_1 = self.env['event.track'].create({
'name': 'Live Testimonial',
@ -431,9 +521,10 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
'is_published': True,
'user_id': self.user_admin.id,
'partner_id': self.event_speaker.id,
'description': 'Description of the live.'
})
self.track_2 = self.env['event.track'].create({
'name': 'Our Last Day Together !',
'name': 'Our Last Day Together!',
'event_id': self.event.id,
'stage_id': self.env.ref('website_event_track.event_track_stage3').id,
'date': self.reference_now + timedelta(days=1),
@ -441,22 +532,7 @@ class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, MockVisitor
'is_published': True,
'user_id': self.user_admin.id,
'partner_id': self.event_speaker.id,
})
# ------------------------------------------------------------
# MEETING ROOMS
# ----------------------------------------------------------
self.env['event.meeting.room'].create({
'name': 'Best wood for furniture',
'summary': 'Let\'s talk about wood types for furniture',
'target_audience': 'wood expert(s)',
'is_pinned': True,
'website_published': True,
'event_id': self.event.id,
'room_lang_id': self.env.ref('base.lang_en').id,
'room_max_capacity': '12',
'room_participant_count': 9,
'description': 'Description of our last day together.'
})
self.env.flush_all()

View file

@ -2,9 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_event_full.tests.common import TestEventFullCommon
from odoo.tests import users
from odoo.tests import tagged, users
@tagged("event_crm")
class TestEventCrm(TestEventFullCommon):
@classmethod

View file

@ -1,78 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo.tests import tagged
from odoo.fields import Command
from odoo.addons.test_event_full.tests.common import TestEventFullCommon
@tagged('post_install', '-at_install')
class TestEventTicketPriceRounding(TestEventFullCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ticket_product.write({
'lst_price': 1.0
})
cls.currency_jpy = cls.env['res.currency'].create({
'name': 'JPX',
'symbol': '¥',
'rounding': 1.0,
'rate_ids': [Command.create({'rate': 133.6200, 'name': time.strftime('%Y-%m-%d')})],
})
cls.currency_cad = cls.env['res.currency'].create({
'name': 'CXD',
'symbol': '$',
'rounding': 0.01,
'rate_ids': [Command.create({'rate': 1.338800, 'name': time.strftime('%Y-%m-%d')})],
})
cls.pricelist_usd = cls.env['product.pricelist'].create({
'name': 'Pricelist USD',
'currency_id': cls.env.ref('base.USD').id,
})
cls.pricelist_jpy = cls.env['product.pricelist'].create({
'name': 'Pricelist JPY',
'currency_id': cls.currency_jpy.id,
})
cls.pricelist_cad = cls.env['product.pricelist'].create({
'name': 'Pricelist CAD',
'currency_id': cls.currency_cad.id,
})
cls.event_type = cls.env['event.type'].create({
'name': 'Test Event Type',
'auto_confirm': True,
'event_type_ticket_ids': [
(0, 0, {
'name': 'Test Event Ticket',
'product_id': cls.ticket_product.id,
'price': 30.0,
})
],
})
cls.event_ticket = cls.event_type.event_type_ticket_ids[0]
def test_no_discount_usd(self):
ticket = self.event_ticket.with_context(pricelist=self.pricelist_usd.id)
ticket._compute_price_reduce()
self.assertAlmostEqual(ticket.price_reduce, 30.0, places=6, msg="No discount should be applied for the USD pricelist.")
def test_no_discount_jpy(self):
ticket = self.event_ticket.with_context(pricelist=self.pricelist_jpy.id)
ticket._compute_price_reduce()
self.assertAlmostEqual(ticket.price_reduce, 30.0, places=6, msg="No discount should be applied for the JPY pricelist.")
def test_no_discount_cad(self):
ticket = self.event_ticket.with_context(pricelist=self.pricelist_cad.id)
ticket._compute_price_reduce()
self.assertAlmostEqual(ticket.price_reduce, 30.0, places=6, msg="No discount should be applied for the CAD pricelist.")

View file

@ -36,16 +36,13 @@ class TestEventEvent(TestEventFullCommon):
# check result
self.assertEqual(event.address_id, self.env.user.company_id.partner_id)
self.assertTrue(event.auto_confirm)
self.assertEqual(event.country_id, self.env.user.company_id.country_id)
self.assertEqual(event.date_tz, 'Europe/Paris')
self.assertEqual(event.event_booth_count, 4)
self.assertEqual(len(event.event_mail_ids), 3)
self.assertEqual(len(event.event_ticket_ids), 2)
self.assertTrue(event.introduction_menu)
self.assertTrue(event.location_menu)
self.assertTrue(event.menu_register_cta)
self.assertEqual(event.message_partner_ids, self.env.user.partner_id + self.env.user.company_id.partner_id)
self.assertEqual(event.message_partner_ids, self.env.user.partner_id)
self.assertEqual(event.note, '<p>Template note</p>')
self.assertTrue(event.register_menu)
self.assertEqual(len(event.question_ids), 3)
@ -75,12 +72,23 @@ class TestEventEvent(TestEventFullCommon):
self.assertTrue(event.is_ongoing)
self.assertTrue(event.event_registrations_started)
def test_event_kanban_state_on_stage_change(self):
"""Test that kanban_state updates correctly when stage is changed."""
test_event_1 = self.env['event.event'].browse(self.test_event.ids)
test_event_2 = test_event_1.copy()
test_event_1.kanban_state = 'done'
test_event_2.kanban_state = 'cancel' # Event Cancelled
new_stage = self.env['event.stage'].create({'name': 'New Stage', 'sequence': 1})
(test_event_1 | test_event_2).stage_id = new_stage.id # Change event stage
self.assertEqual(test_event_1.kanban_state, 'normal', 'kanban state should reset to "normal" on stage change')
self.assertEqual(test_event_2.kanban_state, 'cancel', 'kanban state should not reset on stage change')
@freeze_time('2021-12-01 11:00:00')
@users('event_user')
def test_event_seats_and_schedulers(self):
now = datetime.now() # used to force create_date, as sql is not wrapped by freeze gun
self.env.cr._now = now
test_event = self.env['event.event'].browse(self.test_event.ids)
ticket_1 = test_event.event_ticket_ids.filtered(lambda ticket: ticket.name == 'Ticket1')
ticket_2 = test_event.event_ticket_ids.filtered(lambda ticket: ticket.name == 'Ticket2')
@ -94,14 +102,15 @@ class TestEventEvent(TestEventFullCommon):
self.assertFalse(ticket_2.sale_available)
# make 9 registrations (let 1 on ticket)
with self.mock_mail_gateway():
with self.mock_datetime_and_now(self.reference_now), \
self.mock_mail_gateway():
self.env['event.registration'].create([
{'create_date': now,
'email': 'test.customer.%02d@test.example.com' % x,
'phone': '04560011%02d' % x,
'event_id': test_event.id,
'event_ticket_id': ticket_1.id,
'name': 'Customer %d' % x,
{
'email': 'test.customer.%02d@test.example.com' % x,
'phone': '04560011%02d' % x,
'event_id': test_event.id,
'event_ticket_id': ticket_1.id,
'name': 'Customer %d' % x,
}
for x in range(0, 9)
])
@ -114,27 +123,29 @@ class TestEventEvent(TestEventFullCommon):
self.assertEqual(ticket_2.seats_available, 0)
# prevent registration due to ticket limit
with self.assertRaises(exceptions.ValidationError):
with self.mock_datetime_and_now(self.reference_now), \
self.assertRaises(exceptions.ValidationError):
self.env['event.registration'].create([
{'create_date': now,
'email': 'additional.customer.%02d@test.example.com' % x,
'phone': '04560011%02d' % x,
'event_id': test_event.id,
'event_ticket_id': ticket_1.id,
'name': 'Additional Customer %d' % x,
{
'email': 'additional.customer.%02d@test.example.com' % x,
'phone': '04560011%02d' % x,
'event_id': test_event.id,
'event_ticket_id': ticket_1.id,
'name': 'Additional Customer %d' % x,
}
for x in range(0, 2)
])
# make 20 registrations (on free ticket)
with self.mock_mail_gateway():
with self.mock_datetime_and_now(self.reference_now), \
self.mock_mail_gateway():
self.env['event.registration'].create([
{'create_date': now,
'email': 'other.customer.%02d@test.example.com' % x,
'phone': '04560011%02d' % x,
'event_id': test_event.id,
'event_ticket_id': ticket_2.id,
'name': 'Other Customer %d' % x,
{
'email': 'other.customer.%02d@test.example.com' % x,
'phone': '04560011%02d' % x,
'event_id': test_event.id,
'event_ticket_id': ticket_2.id,
'name': 'Other Customer %d' % x,
}
for x in range(0, 20)
])
@ -145,14 +156,15 @@ class TestEventEvent(TestEventFullCommon):
self.assertEqual(ticket_2.seats_available, 0)
# prevent registration due to event limit
with self.assertRaises(exceptions.ValidationError):
with self.mock_datetime_and_now(self.reference_now), \
self.assertRaises(exceptions.ValidationError):
self.env['event.registration'].create([
{'create_date': now,
'email': 'additional.customer.%02d@test.example.com' % x,
'phone': '04560011%02d' % x,
'event_id': test_event.id,
'event_ticket_id': ticket_2.id,
'name': 'Additional Customer %d' % x,
{
'email': 'additional.customer.%02d@test.example.com' % x,
'phone': '04560011%02d' % x,
'event_id': test_event.id,
'event_ticket_id': ticket_2.id,
'name': 'Additional Customer %d' % x,
}
for x in range(0, 2)
])

View file

@ -2,15 +2,15 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from freezegun import freeze_time
from unittest.mock import patch
from odoo.addons.mail.tests.common import MockEmail
from odoo.addons.sms.tests.common import MockSMS
from odoo.addons.test_event_full.tests.common import TestWEventCommon
from odoo.exceptions import ValidationError
from odoo.tools import mute_logger
from odoo.addons.test_event_full.tests.common import TestEventFullCommon, TestEventMailCommon
from odoo.tests import tagged, users
from odoo.tools import formataddr
class TestTemplateRefModel(TestWEventCommon):
@tagged('event_mail', 'post_install', '-at_install')
class TestEventMailInternals(TestEventMailCommon):
def test_template_ref_delete_lines(self):
""" When deleting a template, related lines should be deleted too """
@ -51,117 +51,303 @@ class TestTemplateRefModel(TestWEventCommon):
self.assertEqual(len(event_type.event_type_mail_ids.exists()), 0)
self.assertEqual(len(event.event_mail_ids.exists()), 0)
def test_template_ref_model_constraint(self):
test_cases = [
('mail', 'mail.template', True),
('mail', 'sms.template', False),
('sms', 'sms.template', True),
('sms', 'mail.template', False),
]
@tagged('event_mail', 'post_install', '-at_install')
class TestEventMailSchedule(TestEventMailCommon):
for notification_type, template_type, valid in test_cases:
with self.subTest(notification_type=notification_type, template_type=template_type):
if template_type == 'mail.template':
template = self.env[template_type].create({
'name': 'test template',
'model_id': self.env['ir.model']._get_id('event.registration'),
})
else:
template = self.env[template_type].create({
'name': 'test template',
'body': 'Body Test',
'model_id': self.env['ir.model']._get_id('event.registration'),
})
if not valid:
with self.assertRaises(ValidationError) as cm:
self.env['event.mail'].create({
'event_id': self.event.id,
'notification_type': notification_type,
'interval_unit': 'now',
'interval_type': 'before_event',
'template_ref': template,
})
if notification_type == 'mail':
self.assertEqual(str(cm.exception), 'The template which is referenced should be coming from mail.template model.')
else:
self.assertEqual(str(cm.exception), 'The template which is referenced should be coming from sms.template model.')
class TestEventSmsMailSchedule(TestWEventCommon, MockEmail, MockSMS):
@freeze_time('2020-07-06 12:00:00')
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_event_mail_before_trigger_sent_count(self):
""" Emails are sent to both confirmed and unconfirmed attendees.
This test checks that the count of sent emails includes the emails sent to unconfirmed ones
Time in the test is frozen to simulate the following state:
NOW Event Start Event End
12:00 13:00 14:00
| | |
| | time
3 hours
Trigger before event
"""
self.sms_template_rem = self.env['sms.template'].create({
'name': 'Test reminder',
'model_id': self.env.ref('event.model_event_registration').id,
'body': '{{ object.event_id.organizer_id.name }} reminder',
'lang': '{{ object.partner_id.lang }}'
})
test_event = self.env['event.event'].create({
'name': 'TestEventMail',
# 'user_id': self.env.ref('base.user_admin').id,
'auto_confirm': False,
'date_begin': datetime.now() + timedelta(hours=1),
'date_end': datetime.now() + timedelta(hours=2),
'event_mail_ids': [
(0, 0, { # email 3 hours before event
'interval_nbr': 3,
'interval_unit': 'hours',
'interval_type': 'before_event',
'template_ref': 'mail.template,%i' % self.env['ir.model.data']._xmlid_to_res_id('event.event_reminder')}),
(0, 0, { # sms 3 hours before event
'interval_nbr': 3,
'interval_unit': 'hours',
'interval_type': 'before_event',
'notification_type': 'sms',
'template_ref': 'sms.template,%i' % self.sms_template_rem.id}),
]
})
mail_scheduler = test_event.event_mail_ids
self.assertEqual(len(mail_scheduler), 2, 'There should be two mail schedulers. One for mail one for sms. Cannot perform test')
""" Emails are only sent to confirmed attendees. """
test_event = self.test_event
mail_schedulers = test_event.event_mail_ids
self.assertEqual(len(mail_schedulers), 6)
before = mail_schedulers.filtered(lambda m: m.interval_type == "before_event" and m.interval_unit == "days")
self.assertEqual(len(before), 2)
# Add registrations
self.env['event.registration'].create([{
_dummy, _dummy, open_reg, done_reg = self.env['event.registration'].create([{
'event_id': test_event.id,
'name': 'RegistrationUnconfirmed',
'email': 'Registration@Unconfirmed.com',
'phone': '1',
'state': 'draft',
}, {
'event_id': test_event.id,
'name': 'RegistrationCanceled',
'email': 'Registration@Canceled.com',
'phone': '2',
'state': 'cancel',
}, {
'event_id': test_event.id,
'name': 'RegistrationConfirmed',
'email': 'Registration@Confirmed.com',
'phone': '3',
'state': 'open',
}, {
'event_id': test_event.id,
'name': 'RegistrationDone',
'email': 'Registration@Done.com',
'phone': '4',
'state': 'done',
}])
with self.mock_mail_gateway(), self.mockSMSGateway():
mail_scheduler.execute()
with self.mock_datetime_and_now(self.event_date_begin - timedelta(days=2)), \
self.mock_mail_gateway(), \
self.mockSMSGateway():
before.execute()
self.assertEqual(len(self._new_mails), 2, 'Mails were not created')
self.assertEqual(len(self._new_sms), 2, 'SMS were not created')
for registration in open_reg, done_reg:
with self.subTest(registration_state=registration.state, medium='mail'):
self.assertMailMailWEmails(
[formataddr((registration.name, registration.email.lower()))],
'outgoing',
)
with self.subTest(registration_state=registration.state, medium='sms'):
self.assertSMS(
self.env['res.partner'],
registration.phone,
None,
)
self.assertEqual(len(self._new_mails), 2, 'Mails should not be sent to draft or cancel registrations')
self.assertEqual(len(self._new_sms), 2, 'SMS should not be sent to draft or cancel registrations')
self.assertEqual(test_event.seats_expected, 2, 'Wrong number of expected seats (attendees)')
self.assertEqual(test_event.seats_taken, 2, 'Wrong number of seats_taken')
self.assertEqual(mail_scheduler.filtered(lambda r: r.notification_type == 'mail').mail_count_done, 2,
'Wrong Emails Sent Count! Probably emails sent to unconfirmed attendees were not included into the Sent Count')
self.assertEqual(mail_scheduler.filtered(lambda r: r.notification_type == 'sms').mail_count_done, 2,
'Wrong SMS Sent Count! Probably SMS sent to unconfirmed attendees were not included into the Sent Count')
for scheduler in before:
self.assertEqual(
scheduler.mail_count_done, 2,
'Wrong Emails Sent Count! Probably emails sent to unconfirmed attendees were not included into the Sent Count'
)
@users('user_eventmanager')
def test_schedule_event_scalability(self):
""" Test scalability / iterative work on event-based schedulers """
test_event = self.env['event.event'].browse(self.test_event.ids)
registrations = self._create_registrations(test_event, 30)
registrations = registrations.sorted("id")
# check event-based schedulers
after_mail = test_event.event_mail_ids.filtered(lambda s: s.interval_type == "after_event" and s.notification_type == "mail")
self.assertEqual(len(after_mail), 1)
self.assertEqual(after_mail.mail_count_done, 0)
self.assertFalse(after_mail.mail_done)
after_sms = test_event.event_mail_ids.filtered(lambda s: s.interval_type == "after_event" and s.notification_type == "sms")
self.assertEqual(len(after_sms), 1)
self.assertEqual(after_sms.mail_count_done, 0)
self.assertFalse(after_sms.mail_done)
before_mail = test_event.event_mail_ids.filtered(lambda s: s.interval_type == "before_event" and s.notification_type == "mail")
self.assertEqual(len(before_mail), 1)
self.assertEqual(before_mail.mail_count_done, 0)
self.assertFalse(before_mail.mail_done)
before_sms = test_event.event_mail_ids.filtered(lambda s: s.interval_type == "before_event" and s.notification_type == "sms")
self.assertEqual(len(before_sms), 1)
self.assertEqual(before_sms.mail_count_done, 0)
self.assertFalse(before_sms.mail_done)
# setup batch and cron limit sizes to check iterative behavior
batch_size, cron_limit = 5, 20
self.env["ir.config_parameter"].sudo().set_param("mail.batch_size", batch_size)
self.env["ir.config_parameter"].sudo().set_param("mail.render.cron.limit", cron_limit)
# launch before event schedulers -> all communications are sent
current_now = self.event_date_begin - timedelta(days=1)
EventMail = type(self.env['event.mail'])
exec_origin = EventMail._execute_event_based_for_registrations
with (
patch.object(
EventMail, '_execute_event_based_for_registrations', autospec=True, wraps=EventMail, side_effect=exec_origin,
) as mock_exec,
self.mock_datetime_and_now(current_now),
self.mockSMSGateway(),
self.mock_mail_gateway(),
self.capture_triggers('event.event_mail_scheduler') as capture,
):
self.event_cron_id.method_direct_trigger()
self.assertFalse(after_mail.last_registration_id)
self.assertEqual(after_mail.mail_count_done, 0)
self.assertFalse(after_mail.mail_done)
self.assertFalse(after_sms.last_registration_id)
self.assertEqual(after_sms.mail_count_done, 0)
self.assertFalse(after_sms.mail_done)
# iterative work on registrations: only 20 (cron limit) are taken into account
self.assertEqual(before_mail.last_registration_id, registrations[19])
self.assertEqual(before_mail.mail_count_done, 20)
self.assertFalse(before_mail.mail_done)
self.assertEqual(before_sms.last_registration_id, registrations[19])
self.assertEqual(before_sms.mail_count_done, 20)
self.assertFalse(before_sms.mail_done)
self.assertEqual(mock_exec.call_count, 8, "Batch of 5 to make 20 registrations: 4 calls / scheduler")
# cron should have been triggered for the remaining registrations
self.assertSchedulerCronTriggers(capture, [current_now] * 2)
# relaunch to close scheduler
with (
self.mock_datetime_and_now(current_now),
self.mockSMSGateway(),
self.mock_mail_gateway(),
self.capture_triggers('event.event_mail_scheduler') as capture,
):
self.event_cron_id.method_direct_trigger()
self.assertEqual(before_mail.last_registration_id, registrations[-1])
self.assertEqual(before_mail.mail_count_done, 30)
self.assertTrue(before_mail.mail_done)
self.assertEqual(before_sms.last_registration_id, registrations[-1])
self.assertEqual(before_sms.mail_count_done, 30)
self.assertTrue(before_sms.mail_done)
self.assertFalse(capture.records)
# launch after event schedulers -> all communications are sent
current_now = self.event_date_end + timedelta(hours=1)
with (
self.mock_datetime_and_now(current_now),
self.mockSMSGateway(),
self.mock_mail_gateway(),
self.capture_triggers('event.event_mail_scheduler') as capture,
):
self.event_cron_id.method_direct_trigger()
# iterative work on registrations: only 20 (cron limit) are taken into account
self.assertEqual(after_mail.last_registration_id, registrations[19])
self.assertEqual(after_mail.mail_count_done, 20)
self.assertFalse(after_mail.mail_done)
self.assertEqual(after_sms.last_registration_id, registrations[19])
self.assertEqual(after_sms.mail_count_done, 20)
self.assertFalse(after_sms.mail_done)
self.assertEqual(mock_exec.call_count, 8, "Batch of 5 to make 20 registrations: 4 calls / scheduler")
# cron should have been triggered for the remaining registrations
self.assertSchedulerCronTriggers(capture, [current_now] * 2)
# relaunch to close scheduler
with (
self.mock_datetime_and_now(current_now),
self.mockSMSGateway(),
self.mock_mail_gateway(),
self.capture_triggers('event.event_mail_scheduler') as capture,
):
self.event_cron_id.method_direct_trigger()
self.assertEqual(after_mail.last_registration_id, registrations[-1])
self.assertEqual(after_mail.mail_count_done, 30)
self.assertTrue(after_mail.mail_done)
self.assertEqual(after_sms.last_registration_id, registrations[-1])
self.assertEqual(after_sms.mail_count_done, 30)
self.assertTrue(after_sms.mail_done)
self.assertFalse(capture.records)
@users('user_eventmanager')
def test_schedule_subscription_scalability(self):
""" Test scalability / iterative work on subscription-based schedulers """
test_event = self.env['event.event'].browse(self.test_event.ids)
sub_mail = test_event.event_mail_ids.filtered(lambda s: s.interval_type == "after_sub" and s.interval_unit == "now" and s.notification_type == "mail")
self.assertEqual(len(sub_mail), 1)
self.assertEqual(sub_mail.mail_count_done, 0)
sub_sms = test_event.event_mail_ids.filtered(lambda s: s.interval_type == "after_sub" and s.interval_unit == "now" and s.notification_type == "sms")
self.assertEqual(len(sub_sms), 1)
self.assertEqual(sub_sms.mail_count_done, 0)
# setup batch and cron limit sizes to check iterative behavior
batch_size, cron_limit = 5, 20
self.env["ir.config_parameter"].sudo().set_param("mail.batch_size", batch_size)
self.env["ir.config_parameter"].sudo().set_param("mail.render.cron.limit", cron_limit)
# create registrations -> each one receives its on subscribe communication
EventMailRegistration = type(self.env['event.mail.registration'])
exec_origin = EventMailRegistration._execute_on_registrations
with patch.object(
EventMailRegistration, '_execute_on_registrations', autospec=True, wraps=EventMailRegistration, side_effect=exec_origin,
) as mock_exec, \
self.mock_datetime_and_now(self.reference_now + timedelta(hours=1)), \
self.mockSMSGateway(), \
self.mock_mail_gateway(), \
self.capture_triggers('event.event_mail_scheduler') as capture:
self._create_registrations(test_event, 30)
# iterative work on registrations: only 20 (cron limit) are taken into account
self.assertEqual(sub_mail.mail_count_done, 20)
self.assertEqual(sub_sms.mail_count_done, 20)
self.assertEqual(mock_exec.call_count, 8, "Batch of 5 to make 20 registrations: 4 calls / scheduler")
# cron should have been triggered for the remaining registrations
self.assertSchedulerCronTriggers(capture, [self.reference_now + timedelta(hours=1)] * 2)
# iterative work on registrations, force cron to close those
with (
patch.object(
EventMailRegistration, '_execute_on_registrations', autospec=True, wraps=EventMailRegistration, side_effect=exec_origin,
) as mock_exec,
self.mock_datetime_and_now(self.reference_now + timedelta(hours=1)),
self.mockSMSGateway(),
self.mock_mail_gateway(),
self.capture_triggers('event.event_mail_scheduler') as capture,
):
self.event_cron_id.method_direct_trigger()
# finished sending communications
self.assertEqual(sub_mail.mail_count_done, 30)
self.assertEqual(sub_sms.mail_count_done, 30)
self.assertFalse(capture.records)
self.assertEqual(mock_exec.call_count, 4, "Batch of 5 to make 10 remaining registrations: 2 calls / scheduler")
@tagged('event_mail', 'post_install', '-at_install')
class TestEventSaleMail(TestEventFullCommon):
def test_event_mail_on_sale_confirmation(self):
"""Test that a mail is sent to the customer when a sale order is confirmed."""
ticket = self.test_event.event_ticket_ids[0]
self.test_event.env.company.partner_id.email = 'test.email@test.example.com'
order_line_vals = {
"event_id": self.test_event.id,
"event_ticket_id": ticket.id,
"product_id": ticket.product_id.id,
"product_uom_qty": 1,
}
self.customer_so.write({"order_line": [(0, 0, order_line_vals)]})
# check sale mail configuration
aftersub = self.test_event.event_mail_ids.filtered(
lambda m: m.interval_type == "after_sub"
)
self.assertTrue(aftersub)
aftersub.template_ref.email_from = "{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}"
self.assertEqual(self.test_event.organizer_id, self.test_event.env.company.partner_id)
registration = self.env["event.registration"].create(
{
**self.website_customer_data[0],
"partner_id": self.event_customer.id,
"sale_order_line_id": self.customer_so.order_line[0].id,
}
)
self.assertEqual(self.test_event.registration_ids, registration)
self.assertEqual(self.customer_so.state, "draft")
self.assertEqual(registration.state, "draft")
with self.mock_mail_gateway():
self.customer_so.action_confirm()
# mail send is done when writing state value, hence flushing for the test
registration.flush_recordset()
self.assertEqual(self.customer_so.state, "sale")
self.assertEqual(registration.state, "open")
# Ensure mails are sent to customers right after subscription
self.assertMailMailWRecord(
registration,
[self.event_customer.id],
"outgoing",
author=self.test_event.organizer_id,
fields_values={
"email_from": self.test_event.organizer_id.email_formatted,
},
)
def test_registration_template_body_translation(self):
self.env['res.lang']._activate_lang('fr_BE')
test_event = self.test_event
self.partners[0].lang = 'fr_BE'
self.env.ref('event.event_subscription').with_context(lang='fr_BE').body_html = 'Bonjour'
with self.mock_mail_gateway(mail_unlink_sent=False):
self.env['event.registration'].create({
'event_id': test_event.id,
'partner_id': self.partners[0].id
})
self.assertEqual(self._new_mails[0].body_html, "<p>Bonjour</p>")

View file

@ -5,6 +5,7 @@ from datetime import datetime, timedelta
from odoo.addons.test_event_full.tests.common import TestEventFullCommon
from odoo.exceptions import AccessError
from odoo.fields import Command
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger
@ -55,7 +56,7 @@ class TestEventSecurity(TestEventFullCommon):
def test_event_access_event_registration(self):
# Event: read ok
event = self.test_event.with_user(self.env.user)
event.read(['name', 'user_id', 'kanban_state_label'])
event.read(['name', 'user_id', 'kanban_state'])
# Event: read only
with self.assertRaises(AccessError):
@ -77,7 +78,7 @@ class TestEventSecurity(TestEventFullCommon):
def test_event_access_event_user(self):
# Event
event = self.test_event.with_user(self.env.user)
event.read(['name', 'user_id', 'kanban_state_label'])
event.read(['name', 'user_id', 'kanban_state'])
event.write({'name': 'New name'})
self.env['event.event'].create({
'name': 'Event',
@ -133,7 +134,7 @@ class TestEventSecurity(TestEventFullCommon):
event_type.unlink()
# Settings access rights required to enable some features
self.user_eventmanager.write({'groups_id': [
self.user_eventmanager.write({'group_ids': [
(3, self.env.ref('base.group_system').id),
(4, self.env.ref('base.group_erp_manager').id)
]})
@ -142,6 +143,45 @@ class TestEventSecurity(TestEventFullCommon):
})
event_config.execute()
def test_event_question_access(self):
""" Check that some user groups have access to questions and answers only if they are linked to at
least one published event. """
question = self.env['event.question'].create({
"title": "Question",
"event_ids": [Command.create({
'name': 'Unpublished Event',
'is_published': False,
})]
})
answer = self.env['event.question.answer'].create({
"name": "Answer",
"question_id": question.id,
})
restricted_users = [self.user_employee, self.user_portal, self.user_public]
unrestricted_users = [self.user_eventmanager, self.user_eventuser]
for user in restricted_users:
with self.assertRaises(AccessError, msg=f'{user.name} should not have access to questions of unpublished events'):
question.with_user(user).read(['title'])
with self.assertRaises(AccessError, msg=f'{user.name} should not have access to answers of unpublished events'):
answer.with_user(user).read(['name'])
for user in unrestricted_users:
question.with_user(user).read(['title'])
answer.with_user(user).read(['name'])
# To check the access of user groups to questions and answers linked to at least one published event.
self.env['event.event'].create({
'name': 'Published Event',
'is_published': True,
'question_ids': [Command.set(question.ids)],
})
# Check that all user groups have access to questions and answers linked to at least one published event.
for user in restricted_users + unrestricted_users:
question.with_user(user).read(['title'])
answer.with_user(user).read(['name'])
def test_implied_groups(self):
"""Test that the implied groups are correctly set.

View file

@ -6,8 +6,7 @@ from freezegun import freeze_time
from odoo.addons.test_event_full.tests.common import TestEventFullCommon
from odoo.addons.website.tests.test_performance import UtilPerf
from odoo.tests.common import users, warmup, Form
from odoo.tests import tagged
from odoo.tests import Form, users, warmup, tagged
@tagged('event_performance', 'post_install', '-at_install', '-standard')
@ -17,6 +16,9 @@ class EventPerformanceCase(TestEventFullCommon):
super(EventPerformanceCase, self).setUp()
# patch registry to simulate a ready environment
self.patch(self.env.registry, 'ready', True)
# we don't use mock_mail_gateway thus want to mock smtp to test the stack
self._mock_smtplib_connection()
self._flush_tracking()
def _flush_tracking(self):
@ -52,7 +54,7 @@ class TestEventPerformance(EventPerformanceCase):
batch_size = 20
# simple without type involved + website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=5368): # tef 4944 / com 4943
with freeze_time(self.reference_now), self.assertQueryCount(event_user=3418): # tef 3316 / com 3315
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = [
dict(self.event_base_vals,
@ -60,7 +62,7 @@ class TestEventPerformance(EventPerformanceCase):
)
for x in range(batch_size)
]
self.env['event.event'].create(event_values)
self.env['event.event'].with_context(lang='en_US').create(event_values)
@users('event_user')
@warmup
@ -70,7 +72,7 @@ class TestEventPerformance(EventPerformanceCase):
event_type = self.env['event.type'].browse(self.test_event_type.ids)
# complex with type
with freeze_time(self.reference_now), self.assertQueryCount(event_user=439): # 439
with freeze_time(self.reference_now), self.assertQueryCount(event_user=432): # tef 432
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = [
dict(self.event_base_vals,
@ -89,7 +91,7 @@ class TestEventPerformance(EventPerformanceCase):
event_type = self.env['event.type'].browse(self.test_event_type.ids)
# complex with type + website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=5480): # tef 5056 / com 5055
with freeze_time(self.reference_now), self.assertQueryCount(event_user=3522): # tef 3420 / com 3419
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = [
dict(self.event_base_vals,
@ -97,7 +99,7 @@ class TestEventPerformance(EventPerformanceCase):
)
for x in range(batch_size)
]
self.env['event.event'].create(event_values)
self.env['event.event'].with_context(lang='en_US').create(event_values)
@users('event_user')
@ -107,7 +109,7 @@ class TestEventPerformance(EventPerformanceCase):
has_social = 'social_menu' in self.env['event.event'] # otherwise view may crash in enterprise
# no type, no website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=206): # tef 160 / com 160
with freeze_time(self.reference_now), self.assertQueryCount(event_user=108): # tef 103 / com 103
self.env.cr._now = self.reference_now # force create_date to check schedulers
# Require for `website_menu` to be visible
# <div name="event_menu_configuration" groups="base.group_no_one">
@ -128,7 +130,7 @@ class TestEventPerformance(EventPerformanceCase):
has_social = 'social_menu' in self.env['event.event'] # otherwise view may crash in enterprise
# no type, website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=666): # tef 565 / com 566
with freeze_time(self.reference_now), self.assertQueryCount(event_user=429): # tef 379 / com 380
self.env.cr._now = self.reference_now # force create_date to check schedulers
# Require for `website_menu` to be visible
# <div name="event_menu_configuration" groups="base.group_no_one">
@ -150,7 +152,7 @@ class TestEventPerformance(EventPerformanceCase):
has_social = 'social_menu' in self.env['event.event'] # otherwise view may crash in enterprise
# type and website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=692): # tef 593 / com 596
with freeze_time(self.reference_now), self.assertQueryCount(event_user=472): # tef 426 / com 428
self.env.cr._now = self.reference_now # force create_date to check schedulers
# Require for `website_menu` to be visible
# <div name="event_menu_configuration" groups="base.group_no_one">
@ -168,7 +170,7 @@ class TestEventPerformance(EventPerformanceCase):
def test_event_create_single_notype(self):
""" Test a single event creation """
# simple without type involved
with freeze_time(self.reference_now), self.assertQueryCount(event_user=31): # 31
with freeze_time(self.reference_now), self.assertQueryCount(event_user=31): # tef 31
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = dict(
self.event_base_vals,
@ -181,13 +183,13 @@ class TestEventPerformance(EventPerformanceCase):
def test_event_create_single_notype_website(self):
""" Test a single event creation """
# simple without type involved + website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=352): # tef 327 / com 326
with freeze_time(self.reference_now), self.assertQueryCount(event_user=242): # tef 228 / com 234
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = dict(
self.event_base_vals,
website_menu=True
)
self.env['event.event'].create([event_values])
self.env['event.event'].with_context(lang='en_US').create([event_values])
@users('event_user')
@warmup
@ -196,7 +198,7 @@ class TestEventPerformance(EventPerformanceCase):
event_type = self.env['event.type'].browse(self.test_event_type.ids)
# complex with type
with freeze_time(self.reference_now), self.assertQueryCount(event_user=58): # 58
with freeze_time(self.reference_now), self.assertQueryCount(event_user=52): # tef 52
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = dict(
self.event_base_vals,
@ -212,13 +214,13 @@ class TestEventPerformance(EventPerformanceCase):
event_type = self.env['event.type'].browse(self.test_event_type.ids)
# complex with type + website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=387): # tef 362 / com 361
with freeze_time(self.reference_now), self.assertQueryCount(event_user=274): # tef 266 / com 265
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = dict(
self.event_base_vals,
event_type_id=event_type.id,
)
self.env['event.event'].create([event_values])
self.env['event.event'].with_context(lang='en_US').create([event_values])
@tagged('event_performance', 'registration_performance', 'post_install', '-at_install', '-standard')
@ -234,7 +236,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
"""
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=720): # tef only: 674? - com runbot 716 - ent runbot 719
with freeze_time(self.reference_now), self.assertQueryCount(event_user=638): # tef 633 / com 636
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = [
dict(reg_data,
@ -258,7 +260,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
"""
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=210): # tef 167 / com runbot 206
with freeze_time(self.reference_now), self.assertQueryCount(event_user=165): # tef 164 / com runbot 163
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = [
dict(reg_data,
@ -280,7 +282,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
form like) """
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=731): # tef only: 685? - com runbot 727
with freeze_time(self.reference_now), self.assertQueryCount(event_user=651): # tef 647 - com 649
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = [
dict(reg_data,
@ -301,12 +303,11 @@ class TestRegistrationPerformance(EventPerformanceCase):
""" Test a single registration creation using Form """
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=231): # tef only: 210? - com runbot 216
with freeze_time(self.reference_now), self.assertQueryCount(event_user=145): # tef 140 / com 143
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['event.registration']) as reg_form:
reg_form.event_id = event
reg_form.email = 'email.00@test.example.com'
reg_form.mobile = '0456999999'
reg_form.name = 'My Customer'
reg_form.phone = '0456000000'
_registration = reg_form.save()
@ -317,7 +318,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
""" Test a single registration creation using Form """
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=233): # tef only: 213? - com runbot 217
with freeze_time(self.reference_now), self.assertQueryCount(event_user=146): # tef 141 / com 144
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['event.registration']) as reg_form:
reg_form.event_id = event
@ -330,7 +331,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
""" Test a single registration creation using Form """
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=124): # tef 107 / com 109
with freeze_time(self.reference_now), self.assertQueryCount(event_user=63): # tef 61 / com 61
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['event.registration'].with_context(event_lead_rule_skip=True)) as reg_form:
reg_form.event_id = event
@ -344,7 +345,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
event = self.env['event.event'].browse(self.test_event.ids)
# simple customer data
with freeze_time(self.reference_now), self.assertQueryCount(event_user=143): # tef only: 135? - com runbot 140
with freeze_time(self.reference_now), self.assertQueryCount(event_user=122): # tef 118 / com 120
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = dict(
self.customer_data[0],
@ -358,7 +359,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
event = self.env['event.event'].browse(self.test_event.ids)
# partner-based customer
with freeze_time(self.reference_now), self.assertQueryCount(event_user=149): # tef only: 142? - com runbot 146
with freeze_time(self.reference_now), self.assertQueryCount(event_user=121): # tef 117 / com 120
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = {
'event_id': event.id,
@ -373,7 +374,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
event = self.env['event.event'].browse(self.test_event.ids)
# partner-based customer
with freeze_time(self.reference_now), self.assertQueryCount(event_user=46): # tef 41 / com 43
with freeze_time(self.reference_now), self.assertQueryCount(event_user=39): # tef 38 / com 38
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = {
'event_id': event.id,
@ -388,7 +389,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
event = self.env['event.event'].browse(self.test_event.ids)
# website customer data
with freeze_time(self.reference_now), self.assertQueryCount(event_user=151): # tef only: 142? - com runbot 146
with freeze_time(self.reference_now), self.assertQueryCount(event_user=126): # tef 122 / com 124
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = dict(
self.website_customer_data[0],
@ -429,7 +430,6 @@ class TestOnlineEventPerformance(EventPerformanceCase, UtilPerf):
])
def _test_url_open(self, url):
url += ('?' not in url and '?' or '') + '&debug=disable-t-cache'
return self.url_open(url)
@warmup
@ -437,7 +437,7 @@ class TestOnlineEventPerformance(EventPerformanceCase, UtilPerf):
# website customer data
with freeze_time(self.reference_now):
self.authenticate('user_eventmanager', 'user_eventmanager')
with self.assertQueryCount(default=36): # tef 35
with self.assertQueryCount(default=35): # tef 34
self._test_url_open('/event/%i' % self.test_event.id)
@warmup
@ -445,7 +445,7 @@ class TestOnlineEventPerformance(EventPerformanceCase, UtilPerf):
# website customer data
with freeze_time(self.reference_now):
self.authenticate(None, None)
with self.assertQueryCount(default=27):
with self.assertQueryCount(default=25):
self._test_url_open('/event/%i' % self.test_event.id)
@warmup
@ -453,7 +453,7 @@ class TestOnlineEventPerformance(EventPerformanceCase, UtilPerf):
# website customer data
with freeze_time(self.reference_now):
self.authenticate('user_eventmanager', 'user_eventmanager')
with self.assertQueryCount(default=39): # tef 38
with self.assertQueryCount(default=43): # tef 42
self._test_url_open('/event')
@warmup
@ -461,23 +461,22 @@ class TestOnlineEventPerformance(EventPerformanceCase, UtilPerf):
# website customer data
with freeze_time(self.reference_now):
self.authenticate(None, None)
with self.assertQueryCount(default=28):
with self.assertQueryCount(default=39):
self._test_url_open('/event')
# @warmup
# def test_register_public(self):
# with freeze_time(self.reference_now + timedelta(hours=3)): # be sure sales has started
# self.assertTrue(self.test_event.event_registrations_started)
# self.authenticate(None, None)
# with self.assertQueryCount(default=99999): # tef only: 1110
# self.browser_js(
# '/event/%i/register' % self.test_event.id,
# 'odoo.__DEBUG__.services["web_tour.tour"].run("wevent_performance_register")',
# 'odoo.__DEBUG__.services["web_tour.tour"].tours.wevent_performance_register.ready',
# login=None,
# timeout=200,
# )
@warmup
def test_register_public(self):
with freeze_time(self.reference_now + timedelta(hours=3)): # be sure sales has started
self.assertTrue(self.test_event.event_registrations_started)
self.authenticate(None, None)
with self.assertQueryCount(default=1197): # tef: 1197
self.start_tour(
'/event/%i/register' % self.test_event.id,
'wevent_performance_register',
login=None,
timeout=200,
)
# # minimal checkup, to be improved in future tests independently from performance
# self.assertEqual(len(self.test_event.registration_ids), 3)
# self.assertEqual(len(self.test_event.registration_ids.visitor_id), 1)
# minimal checkup, to be improved in future tests independently from performance
self.assertEqual(len(self.test_event.registration_ids), 3)
self.assertEqual(len(self.test_event.registration_ids.visitor_id), 1)

View file

@ -0,0 +1,41 @@
from odoo.addons.test_event_full.tests.common import TestWEventCommon
from odoo.tests import tagged
from odoo.tests.common import users
@tagged('event_online', 'post_install', '-at_install')
class TestWEventMenu(TestWEventCommon):
@users('admin')
def test_seo_data(self):
"""Test SEO data for submenus on event website page"""
self.assertFalse(self.event.website_meta_title, 'Event should initially have no meta title')
self.event.write({
'website_meta_title': 'info',
})
self.assertTrue(self.event.website_meta_title, 'Event should have a meta title after writing')
menus = [
('booth_menu_ids', 'Get a Booth'),
('exhibitor_menu_ids', 'Exhibitor'),
('community_menu_ids', 'Leaderboard'),
('track_menu_ids', 'Talks'),
('track_menu_ids', 'Agenda'),
('track_proposal_menu_ids', 'Talk Proposal'),
]
for menu_field, menu_name in menus:
menu = self.event[menu_field]
if menu_field == 'track_menu_ids':
menu_url = '/track' if menu_name == 'Talks' else '/agenda'
menu = self.event[menu_field].filtered(lambda menu: menu.menu_id.url.endswith(menu_url))
self.assertFalse(menu.website_meta_title, f"{menu_name} page should initially have no meta title")
menu.write({'website_meta_title': menu_name})
web_page = self.url_open(menu.menu_id.url)
self.assertTrue(menu.website_meta_title, f"{menu_name} page should have a meta title after writing")
self.assertIn(f"<title>{menu.website_meta_title}</title>", web_page.text)

View file

@ -4,6 +4,7 @@
from freezegun import freeze_time
from odoo import tests
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_event_full.tests.common import TestWEventCommon
@ -11,13 +12,9 @@ from odoo.addons.test_event_full.tests.common import TestWEventCommon
class TestWEventRegister(TestWEventCommon):
def test_register(self):
self.env.company.country_id = self.env.ref('base.us')
with freeze_time(self.reference_now, tick=True):
self.browser_js(
'/event',
'odoo.__DEBUG__.services["web_tour.tour"].run("wevent_register")',
'odoo.__DEBUG__.services["web_tour.tour"].tours.wevent_register.ready',
login=None
)
self.start_tour('/event', 'wevent_register', login=None)
new_registrations = self.event.registration_ids
visitor = new_registrations.visitor_id
@ -40,5 +37,15 @@ class TestWEventRegister(TestWEventCommon):
self.assertEqual(visitor.display_name, "Raoulette Poiluchette")
self.assertEqual(visitor.event_registration_ids, new_registrations)
self.assertEqual(visitor.partner_id, self.env['res.partner'])
self.assertEqual(visitor.mobile, "0456112233")
self.assertEqual(visitor.email, "raoulette@example.com")
def test_internal_user_register(self):
mail_new_test_user(
self.env,
name='User Internal',
login='user_internal',
email='user_internal@example.com',
groups='base.group_user',
)
with freeze_time(self.reference_now, tick=True):
self.start_tour('/event', 'wevent_register', login='user_internal')

View file

@ -12,38 +12,15 @@ pip install odoo-bringout-oca-ocb-test_mail
## Dependencies
This addon depends on:
- mail
- test_performance
## Manifest Information
- **Name**: Mail Tests
- **Version**: 1.0
- **Category**: Hidden
- **License**: LGPL-3
- **Installable**: True
- test_orm
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_mail`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/test_mail
## 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,13 +1,14 @@
[project]
name = "odoo-bringout-oca-ocb-test_mail"
version = "16.0.0"
description = "Mail Tests - Mail Tests: performances and tests specific to mail"
description = "Mail Tests -
Mail Tests: performances and tests specific to mail
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-mail>=16.0.0",
"odoo-bringout-oca-ocb-test_performance>=16.0.0",
"odoo-bringout-oca-ocb-mail>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -17,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

@ -11,7 +11,7 @@ present in a separate module as it contains models used only to perform
tests independently to functional aspects of other models. """,
'depends': [
'mail',
'test_performance',
'test_orm',
],
'data': [
'security/ir.model.access.csv',
@ -21,16 +21,14 @@ tests independently to functional aspects of other models. """,
'data/subtype_data.xml',
],
'assets': {
'web.qunit_suite_tests': [
'test_mail/static/tests/*',
'web.assets_unit_tests': [
'test_mail/static/tests/**/*',
],
'web.qunit_mobile_suite_tests': [
'test_mail/static/tests/mobile/activity_tests.js',
],
'web.tests_assets': [
'test_mail/static/tests/helpers/*',
'web.assets_tests': [
'test_mail/static/tests/tours/*',
],
},
'installable': True,
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -36,5 +36,18 @@
<field name="chaining_type">trigger</field>
<field name="triggered_next_type_id" ref="test_mail.mail_act_test_chained_2"/>
</record>
<record id="mail_act_test_upload_document" model="mail.activity.type">
<field name="name">Document</field>
<field name="summary">Document</field>
<field name="delay_count">5</field>
<field name="category">upload_file</field>
<field name="res_model">mail.test.activity</field>
</record>
<record id="mail_act_test_todo_generic" model="mail.activity.type">
<field name="name">Do Stuff</field>
<field name="summary">Hey Zoidberg! Get in here!</field>
<field name="category">default</field>
</record>
</odoo>

View file

@ -4,6 +4,7 @@
<field name="name">Mail Test Full: Tracking Template</field>
<field name="subject">Test Template</field>
<field name="partner_to">{{ object.customer_id.id }}</field>
<field name="use_default_to" eval="False"/>
<field name="body_html" type="html"><p>Hello <t t-out="object.name or ''"></t></p></field>
<field name="model_id" ref="test_mail.model_mail_test_ticket"/>
<field name="auto_delete" eval="True"/>
@ -13,6 +14,7 @@
<field name="name">Mail Test: Template</field>
<field name="subject">Post on {{ object.name }}</field>
<field name="partner_to">{{ object.customer_id.id }}</field>
<field name="use_default_to" eval="False"/>
<field name="body_html" type="html"><p>Adding stuff on <t t-out="object.name or ''"></t></p></field>
<field name="model_id" ref="test_mail.model_mail_test_container"/>
<field name="auto_delete" eval="True"/>
@ -33,6 +35,30 @@
</t>
</template>
<template id="mail_test_ticket_test_template_2">
<t t-call="web.html_container">
<t t-set="o" t-value="res_company"/>
<t t-call="web.external_layout">
<div class="page">
<p>This is another sample of an external report.</p>
</div>
</t>
</t>
</template>
<template id="mail_test_ticket_test_variable_template">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="ticket">
<t t-call="web.external_layout">
<div class="page">
<p>This is a sample of an external report for a ticket for
<span t-out="ticket.count"></span> people.</p>
</div>
</t>
</t>
</t>
</template>
<template id="mail_template_simple_test">
<p>Hello <t t-out="partner.name"/>, this comes from <t t-out="object.name"/>.</p>
</template>

View file

@ -52,4 +52,13 @@
<field name="internal" eval="True"/>
</record>
<!-- mail.test.ticket.partner -->
<record id="st_mail_test_ticket_partner_new" model="mail.message.subtype">
<field name="name">New ticket</field>
<field name="description">New Ticket</field>
<field name="res_model">mail.test.ticket.partner</field>
<field name="default" eval="True"/>
<field name="internal" eval="False"/>
</record>
</odoo>

View file

@ -11,7 +11,7 @@ Subject: {subject}
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_4200734_24778174.1344608186754"
Date: Fri, 10 Aug 2012 14:16:26 +0000
Date: {date}
Message-ID: {msg_id}
{extra}
------=_Part_4200734_24778174.1344608186754
@ -135,6 +135,38 @@ Message-ID: {msg_id}
</html>
"""
MAIL_TEMPLATE_SHORT = """Return-Path: {return_path}
To: {to}
cc: {cc}
Received: by mail1.openerp.com (Postfix, from userid 10002)
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
From: {email_from}
Subject: {subject}
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_4200734_24778174.1344608186754"
Date: Fri, 10 Aug 2012 14:16:26 +0000
Message-ID: {msg_id}
{extra}
------=_Part_4200734_24778174.1344608186754
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Eli alla à l'eau
--
Signature
------=_Part_4200734_24778174.1344608186754
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<div>Eli alla à l'eau<br/>
--<br/>
Sylvie
</div>
------=_Part_4200734_24778174.1344608186754--
"""
MAIL_MULTIPART_MIXED = """Return-Path: <ignasse.carambar@gmail.com>
X-Original-To: raoul@grosbedon.fr
Delivered-To: raoul@grosbedon.fr
@ -249,7 +281,7 @@ Date: Sun, 26 Mar 2023 05:23:22 +0200
Message-ID: {msg_id}
Subject: {subject}
From: "Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>
To: groups@test.com
To: groups@test.mycompany.com
Content-Type: multipart/mixed; boundary="000000000000b951de05f7c47a9e"
--000000000000b951de05f7c47a9e
@ -648,11 +680,11 @@ AAAAACwAAAAAAgACAAAEA3DJFQA7
--001a11416b9e9b229a05272b7052--
"""
MAIL_EML_ATTACHMENT = """Subject: Re: test attac
MAIL_EML_ATTACHMENT = """Subject: {subject}
From: {email_from}
To: {to}
References: <f3b9f8f8-28fa-2543-cab2-7aa68f679ebb@odoo.com>
Message-ID: <cb7eaf62-58dc-2017-148c-305d0c78892f@odoo.com>
References: {references}
Message-ID: {msg_id}
Date: Wed, 14 Mar 2018 14:26:58 +0100
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
Thunderbird/52.6.0
@ -1395,6 +1427,7 @@ Date: Fri, 10 Aug 2012 14:16:26 +0000
------=_Part_4200734_24778174.1344608186754
Content-Type: {pdf_mime}; name="scan_soraya.lernout_1691652648.pdf"
Content-Disposition: attachment; filename="scan_soraya.lernout_1691652648.pdf"
Content-Transfer-Encoding: base64
JVBERi0xLjEKJcKlwrHDqwoKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAgL1BhZ2VzIDIgMCBSCiAgPj4KZW5kb2JqCgoyIDAgb2JqCiAgPDwgL1R5cGUgL1BhZ2VzCiAgICAgL0tpZHMgWzMgMCBSXQogICAgIC9Db3VudCAxCiAgICAgL01lZGlhQm94IFswIDAgMzAwIDE0NF0KICA+PgplbmRvYmoKCjMgMCBvYmoKICA8PCAgL1R5cGUgL1BhZ2UKICAgICAgL1BhcmVudCAyIDAgUgogICAgICAvUmVzb3VyY2VzCiAgICAgICA8PCAvRm9udAogICAgICAgICAgIDw8IC9GMQogICAgICAgICAgICAgICA8PCAvVHlwZSAvRm9udAogICAgICAgICAgICAgICAgICAvU3VidHlwZSAvVHlwZTEKICAgICAgICAgICAgICAgICAgL0Jhc2VGb250IC9UaW1lcy1Sb21hbgogICAgICAgICAgICAgICA+PgogICAgICAgICAgID4+CiAgICAgICA+PgogICAgICAvQ29udGVudHMgNCAwIFIKICA+PgplbmRvYmoKCjQgMCBvYmoKICA8PCAvTGVuZ3RoIDU1ID4+CnN0cmVhbQogIEJUCiAgICAvRjEgMTggVGYKICAgIDAgMCBUZAogICAgKEhlbGxvIFdvcmxkKSBUagogIEVUCmVuZHN0cmVhbQplbmRvYmoKCnhyZWYKMCA1CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDAxOCAwMDAwMCBuIAowMDAwMDAwMDc3IDAwMDAwIG4gCjAwMDAwMDAxNzggMDAwMDAgbiAKMDAwMDAwMDQ1NyAwMDAwMCBuIAp0cmFpbGVyCiAgPDwgIC9Sb290IDEgMCBSCiAgICAgIC9TaXplIDUKICA+PgpzdGFydHhyZWYKNTY1CiUlRU9GCg==

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail_test_access
from . import mail_test_lead
from . import mail_test_ticket
from . import test_mail_corner_case_models
from . import test_mail_feature_models
from . import test_mail_models
from . import test_mail_thread_models

View file

@ -1,9 +1,11 @@
from odoo import exceptions, fields, models
from odoo import fields, models, tools
class MailTestAccess(models.Model):
""" Test access on mail models without depending on real models like channel
or partner which have their own set of ACLs. """
or partner which have their own set of ACLs. Public, portal and internal
have access to this model depending on 'access' field, allowing to check
ir.rule usage. """
_description = 'Mail Access Test'
_name = 'mail.test.access'
_inherit = ['mail.thread.blacklist']
@ -27,7 +29,7 @@ class MailTestAccess(models.Model):
],
name='Access', default='public')
def _mail_get_partner_fields(self):
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
@ -36,7 +38,7 @@ class MailTestAccessCusto(models.Model):
or partner which have their own set of ACLs. """
_description = 'Mail Access Test with Custo'
_name = 'mail.test.access.custo'
_inherit = ['mail.thread.blacklist']
_inherit = ['mail.thread.blacklist', 'mail.activity.mixin']
_mail_post_access = 'write' # default value but ease mock
_order = 'id DESC'
_primary_email = 'email_from'
@ -46,15 +48,47 @@ class MailTestAccessCusto(models.Model):
phone = fields.Char()
customer_id = fields.Many2one('res.partner', 'Customer')
is_locked = fields.Boolean()
is_readonly = fields.Boolean()
def _mail_get_partner_fields(self):
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
def _get_mail_message_access(self, res_ids, operation, model_name=None):
# customize message creation
if operation == "create":
if any(record.is_locked for record in self.browse(res_ids)):
raise exceptions.AccessError('Cannot post on locked records')
else:
return "read"
return super()._get_mail_message_access(res_ids, operation, model_name=model_name)
def _mail_get_operation_for_mail_message_operation(self, message_operation):
# customize message creation: only unlocked, except admins
if message_operation == "create" and not self.env.user._is_admin():
return dict.fromkeys(self.filtered(lambda r: not r.is_locked), 'read')
# customize read: read access on unlocked, write access on locked
elif message_operation == "read":
return {
record: 'write' if record.is_locked else 'read'
for record in self
}
return super()._mail_get_operation_for_mail_message_operation(message_operation)
class MailTestAccessPublic(models.Model):
"""A model inheriting from mail.thread with public read and write access
to test some public and guest interactions."""
_description = "Access Test Public"
_name = "mail.test.access.public"
_inherit = ["mail.thread"]
name = fields.Char("Name")
customer_id = fields.Many2one('res.partner', 'Customer')
email = fields.Char('Email')
mobile = fields.Char('Mobile')
is_locked = fields.Boolean()
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
def _get_customer_information(self):
email_key_to_values = super()._get_customer_information()
for record in self.filtered('email'):
# do not fill Falsy with random data, unless monorecord (= always correct)
if not tools.email_normalize(record.email) and len(self) > 1:
continue
values = email_key_to_values.setdefault(record.email, {})
if not values.get('phone'):
values['phone'] = record.mobile
return email_key_to_values

View file

@ -0,0 +1,55 @@
from odoo import fields, models, _
from odoo.tools.mail import parse_contact_from_email
class MailTestTLead(models.Model):
""" Lead-like model for business flows testing """
_name = "mail.test.lead"
_description = 'Lead-like model'
_inherit = [
'mail.thread.blacklist',
'mail.thread.cc',
'mail.activity.mixin',
]
_mail_defaults_to_email = True
_primary_email = 'email_from'
name = fields.Char()
company_id = fields.Many2one('res.company')
user_id = fields.Many2one('res.users', tracking=1)
email_from = fields.Char()
customer_name = fields.Char()
partner_id = fields.Many2one('res.partner', tracking=2)
lang_code = fields.Char()
phone = fields.Char()
def _creation_message(self):
self.ensure_one()
return _('A new lead has been created and is assigned to %(user_name)s.', user_name=self.user_id.name or _('nobody'))
def _get_customer_information(self):
email_normalized_to_values = super()._get_customer_information()
for lead in self:
email_key = lead.email_normalized or lead.email_from
values = email_normalized_to_values.setdefault(email_key, {})
values['lang'] = values.get('lang') or lead.lang_code
values['name'] = values.get('name') or lead.customer_name or parse_contact_from_email(lead.email_from)[0] or lead.email_from
values['phone'] = values.get('phone') or lead.phone
return email_normalized_to_values
def _message_post_after_hook(self, message, msg_vals):
if self.email_from and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
new_partner = message.partner_ids.filtered(
lambda partner: partner.email == self.email_from or (self.email_normalized and partner.email_normalized == self.email_normalized)
)
if new_partner:
if new_partner[0].email_normalized:
email_domain = ('email_normalized', '=', new_partner[0].email_normalized)
else:
email_domain = ('email_from', '=', new_partner[0].email)
self.search([('partner_id', '=', False), email_domain]).write({'partner_id': new_partner[0].id})
return super()._message_post_after_hook(message, msg_vals)

View file

@ -0,0 +1,266 @@
import ast
from odoo import api, fields, models, _
from odoo.tools import email_normalize
class MailTestTicket(models.Model):
""" This model can be used in tests when complex chatter features are
required like modeling tasks or tickets. """
_description = 'Ticket-like model'
_name = "mail.test.ticket"
_inherit = ['mail.thread']
_primary_email = 'email_from'
name = fields.Char()
email_from = fields.Char(tracking=True)
phone_number = fields.Char()
count = fields.Integer(default=1)
datetime = fields.Datetime(default=fields.Datetime.now)
mail_template = fields.Many2one('mail.template', 'Template')
customer_id = fields.Many2one('res.partner', 'Customer', tracking=2)
user_id = fields.Many2one('res.users', 'Responsible', tracking=1)
container_id = fields.Many2one('mail.test.container', tracking=True)
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
def _message_compute_subject(self):
self.ensure_one()
return f"Ticket for {self.name} on {self.datetime.strftime('%m/%d/%Y, %H:%M:%S')}"
def _notify_get_recipients_groups(self, message, model_description, msg_vals=False):
# Activate more groups to test query counters notably (and be backward compatible for tests)
groups = super()._notify_get_recipients_groups(
message, model_description, msg_vals=msg_vals
)
for group_name, _group_method, group_data in groups:
if group_name == 'portal':
group_data['active'] = True
elif group_name == 'customer':
group_data['active'] = True
group_data['has_button_access'] = True
return groups
def _track_template(self, changes):
res = super()._track_template(changes)
record = self[0]
if 'customer_id' in changes and record.mail_template:
res['customer_id'] = (
record.mail_template,
{
'composition_mode': 'mass_mail',
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
}
)
elif 'datetime' in changes:
res['datetime'] = (
'test_mail.mail_test_ticket_tracking_view',
{
'composition_mode': 'mass_mail',
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
}
)
return res
def _creation_subtype(self):
if self.container_id:
return self.env.ref('test_mail.st_mail_test_ticket_container_upd')
return super(MailTestTicket, self)._creation_subtype()
def _track_subtype(self, init_values):
self.ensure_one()
if 'container_id' in init_values and self.container_id:
return self.env.ref('test_mail.st_mail_test_ticket_container_upd')
return super(MailTestTicket, self)._track_subtype(init_values)
def _get_customer_information(self):
email_keys_to_values = super()._get_customer_information()
for ticket in self:
email_key = email_normalize(ticket.email_from) or ticket.email_from
# do not fill Falsy with random data, unless monorecord (= always correct)
if not email_key and len(self) > 1:
continue
values = email_keys_to_values.setdefault(email_key, {})
if not values.get('phone'):
values['phone'] = ticket.phone_number
return email_keys_to_values
class MailTestTicketEl(models.Model):
""" Just mail.test.ticket, but exclusion-list enabled. Kept as different
model to avoid messing with existing tests, notably performance, and ease
backward comparison. """
_description = 'Ticket-like model with exclusion list'
_name = "mail.test.ticket.el"
_inherit = [
'mail.test.ticket',
'mail.thread.blacklist',
]
_primary_email = 'email_from'
email_from = fields.Char(
'Email',
compute='_compute_email_from', readonly=False, store=True)
@api.depends('customer_id')
def _compute_email_from(self):
for ticket in self.filtered(lambda r: r.customer_id and not r.email_from):
ticket.email_from = ticket.customer_id.email_formatted
class MailTestTicketMc(models.Model):
""" Just mail.test.ticket, but multi company. Kept as different model to
avoid messing with existing tests, notably performance, and ease backward
comparison. """
_description = 'Ticket-like model'
_name = "mail.test.ticket.mc"
_inherit = ['mail.test.ticket']
_primary_email = 'email_from'
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
container_id = fields.Many2one('mail.test.container.mc', tracking=True)
def _get_customer_information(self):
email_keys_to_values = super()._get_customer_information()
for ticket in self:
email_key = email_normalize(ticket.email_from) or ticket.email_from
# do not fill Falsy with random data, unless monorecord (= always correct)
if not email_key and len(self) > 1:
continue
values = email_keys_to_values.setdefault(email_key, {})
if not values.get('company_id'):
values['company_id'] = ticket.company_id.id
return email_keys_to_values
def _notify_get_reply_to(self, default=None, author_id=False):
# Override to use alias of the parent container
aliases = self.sudo().mapped('container_id')._notify_get_reply_to(default=default, author_id=author_id)
res = {ticket.id: aliases.get(ticket.container_id.id) for ticket in self}
leftover = self.filtered(lambda rec: not rec.container_id)
if leftover:
res.update(super()._notify_get_reply_to(default=default, author_id=author_id))
return res
def _creation_subtype(self):
if self.container_id:
return self.env.ref('test_mail.st_mail_test_ticket_container_mc_upd')
return super()._creation_subtype()
def _track_subtype(self, init_values):
self.ensure_one()
if 'container_id' in init_values and self.container_id:
return self.env.ref('test_mail.st_mail_test_ticket_container_mc_upd')
return super()._track_subtype(init_values)
class MailTestTicketPartner(models.Model):
""" Mail.test.ticket.mc, with complete partner support. More functional
and therefore done in a separate model to avoid breaking other tests. """
_description = 'MC ticket-like model with partner support'
_name = "mail.test.ticket.partner"
_inherit = [
'mail.test.ticket.mc',
'mail.thread.blacklist',
]
_primary_email = 'email_from'
# fields to mimic stage-based tracing
state = fields.Selection(
[('new', 'New'), ('open', 'Open'), ('close', 'Close'),],
default='open', tracking=10)
state_template_id = fields.Many2one('mail.template')
def _message_post_after_hook(self, message, msg_vals):
if self.email_from and not self.customer_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
new_partner = message.partner_ids.filtered(
lambda partner: partner.email == self.email_from or (self.email_normalized and partner.email_normalized == self.email_normalized)
)
if new_partner:
if new_partner[0].email_normalized:
email_domain = ('email_normalized', '=', new_partner[0].email_normalized)
else:
email_domain = ('email_from', '=', new_partner[0].email)
self.search([
('customer_id', '=', False), email_domain,
]).write({'customer_id': new_partner[0].id})
return super()._message_post_after_hook(message, msg_vals)
def _creation_subtype(self):
if self.state == 'new':
return self.env.ref('test_mail.st_mail_test_ticket_partner_new')
return super(MailTestTicket, self)._creation_subtype()
def _track_template(self, changes):
res = super()._track_template(changes)
record = self[0]
# acknowledgement-like email, like in project/helpdesk
if 'state' in changes and record.state == 'new' and record.state_template_id:
res['state'] = (
record.state_template_id,
{
'auto_delete_keep_log': False,
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
'email_layout_xmlid': 'mail.mail_notification_light'
},
)
return res
class MailTestContainer(models.Model):
""" This model can be used in tests when container records like projects
or teams are required. """
_description = 'Project-like model with alias'
_name = "mail.test.container"
_mail_post_access = 'read'
_inherit = ['mail.thread', 'mail.alias.mixin']
name = fields.Char()
description = fields.Text()
customer_id = fields.Many2one('res.partner', 'Customer')
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
def _notify_get_recipients_groups(self, message, model_description, msg_vals=False):
# Activate more groups to test query counters notably (and be backward compatible for tests)
groups = super()._notify_get_recipients_groups(
message, model_description, msg_vals=msg_vals
)
for group_name, _group_method, group_data in groups:
if group_name == 'portal':
group_data['active'] = True
return groups
def _alias_get_creation_values(self):
values = super()._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('mail.test.ticket').id
values['alias_force_thread_id'] = False
if self.id:
values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}")
defaults['container_id'] = self.id
return values
class MailTestContainerMc(models.Model):
""" Just mail.test.container, but multi company. Kept as different model to
avoid messing with existing tests, notably performance, and ease backward
comparison. """
_description = 'Project-like model with alias (MC)'
_name = "mail.test.container.mc"
_mail_post_access = 'read'
_inherit = ['mail.test.container']
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
def _alias_get_creation_values(self):
values = super()._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('mail.test.ticket.mc').id
return values

View file

@ -21,6 +21,19 @@ class MailPerformanceThread(models.Model):
record.value_pc = float(record.value) / 100
class MailPerformanceThreadRecipients(models.Model):
_name = 'mail.performance.thread.recipients'
_description = 'Performance: mail.thread, for recipients'
_inherit = ['mail.thread']
_primary_email = 'email_from'
name = fields.Char()
value = fields.Integer()
email_from = fields.Char('Email From')
partner_id = fields.Many2one('res.partner', string='Customer')
user_id = fields.Many2one('res.users', 'Responsible', tracking=1)
class MailPerformanceTracking(models.Model):
_name = 'mail.performance.tracking'
_description = 'Performance: multi tracking'
@ -36,7 +49,7 @@ class MailTestFieldType(models.Model):
""" Test default values, notably type, messing through models during gateway
processing (i.e. lead.type versus attachment.type). """
_description = 'Test Field Type'
_name = 'mail.test.field.type'
_name = "mail.test.field.type"
_inherit = ['mail.thread']
name = fields.Char()
@ -49,11 +62,11 @@ class MailTestFieldType(models.Model):
@api.model_create_multi
def create(self, vals_list):
# Emulate an addon that alters the creation context, such as `crm`
if not self._context.get('default_type'):
if not self.env.context.get('default_type'):
self = self.with_context(default_type='first')
return super(MailTestFieldType, self).create(vals_list)
def _mail_get_partner_fields(self):
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
@ -61,7 +74,7 @@ class MailTestLang(models.Model):
""" A simple chatter model with lang-based capabilities, allowing to
test translations. """
_description = 'Lang Chatter Model'
_name = 'mail.test.lang'
_name = "mail.test.lang"
_inherit = ['mail.thread']
name = fields.Char()
@ -69,30 +82,85 @@ class MailTestLang(models.Model):
customer_id = fields.Many2one('res.partner')
lang = fields.Char('Lang')
def _mail_get_partner_fields(self):
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
def _notify_get_recipients_groups(self, msg_vals=None):
groups = super(MailTestLang, self)._notify_get_recipients_groups(msg_vals=msg_vals)
local_msg_vals = dict(msg_vals or {})
def _notify_get_recipients_groups(self, message, model_description, msg_vals=False):
groups = super()._notify_get_recipients_groups(
message, model_description, msg_vals=msg_vals
)
for group in [g for g in groups if g[0] in('follower', 'customer')]:
group_options = group[2]
group_options['has_button_access'] = True
group_options['actions'] = [
{'url': self._notify_get_action_link('controller', controller='/test_mail/do_stuff', **local_msg_vals),
'title': _('NotificationButtonTitle')}
]
return groups
# ------------------------------------------------------------
# TRACKING MODELS
# ------------------------------------------------------------
class MailTestTrackAllM2m(models.Model):
_description = 'Sub-model: pseudo tags for tracking'
_name = "mail.test.track.all.m2m"
_inherit = ['mail.thread']
name = fields.Char('Name')
class MailTestTrackAllO2m(models.Model):
_description = 'Sub-model: pseudo tags for tracking'
_name = "mail.test.track.all.o2m"
_inherit = ['mail.thread']
name = fields.Char('Name')
mail_track_all_id = fields.Many2one('mail.test.track.all')
class MailTestTrackAllPropertiesParent(models.Model):
_description = 'Properties Parent'
_name = "mail.test.track.all.properties.parent"
definition_properties = fields.PropertiesDefinition()
class MailTestTrackAll(models.Model):
_description = 'Test tracking on all field types'
_name = "mail.test.track.all"
_inherit = ['mail.thread']
boolean_field = fields.Boolean('Boolean', tracking=1)
char_field = fields.Char('Char', tracking=2)
company_id = fields.Many2one('res.company')
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
date_field = fields.Date('Date', tracking=3)
datetime_field = fields.Datetime('Datetime', tracking=4)
float_field = fields.Float('Float', tracking=5)
float_field_with_digits = fields.Float('Precise Float', digits=(10, 8), tracking=5)
html_field = fields.Html('Html', tracking=False)
integer_field = fields.Integer('Integer', tracking=7)
many2many_field = fields.Many2many(
'mail.test.track.all.m2m', string='Many2Many',
tracking=8)
many2one_field_id = fields.Many2one('res.partner', string='Many2one', tracking=9)
monetary_field = fields.Monetary('Monetary', tracking=10)
one2many_field = fields.One2many(
'mail.test.track.all.o2m', 'mail_track_all_id',
string='One2Many',
tracking=11)
properties_parent_id = fields.Many2one('mail.test.track.all.properties.parent', tracking=True)
properties = fields.Properties('Properties', definition='properties_parent_id.definition_properties')
selection_field = fields.Selection(
string='Selection',
selection=[('first', 'FIRST'), ('second', 'SECOND')],
tracking=12)
text_field = fields.Text('Text', tracking=13)
name = fields.Char('Name')
class MailTestTrackCompute(models.Model):
_name = 'mail.test.track.compute'
_description = "Test tracking with computed fields"
_name = "mail.test.track.compute"
_inherit = ['mail.thread']
partner_id = fields.Many2one('res.partner', tracking=True)
@ -101,63 +169,60 @@ class MailTestTrackCompute(models.Model):
partner_phone = fields.Char(related='partner_id.phone', tracking=True)
class MailTestTrackDurationMixin(models.Model):
_description = 'Fake model to test the mixin mail.tracking.duration.mixin'
_name = "mail.test.track.duration.mixin"
_track_duration_field = 'customer_id'
_inherit = ['mail.tracking.duration.mixin']
name = fields.Char()
customer_id = fields.Many2one('res.partner', 'Customer', tracking=True)
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
class MailTestTrackGroups(models.Model):
_description = "Test tracking with groups"
_name = "mail.test.track.groups"
_inherit = ['mail.thread']
name = fields.Char(tracking=1)
partner_id = fields.Many2one('res.partner', tracking=2, groups="base.group_user")
secret = fields.Char(tracking=3, groups="base.group_user")
class MailTestTrackMonetary(models.Model):
_name = 'mail.test.track.monetary'
_description = 'Test tracking monetary field'
_name = "mail.test.track.monetary"
_inherit = ['mail.thread']
company_id = fields.Many2one('res.company')
company_currency = fields.Many2one("res.currency", string='Currency', related='company_id.currency_id', readonly=True, tracking=True)
revenue = fields.Monetary('Revenue', currency_field='company_currency', tracking=True)
class MailTestMultiCompanyWithActivity(models.Model):
""" This model can be used in multi company tests with activity"""
_name = "mail.test.multi.company.with.activity"
_description = "Test Multi Company Mail With Activity"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char()
company_id = fields.Many2one("res.company")
class MailTestSelectionTracking(models.Model):
class MailTestTrackSelection(models.Model):
""" Test tracking for selection fields """
_description = 'Test Selection Tracking'
_name = 'mail.test.track.selection'
_name = "mail.test.track.selection"
_inherit = ['mail.thread']
name = fields.Char()
selection_type = fields.Selection([('first', 'First'), ('second', 'Second')], tracking=True)
class MailTestTrackAll(models.Model):
_name = 'mail.test.track.all'
_description = 'Test tracking on all field types'
_inherit = ['mail.thread']
boolean_field = fields.Boolean('Boolean', tracking=True)
char_field = fields.Char('Char', tracking=True)
company_id = fields.Many2one('res.company')
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
date_field = fields.Date('Date', tracking=True)
datetime_field = fields.Datetime('Datetime', tracking=True)
float_field = fields.Float('Float', tracking=True)
html_field = fields.Html('Html', tracking=True)
integer_field = fields.Integer('Integer', tracking=True)
many2one_field_id = fields.Many2one('res.partner', string='Many2one', tracking=True)
monetary_field = fields.Monetary('Monetary', tracking=True)
selection_field = fields.Selection(string='Selection', selection=[['first', 'FIRST']], tracking=True)
text_field = fields.Text('Text', tracking=True)
# ------------------------------------------------------------
# OTHER
# ------------------------------------------------------------
class MailTestMultiCompany(models.Model):
""" This model can be used in multi company tests"""
_name = 'mail.test.multi.company'
""" This model can be used in multi company tests, with attachments support
for checking record update in MC """
_description = "Test Multi Company Mail"
_inherit = 'mail.thread'
_name = "mail.test.multi.company"
_inherit = ['mail.thread.main.attachment']
name = fields.Char()
company_id = fields.Many2one('res.company')
@ -168,16 +233,29 @@ class MailTestMultiCompanyRead(models.Model):
even if the user has no write access. """
_description = 'Simple Chatter Model '
_name = 'mail.test.multi.company.read'
_inherit = ['mail.test.multi.company']
_inherit = ['mail.test.multi.company', 'mail.activity.mixin']
_mail_post_access = 'read'
class MailTestNotMailThread(models.Model):
class MailTestMultiCompanyWithActivity(models.Model):
""" This model can be used in multi company tests with activity"""
_description = "Test Multi Company Mail With Activity"
_name = "mail.test.multi.company.with.activity"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char()
company_id = fields.Many2one("res.company")
class MailTestNothread(models.Model):
""" Models not inheriting from mail.thread but using some cross models
capabilities of mail. """
_name = 'mail.test.nothread'
_description = "NoThread Model"
_name = "mail.test.nothread"
name = fields.Char()
company_id = fields.Many2one('res.company')
customer_id = fields.Many2one('res.partner')
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']

View file

@ -0,0 +1,95 @@
from odoo import api, fields, models
from odoo.fields import Domain
# ------------------------------------------------------------
# RECIPIENTS
# ------------------------------------------------------------
class MailTestRecipients(models.Model):
_name = 'mail.test.recipients'
_description = "Test Recipients Computation"
_inherit = ['mail.thread.cc']
_primary_email = 'customer_email'
company_id = fields.Many2one('res.company')
contact_ids = fields.Many2many('res.partner')
customer_id = fields.Many2one('res.partner')
customer_email = fields.Char('Customer Email', compute='_compute_customer_email', readonly=False, store=True)
customer_phone = fields.Char('Customer Phone', compute='_compute_customer_phone', readonly=False, store=True)
name = fields.Char()
@api.depends('customer_id')
def _compute_customer_email(self):
for source in self.filtered(lambda r: r.customer_id and not r.customer_email):
source.customer_email = source.customer_id.email_formatted
@api.depends('customer_id')
def _compute_customer_phone(self):
for source in self.filtered(lambda r: r.customer_id and not r.customer_phone):
source.customer_phone = source.customer_id.phone
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id', 'contact_ids']
class MailTestThreadCustomer(models.Model):
_name = 'mail.test.thread.customer'
_description = "Test Customer Thread Model"
_inherit = ['mail.test.recipients']
_mail_thread_customer = True
_primary_email = 'customer_email'
# ------------------------------------------------------------
# PROPERTIES
# ------------------------------------------------------------
class MailTestProperties(models.Model):
_name = 'mail.test.properties'
_description = 'Mail Test Properties'
_inherit = ['mail.thread']
name = fields.Char('Name')
parent_id = fields.Many2one('mail.test.properties', string='Parent')
properties = fields.Properties('Properties', definition='parent_id.definition_properties')
definition_properties = fields.PropertiesDefinition('Definitions')
# ------------------------------------------------------------
# ROTTING RESOURCES
# ------------------------------------------------------------
class MailTestStageField(models.Model):
_description = 'Fake model to be a stage to help test rotting implementation'
_name = 'mail.test.rotting.stage'
name = fields.Char()
rotting_threshold_days = fields.Integer(default=3)
no_rot = fields.Boolean(default=False)
class MailTestRottingMixin(models.Model):
_description = 'Fake model to test the rotting part of the mixin mail.thread.tracking.duration.mixin'
_name = 'mail.test.rotting.resource'
_track_duration_field = 'stage_id'
_inherit = ['mail.tracking.duration.mixin']
name = fields.Char()
date_last_stage_update = fields.Datetime(
'Last Stage Update', compute='_compute_date_last_stage_update', index=True, readonly=True, store=True)
stage_id = fields.Many2one('mail.test.rotting.stage', 'Stage')
done = fields.Boolean(default=False)
def _get_rotting_depends_fields(self):
return super()._get_rotting_depends_fields() + ['done', 'stage_id.no_rot']
def _get_rotting_domain(self):
return super()._get_rotting_domain() & Domain([
('done', '=', False),
('stage_id.no_rot', '=', False),
])
@api.depends('stage_id')
def _compute_date_last_stage_update(self):
self.date_last_stage_update = fields.Datetime.now()

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
@ -8,29 +5,93 @@ class MailTestSimple(models.Model):
""" A very simple model only inheriting from mail.thread when only
communication history is necessary. """
_description = 'Simple Chatter Model'
_name = 'mail.test.simple'
_name = "mail.test.simple"
_inherit = ['mail.thread']
name = fields.Char()
email_from = fields.Char()
def _message_compute_subject(self):
""" To ease mocks """
_a = super()._message_compute_subject()
return _a
def _notify_by_email_get_final_mail_values(self, *args, **kwargs):
""" To ease mocks """
_a = super()._notify_by_email_get_final_mail_values(*args, **kwargs)
return _a
def _notify_by_email_get_headers(self, headers=None):
headers = super()._notify_by_email_get_headers(headers=headers)
headers['X-Custom'] = 'Done'
return headers
class MailTestSimpleUnnamed(models.Model):
""" A very simple model only inheriting from mail.thread when only
communication history is necessary, and has no 'name' field """
_description = 'Simple Chatter Model without "name" field'
_name = 'mail.test.simple.unnamed'
_inherit = ['mail.thread']
_rec_name = "description"
description = fields.Char()
class MailTestSimpleMainAttachment(models.Model):
_description = 'Simple Chatter Model With Main Attachment Management'
_name = "mail.test.simple.main.attachment"
_inherit = ['mail.test.simple', 'mail.thread.main.attachment']
class MailTestSimpleUnfollow(models.Model):
""" A very simple model only inheriting from mail.thread when only
communication history is necessary with unfollow link enabled in
notification emails even for non-internal user. """
_description = 'Simple Chatter Model'
_name = "mail.test.simple.unfollow"
_inherit = ['mail.thread']
_partner_unfollow_enabled = True
name = fields.Char()
company_id = fields.Many2one('res.company')
email_from = fields.Char()
class MailTestAliasOptional(models.Model):
""" A chatter model inheriting from the alias mixin using optional alias_id
field, hence no inherits. """
_description = 'Chatter Model using Optional Alias Mixin'
_name = "mail.test.alias.optional"
_inherit = ['mail.alias.mixin.optional']
name = fields.Char()
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
email_from = fields.Char()
def _alias_get_creation_values(self):
""" Updates itself """
values = super()._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get_id('mail.test.alias.optional')
if self.id:
values['alias_force_thread_id'] = self.id
values['alias_defaults'] = {'company_id': self.company_id.id}
return values
class MailTestGateway(models.Model):
""" A very simple model only inheriting from mail.thread to test pure mass
mailing features and base performances. """
_description = 'Simple Chatter Model for Mail Gateway'
_name = 'mail.test.gateway'
_name = "mail.test.gateway"
_inherit = ['mail.thread.blacklist']
_primary_email = 'email_from'
name = fields.Char()
email_from = fields.Char()
custom_field = fields.Char()
user_id = fields.Many2one('res.users', 'Responsible')
@api.model
def message_new(self, msg_dict, custom_values=None):
""" Check override of 'message_new' allowing to update record values
base on incoming email. """
defaults = {
'email_from': msg_dict.get('from'),
}
@ -38,11 +99,32 @@ class MailTestGateway(models.Model):
return super().message_new(msg_dict, custom_values=defaults)
class MailTestGatewayCompany(models.Model):
""" A very simple model only inheriting from mail.thread to test pure mass
mailing features and base performances, with a company field. """
_description = 'Simple Chatter Model for Mail Gateway with company'
_name = "mail.test.gateway.company"
_inherit = ['mail.test.gateway']
company_id = fields.Many2one('res.company', 'Company')
class MailTestGatewayMainAttachment(models.Model):
""" A very simple model only inheriting from mail.thread to test pure mass
mailing features and base performances, with a company field and main
attachment management. """
_description = 'Simple Chatter Model for Mail Gateway with company'
_name = "mail.test.gateway.main.attachment"
_inherit = ['mail.test.gateway', 'mail.thread.main.attachment']
company_id = fields.Many2one('res.company', 'Company')
class MailTestGatewayGroups(models.Model):
""" A model looking like discussion channels / groups (flat thread and
alias). Used notably for advanced gatewxay tests. """
_description = 'Channel/Group-like Chatter Model for Mail Gateway'
_name = 'mail.test.gateway.groups'
_name = "mail.test.gateway.groups"
_inherit = ['mail.thread.blacklist', 'mail.alias.mixin']
_mail_flat_thread = False
_primary_email = 'email_from'
@ -60,25 +142,15 @@ class MailTestGatewayGroups(models.Model):
values['alias_parent_thread_id'] = self.id
return values
def _mail_get_partner_fields(self):
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']
def _message_get_default_recipients(self):
return dict(
(record.id, {
'email_cc': False,
'email_to': record.email_from if not record.customer_id.ids else False,
'partner_ids': record.customer_id.ids,
})
for record in self
)
class MailTestStandard(models.Model):
class MailTestTrack(models.Model):
""" This model can be used in tests when automatic subscription and simple
tracking is necessary. Most features are present in a simple way. """
_description = 'Standard Chatter Model'
_name = 'mail.test.track'
_name = "mail.test.track"
_inherit = ['mail.thread']
name = fields.Char()
@ -86,24 +158,40 @@ class MailTestStandard(models.Model):
user_id = fields.Many2one('res.users', 'Responsible', tracking=True)
container_id = fields.Many2one('mail.test.container', tracking=True)
company_id = fields.Many2one('res.company')
track_fields_tofilter = fields.Char() # comma-separated list of field names
track_enable_default_log = fields.Boolean(default=False)
parent_id = fields.Many2one('mail.test.track', string='Parent')
def _track_filter_for_display(self, tracking_values):
values = super()._track_filter_for_display(tracking_values)
filtered_fields = set(self.track_fields_tofilter.split(',') if self.track_fields_tofilter else '')
return values.filtered(lambda val: val.field_id.name not in filtered_fields)
def _track_get_default_log_message(self, changes):
filtered_fields = set(self.track_fields_tofilter.split(',') if self.track_fields_tofilter else '')
if self.track_enable_default_log and not all(change in filtered_fields for change in changes):
return f'There was a change on {self.name} for fields "{",".join(changes)}"'
return super()._track_get_default_log_message(changes)
class MailTestActivity(models.Model):
""" This model can be used to test activities in addition to simple chatter
features. """
_description = 'Activity Model'
_name = 'mail.test.activity'
_name = "mail.test.activity"
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char()
date = fields.Date()
email_from = fields.Char()
active = fields.Boolean(default=True)
company_id = fields.Many2one('res.company')
def action_start(self, action_summary):
return self.activity_schedule(
'test_mail.mail_act_test_todo',
summary=action_summary
summary=action_summary,
user_id=self.env.uid,
)
def action_close(self, action_feedback, attachment_ids=None):
@ -112,196 +200,18 @@ class MailTestActivity(models.Model):
attachment_ids=attachment_ids)
class MailTestTicket(models.Model):
""" This model can be used in tests when complex chatter features are
required like modeling tasks or tickets. """
_description = 'Ticket-like model'
_name = 'mail.test.ticket'
_inherit = ['mail.thread']
_primary_email = 'email_from'
name = fields.Char()
email_from = fields.Char(tracking=True)
count = fields.Integer(default=1)
datetime = fields.Datetime(default=fields.Datetime.now)
mail_template = fields.Many2one('mail.template', 'Template')
customer_id = fields.Many2one('res.partner', 'Customer', tracking=2)
user_id = fields.Many2one('res.users', 'Responsible', tracking=1)
container_id = fields.Many2one('mail.test.container', tracking=True)
def _mail_get_partner_fields(self):
return ['customer_id']
def _message_get_default_recipients(self):
return dict(
(record.id, {
'email_cc': False,
'email_to': record.email_from if not record.customer_id.ids else False,
'partner_ids': record.customer_id.ids,
})
for record in self
)
def _notify_get_recipients_groups(self, msg_vals=None):
""" Activate more groups to test query counters notably (and be backward
compatible for tests). """
local_msg_vals = dict(msg_vals or {})
groups = super()._notify_get_recipients_groups(msg_vals=msg_vals)
for group_name, _group_method, group_data in groups:
if group_name == 'portal':
group_data['active'] = True
elif group_name == 'customer':
group_data['active'] = True
group_data['has_button_access'] = True
group_data['actions'] = [{
'url': self._notify_get_action_link(
'controller',
controller='/test_mail/do_stuff',
**local_msg_vals
),
'title': _('NotificationButtonTitle')
}]
return groups
def _track_template(self, changes):
res = super(MailTestTicket, self)._track_template(changes)
record = self[0]
if 'customer_id' in changes and record.mail_template:
res['customer_id'] = (record.mail_template, {'composition_mode': 'mass_mail'})
elif 'datetime' in changes:
res['datetime'] = ('test_mail.mail_test_ticket_tracking_view', {'composition_mode': 'mass_mail'})
return res
def _creation_subtype(self):
if self.container_id:
return self.env.ref('test_mail.st_mail_test_ticket_container_upd')
return super(MailTestTicket, self)._creation_subtype()
def _track_subtype(self, init_values):
self.ensure_one()
if 'container_id' in init_values and self.container_id:
return self.env.ref('test_mail.st_mail_test_ticket_container_upd')
return super(MailTestTicket, self)._track_subtype(init_values)
class MailTestTicketEL(models.Model):
""" Just mail.test.ticket, but exclusion-list enabled. Kept as different
model to avoid messing with existing tests, notably performance, and ease
backward comparison. """
_description = 'Ticket-like model with exclusion list'
_name = 'mail.test.ticket.el'
_inherit = [
'mail.test.ticket',
'mail.thread.blacklist',
]
_primary_email = 'email_from'
email_from = fields.Char(
'Email',
compute='_compute_email_from', readonly=False, store=True)
@api.depends('customer_id')
def _compute_email_from(self):
for ticket in self.filtered(lambda r: r.customer_id and not r.email_from):
ticket.email_from = ticket.customer_id.email_formatted
class MailTestTicketMC(models.Model):
""" Just mail.test.ticket, but multi company. Kept as different model to
avoid messing with existing tests, notably performance, and ease backward
comparison. """
_description = 'Ticket-like model'
_name = 'mail.test.ticket.mc'
_inherit = ['mail.test.ticket']
_primary_email = 'email_from'
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
container_id = fields.Many2one('mail.test.container.mc', tracking=True)
def _creation_subtype(self):
if self.container_id:
return self.env.ref('test_mail.st_mail_test_ticket_container_mc_upd')
return super()._creation_subtype()
def _track_subtype(self, init_values):
self.ensure_one()
if 'container_id' in init_values and self.container_id:
return self.env.ref('test_mail.st_mail_test_ticket_container_mc_upd')
return super()._track_subtype(init_values)
class MailTestContainer(models.Model):
""" This model can be used in tests when container records like projects
or teams are required. """
_description = 'Project-like model with alias'
_name = 'mail.test.container'
_mail_post_access = 'read'
_inherit = ['mail.thread', 'mail.alias.mixin']
name = fields.Char()
description = fields.Text()
customer_id = fields.Many2one('res.partner', 'Customer')
alias_id = fields.Many2one(
'mail.alias', 'Alias',
delegate=True)
def _mail_get_partner_fields(self):
return ['customer_id']
def _message_get_default_recipients(self):
return dict(
(record.id, {
'email_cc': False,
'email_to': False,
'partner_ids': record.customer_id.ids,
})
for record in self
)
def _notify_get_recipients_groups(self, msg_vals=None):
""" Activate more groups to test query counters notably (and be backward
compatible for tests). """
groups = super(MailTestContainer, self)._notify_get_recipients_groups(msg_vals=msg_vals)
for group_name, _group_method, group_data in groups:
if group_name == 'portal':
group_data['active'] = True
return groups
def _alias_get_creation_values(self):
values = super(MailTestContainer, self)._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('mail.test.container').id
if self.id:
values['alias_force_thread_id'] = self.id
values['alias_parent_thread_id'] = self.id
return values
class MailTestContainerMC(models.Model):
""" Just mail.test.container, but multi company. Kept as different model to
avoid messing with existing tests, notably performance, and ease backward
comparison. """
_description = 'Project-like model with alias (MC)'
_name = 'mail.test.container.mc'
_mail_post_access = 'read'
_inherit = ['mail.test.container']
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
class MailTestComposerMixin(models.Model):
""" A simple invite-like wizard using the composer mixin, rendering on
composer source test model. Purpose is to have a minimal composer which
runs on other records and check notably dynamic template support and
translations. """
_description = 'Invite-like Wizard'
_name = 'mail.test.composer.mixin'
_name = "mail.test.composer.mixin"
_inherit = ['mail.composer.mixin']
name = fields.Char('Name')
author_id = fields.Many2one('res.partner')
description = fields.Html('Description', render_engine="qweb", render_options={"post_process": True}, sanitize=False)
description = fields.Html('Description', render_engine="qweb", render_options={"post_process": True}, sanitize='email_outgoing')
source_ids = fields.Many2many('mail.test.composer.source', string='Invite source')
def _compute_render_model(self):
@ -310,8 +220,8 @@ class MailTestComposerMixin(models.Model):
class MailTestComposerSource(models.Model):
""" A simple model on which invites are sent. """
_description = 'Invite-like Wizard'
_name = 'mail.test.composer.source'
_description = 'Invite-like Source'
_name = "mail.test.composer.source"
_inherit = ['mail.thread.blacklist']
_primary_email = 'email_from'
@ -326,5 +236,5 @@ class MailTestComposerSource(models.Model):
for source in self.filtered(lambda r: r.customer_id and not r.email_from):
source.email_from = source.customer_id.email_formatted
def _mail_get_partner_fields(self):
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']

View file

@ -4,7 +4,7 @@
from odoo import api, fields, models
class MailTestCC(models.Model):
class MailTestCc(models.Model):
_name = 'mail.test.cc'
_description = "Test Email CC Thread"
_inherit = ['mail.thread.cc']

View file

@ -1,15 +1,29 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mail_performance_thread,access_mail_performance_thread,model_mail_performance_thread,,1,1,1,1
access_mail_performance_thread,access_mail_performance_thread,model_mail_performance_thread,base.group_user,1,1,1,1
access_mail_performance_thread_recipients,access_mail_performance_thread_recipients,model_mail_performance_thread_recipients,base.group_user,1,1,1,1
access_mail_performance_tracking_user,mail.performance.tracking,model_mail_performance_tracking,base.group_user,1,1,1,1
access_mail_test_access_portal,mail.access.portal.portal,model_mail_test_access,base.group_portal,1,1,0,0
access_mail_test_access_public,mail.access.portal.public,model_mail_test_access,base.group_public,1,0,0,0
access_mail_test_access_user,mail.access.portal.user,model_mail_test_access,base.group_user,1,1,1,1
access_mail_test_access_custo_portal,mail.access.portal.portal,model_mail_test_access_custo,base.group_portal,1,0,0,0
access_mail_test_access_custo_user,mail.access.portal.user,model_mail_test_access_custo,base.group_user,1,1,1,1
access_mail_test_access_public_public,mail.test.access.public.public,model_mail_test_access_public,base.group_public,1,1,0,0
access_mail_test_access_public_portal,mail.test.access.public.portal,model_mail_test_access_public,base.group_portal,1,1,0,0
access_mail_test_access_public_user,mail.test.access.public.user,model_mail_test_access_public,base.group_user,1,1,1,1
access_mail_test_alias_optional_portal,mail.test.alias.optional.portal,model_mail_test_alias_optional,base.group_portal,1,0,0,0
access_mail_test_alias_optional_user,mail.test.alias.optional.user,model_mail_test_alias_optional,base.group_user,1,1,1,1
access_mail_test_simple_portal,mail.test.simple.portal,model_mail_test_simple,base.group_portal,1,0,0,0
access_mail_test_simple_user,mail.test.simple.user,model_mail_test_simple,base.group_user,1,1,1,1
access_mail_test_simple_unnamed_portal,mail.test.simple.unnamed.portal,model_mail_test_simple_unnamed,base.group_portal,1,0,0,0
access_mail_test_simple_unnamed_user,mail.test.simple.unnamed.user,model_mail_test_simple_unnamed,base.group_user,1,1,1,1
access_mail_test_simple_unfollow_portal,mail.test.simple.unfollow.portal,model_mail_test_simple_unfollow,base.group_portal,0,0,0,0
access_mail_test_simple_unfollow_user,mail.test.simple.unfollow.user,model_mail_test_simple_unfollow,base.group_user,1,1,1,1
access_mail_test_simple_main_attachment_portal,mail.test.simple.main.attachment.portal,model_mail_test_simple_main_attachment,base.group_portal,1,0,0,0
access_mail_test_simple_main_attachment_user,mail.test.simple.main.attachment.user,model_mail_test_simple_main_attachment,base.group_user,1,1,1,1
access_mail_test_gateway_portal,mail.test.gateway.portal,model_mail_test_gateway,base.group_portal,1,0,0,0
access_mail_test_gateway_user,mail.test.gateway.user,model_mail_test_gateway,base.group_user,1,1,1,1
access_mail_test_gateway_company_user,mail.test.gateway.company.user,model_mail_test_gateway_company,base.group_user,1,1,1,1
access_mail_test_gateway_main_attachment_user,mail.test.gateway.main.attachment.user,model_mail_test_gateway_main_attachment,base.group_user,1,1,1,1
access_mail_test_gateway_groups_portal,mail.test.gateway.groups.portal,model_mail_test_gateway_groups,base.group_portal,1,0,0,0
access_mail_test_gateway_groups_user,mail.test.gateway.groups.user,model_mail_test_gateway_groups,base.group_user,1,1,1,1
access_mail_test_track_portal,mail.test.track.portal,model_mail_test_track,base.group_portal,0,0,0,0
@ -18,15 +32,18 @@ access_mail_test_activity_portal,mail.test.activity.portal,model_mail_test_activ
access_mail_test_activity_user,mail.test.activity.user,model_mail_test_activity,base.group_user,1,1,1,1
access_mail_test_field_type_portal,mail.test.field.type.portal,model_mail_test_field_type,base.group_portal,0,0,0,0
access_mail_test_field_type_user,mail.test.field.type.user,model_mail_test_field_type,base.group_user,1,1,1,1
access_mail_test_lead_user,mail.test.lead.user,model_mail_test_lead,base.group_user,1,1,1,1
access_mail_test_ticket_portal,mail.test.ticket.portal,model_mail_test_ticket,base.group_portal,1,0,0,0
access_mail_test_ticket_user,mail.test.ticket.user,model_mail_test_ticket,base.group_user,1,1,1,1
access_mail_test_ticket_el_portal,mail.test.ticket.el.portal,model_mail_test_ticket_el,base.group_portal,1,0,0,0
access_mail_test_ticket_el_user,mail.test.ticket.el.user,model_mail_test_ticket_el,base.group_user,1,1,1,1
access_mail_test_ticket_mc_portal,mail.test.ticket.mc.portal,model_mail_test_ticket_mc,base.group_portal,1,0,0,0
access_mail_test_ticket_mc_user,mail.test.ticket.mc.user,model_mail_test_ticket_mc,base.group_user,1,1,1,1
access_mail_test_ticket_partner_portal,mail.test.ticket.partner.portal,model_mail_test_ticket_partner,base.group_portal,1,0,0,0
access_mail_test_ticket_partner_user,mail.test.ticket.partner.user,model_mail_test_ticket_partner,base.group_user,1,1,1,1
access_mail_test_composer_mixin_all,mail.test.composer.mixin.all,model_mail_test_composer_mixin,,0,0,0,0
access_mail_test_composer_mixin_user,mail.test.composer.mixin.user,model_mail_test_composer_mixin,base.group_user,1,1,1,1
access_mail_test_composer_source_all,mail.test.composer.source.all,model_mail_test_composer_source,,1,0,0,0
access_mail_test_composer_source_all,mail.test.composer.source.all,model_mail_test_composer_source,base.group_user,1,0,0,0
access_mail_test_composer_source_user,mail.test.composer.source.user,model_mail_test_composer_source,base.group_user,1,1,1,1
access_mail_test_container_portal,mail.test.container_portal,model_mail_test_container,base.group_portal,1,0,0,0
access_mail_test_container_user,mail.test.container.user,model_mail_test_container,base.group_user,1,1,1,1
@ -44,8 +61,18 @@ access_mail_test_multi_company_with_activity_user,mail.test.multi.company.with.a
access_mail_test_multi_company_with_activity_portal,mail.test.multi.company.with.activity.portal,model_mail_test_multi_company_with_activity,base.group_portal,1,0,0,0
access_mail_test_nothread_user,mail.test.nothread.user,model_mail_test_nothread,base.group_user,1,1,1,1
access_mail_test_nothread_portal,mail.test.nothread.portal,model_mail_test_nothread,base.group_portal,1,0,0,0
access_mail_test_recipients_user,mail.test.recipients.user,model_mail_test_recipients,base.group_user,1,1,1,1
access_mail_test_rotting_resource,mail.test.rotting.resource,model_mail_test_rotting_resource,base.group_user,1,1,1,1
access_mail_test_rotting_stage,mail.test.rotting.stage,model_mail_test_rotting_stage,base.group_user,1,1,1,1
access_mail_test_thread_customer_user,mail.test.thread.customer.user,model_mail_test_thread_customer,base.group_user,1,1,1,1
access_mail_test_track_all,mail.test.track.all,model_mail_test_track_all,base.group_user,1,1,1,1
access_mail_test_track_all_properties_parent,access_mail_test_track_all_properties_parent,model_mail_test_track_all_properties_parent,base.group_user,1,0,0,0
access_mail_test_track_all_m2m,mail.test.track.all.m2m,model_mail_test_track_all_m2m,base.group_user,1,1,1,1
access_mail_test_track_all_o2m,mail.test.track.all.o2m,model_mail_test_track_all_o2m,base.group_user,1,1,1,1
access_mail_test_track_compute,mail.test.track.compute,model_mail_test_track_compute,base.group_user,1,1,1,1
access_mail_test_track_groups,mail.test.track.groups,model_mail_test_track_groups,base.group_user,1,1,1,1
access_mail_test_track_monetary,mail.test.track.monetary,model_mail_test_track_monetary,base.group_user,1,1,1,1
access_mail_test_track_selection_portal,mail.test.track.selection.portal,model_mail_test_track_selection,base.group_portal,0,0,0,0
access_mail_test_track_selection_user,mail.test.track.selection.user,model_mail_test_track_selection,base.group_user,1,1,1,1
access_mail_test_properties_user,mail.test.properties.user,model_mail_test_properties,base.group_user,1,1,1,1
access_mail_test_track_duration_mixin,mail.test.track.duration.mixin,model_mail_test_track_duration_mixin,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_mail_performance_thread access_mail_performance_thread model_mail_performance_thread base.group_user 1 1 1 1
3 access_mail_performance_thread_recipients access_mail_performance_thread_recipients model_mail_performance_thread_recipients base.group_user 1 1 1 1
4 access_mail_performance_tracking_user mail.performance.tracking model_mail_performance_tracking base.group_user 1 1 1 1
5 access_mail_test_access_portal mail.access.portal.portal model_mail_test_access base.group_portal 1 1 0 0
6 access_mail_test_access_public mail.access.portal.public model_mail_test_access base.group_public 1 0 0 0
7 access_mail_test_access_user mail.access.portal.user model_mail_test_access base.group_user 1 1 1 1
8 access_mail_test_access_custo_portal mail.access.portal.portal model_mail_test_access_custo base.group_portal 1 0 0 0
9 access_mail_test_access_custo_user mail.access.portal.user model_mail_test_access_custo base.group_user 1 1 1 1
10 access_mail_test_access_public_public mail.test.access.public.public model_mail_test_access_public base.group_public 1 1 0 0
11 access_mail_test_access_public_portal mail.test.access.public.portal model_mail_test_access_public base.group_portal 1 1 0 0
12 access_mail_test_access_public_user mail.test.access.public.user model_mail_test_access_public base.group_user 1 1 1 1
13 access_mail_test_alias_optional_portal mail.test.alias.optional.portal model_mail_test_alias_optional base.group_portal 1 0 0 0
14 access_mail_test_alias_optional_user mail.test.alias.optional.user model_mail_test_alias_optional base.group_user 1 1 1 1
15 access_mail_test_simple_portal mail.test.simple.portal model_mail_test_simple base.group_portal 1 0 0 0
16 access_mail_test_simple_user mail.test.simple.user model_mail_test_simple base.group_user 1 1 1 1
17 access_mail_test_simple_unnamed_portal mail.test.simple.unnamed.portal model_mail_test_simple_unnamed base.group_portal 1 0 0 0
18 access_mail_test_simple_unnamed_user mail.test.simple.unnamed.user model_mail_test_simple_unnamed base.group_user 1 1 1 1
19 access_mail_test_simple_unfollow_portal mail.test.simple.unfollow.portal model_mail_test_simple_unfollow base.group_portal 0 0 0 0
20 access_mail_test_simple_unfollow_user mail.test.simple.unfollow.user model_mail_test_simple_unfollow base.group_user 1 1 1 1
21 access_mail_test_simple_main_attachment_portal mail.test.simple.main.attachment.portal model_mail_test_simple_main_attachment base.group_portal 1 0 0 0
22 access_mail_test_simple_main_attachment_user mail.test.simple.main.attachment.user model_mail_test_simple_main_attachment base.group_user 1 1 1 1
23 access_mail_test_gateway_portal mail.test.gateway.portal model_mail_test_gateway base.group_portal 1 0 0 0
24 access_mail_test_gateway_user mail.test.gateway.user model_mail_test_gateway base.group_user 1 1 1 1
25 access_mail_test_gateway_company_user mail.test.gateway.company.user model_mail_test_gateway_company base.group_user 1 1 1 1
26 access_mail_test_gateway_main_attachment_user mail.test.gateway.main.attachment.user model_mail_test_gateway_main_attachment base.group_user 1 1 1 1
27 access_mail_test_gateway_groups_portal mail.test.gateway.groups.portal model_mail_test_gateway_groups base.group_portal 1 0 0 0
28 access_mail_test_gateway_groups_user mail.test.gateway.groups.user model_mail_test_gateway_groups base.group_user 1 1 1 1
29 access_mail_test_track_portal mail.test.track.portal model_mail_test_track base.group_portal 0 0 0 0
32 access_mail_test_activity_user mail.test.activity.user model_mail_test_activity base.group_user 1 1 1 1
33 access_mail_test_field_type_portal mail.test.field.type.portal model_mail_test_field_type base.group_portal 0 0 0 0
34 access_mail_test_field_type_user mail.test.field.type.user model_mail_test_field_type base.group_user 1 1 1 1
35 access_mail_test_lead_user mail.test.lead.user model_mail_test_lead base.group_user 1 1 1 1
36 access_mail_test_ticket_portal mail.test.ticket.portal model_mail_test_ticket base.group_portal 1 0 0 0
37 access_mail_test_ticket_user mail.test.ticket.user model_mail_test_ticket base.group_user 1 1 1 1
38 access_mail_test_ticket_el_portal mail.test.ticket.el.portal model_mail_test_ticket_el base.group_portal 1 0 0 0
39 access_mail_test_ticket_el_user mail.test.ticket.el.user model_mail_test_ticket_el base.group_user 1 1 1 1
40 access_mail_test_ticket_mc_portal mail.test.ticket.mc.portal model_mail_test_ticket_mc base.group_portal 1 0 0 0
41 access_mail_test_ticket_mc_user mail.test.ticket.mc.user model_mail_test_ticket_mc base.group_user 1 1 1 1
42 access_mail_test_ticket_partner_portal mail.test.ticket.partner.portal model_mail_test_ticket_partner base.group_portal 1 0 0 0
43 access_mail_test_ticket_partner_user mail.test.ticket.partner.user model_mail_test_ticket_partner base.group_user 1 1 1 1
44 access_mail_test_composer_mixin_all mail.test.composer.mixin.all model_mail_test_composer_mixin 0 0 0 0
45 access_mail_test_composer_mixin_user mail.test.composer.mixin.user model_mail_test_composer_mixin base.group_user 1 1 1 1
46 access_mail_test_composer_source_all mail.test.composer.source.all model_mail_test_composer_source base.group_user 1 0 0 0
47 access_mail_test_composer_source_user mail.test.composer.source.user model_mail_test_composer_source base.group_user 1 1 1 1
48 access_mail_test_container_portal mail.test.container_portal model_mail_test_container base.group_portal 1 0 0 0
49 access_mail_test_container_user mail.test.container.user model_mail_test_container base.group_user 1 1 1 1
61 access_mail_test_multi_company_with_activity_portal mail.test.multi.company.with.activity.portal model_mail_test_multi_company_with_activity base.group_portal 1 0 0 0
62 access_mail_test_nothread_user mail.test.nothread.user model_mail_test_nothread base.group_user 1 1 1 1
63 access_mail_test_nothread_portal mail.test.nothread.portal model_mail_test_nothread base.group_portal 1 0 0 0
64 access_mail_test_recipients_user mail.test.recipients.user model_mail_test_recipients base.group_user 1 1 1 1
65 access_mail_test_rotting_resource mail.test.rotting.resource model_mail_test_rotting_resource base.group_user 1 1 1 1
66 access_mail_test_rotting_stage mail.test.rotting.stage model_mail_test_rotting_stage base.group_user 1 1 1 1
67 access_mail_test_thread_customer_user mail.test.thread.customer.user model_mail_test_thread_customer base.group_user 1 1 1 1
68 access_mail_test_track_all mail.test.track.all model_mail_test_track_all base.group_user 1 1 1 1
69 access_mail_test_track_all_properties_parent access_mail_test_track_all_properties_parent model_mail_test_track_all_properties_parent base.group_user 1 0 0 0
70 access_mail_test_track_all_m2m mail.test.track.all.m2m model_mail_test_track_all_m2m base.group_user 1 1 1 1
71 access_mail_test_track_all_o2m mail.test.track.all.o2m model_mail_test_track_all_o2m base.group_user 1 1 1 1
72 access_mail_test_track_compute mail.test.track.compute model_mail_test_track_compute base.group_user 1 1 1 1
73 access_mail_test_track_groups mail.test.track.groups model_mail_test_track_groups base.group_user 1 1 1 1
74 access_mail_test_track_monetary mail.test.track.monetary model_mail_test_track_monetary base.group_user 1 1 1 1
75 access_mail_test_track_selection_portal mail.test.track.selection.portal model_mail_test_track_selection base.group_portal 0 0 0 0
76 access_mail_test_track_selection_user mail.test.track.selection.user model_mail_test_track_selection base.group_user 1 1 1 1
77 access_mail_test_properties_user mail.test.properties.user model_mail_test_properties base.group_user 1 1 1 1
78 access_mail_test_track_duration_mixin mail.test.track.duration.mixin model_mail_test_track_duration_mixin base.group_user 1 1 1 1

View file

@ -1,6 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Having rules triggers call to check_access_rules and allow to spot crashes
notably when records are unlinked. Without rule, method is not called and
some crashes are not trigerred in tests. -->
<record id="ir_rule_mail_test_simple_dummy" model="ir.rule">
<field name="name">Dummy rule, just to enable rule evaluation, shows some specific errors</field>
<field name="model_id" ref="test_mail.model_mail_test_simple"/>
<field name="domain_force">[('email_from', '!=', 'donotsetmewiththisvalue')]</field>
</record>
<!-- MAIL.TEST.ACCESS -->
<record id="ir_rule_mail_test_access_public" model="ir.rule">
<field name="name">Public: public only</field>
<field name="model_id" ref="test_mail.model_mail_test_access"/>
@ -55,21 +65,45 @@
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
</record>
<record id="mail_test_multi_company_rule" model="ir.rule">
<field name="name">Mail Test Multi Company</field>
<field name="model_id" ref="test_mail.model_mail_test_multi_company"/>
<field eval="True" name="global"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
<!-- MAIL.TEST.ACCESS.CUSTO -->
<record id="ir_rule_mail_test_access_custo_portal_read" model="ir.rule">
<field name="name">Portal: read unlocked</field>
<field name="model_id" ref="test_mail.model_mail_test_access_custo"/>
<field name="domain_force">[('is_locked', '=', False)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_mail_test_access_custo_update" model="ir.rule">
<field name="name">Internal: create/write/unlink unlocked and not readonly</field>
<field name="model_id" ref="test_mail.model_mail_test_access_custo"/>
<field name="domain_force">[('is_readonly', '=', False), ('is_locked', '=', False)]</field>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<record id="ir_rule_mail_test_access_custo_update_admin" model="ir.rule">
<field name="name">Admin: all</field>
<field name="model_id" ref="test_mail.model_mail_test_access_custo"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<!-- MAIL.TEST.MULTI.COMPANY(.*) -->
<record id="mail_test_multi_company_rule" model="ir.rule">
<field name="name">Mail Test Multi Company</field>
<field name="model_id" ref="test_mail.model_mail_test_multi_company"/>
<field eval="True" name="global"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<record id="mail_test_multi_company_read_rule" model="ir.rule">
<field name="name">MC Readonly Rule</field>
<field name="model_id" ref="test_mail.model_mail_test_multi_company_read"/>
@ -77,7 +111,6 @@
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
<field name="global" eval="True"/>
</record>
<record id="mail_test_multi_company_with_activity_rule" model="ir.rule">
<field name="name">Mail Test Multi Company With Activity</field>
<field name="model_id" ref="test_mail.model_mail_test_multi_company_with_activity"/>
@ -85,15 +118,13 @@
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<!-- TICKET-LIKE -->
<!-- MAIL.TEST.TICKET(.*) (TICKET-LIKE) -->
<record id="mail_test_ticket_rule_portal" model="ir.rule">
<field name="name">Portal Mail Test Ticket</field>
<field name="model_id" ref="test_mail.model_mail_test_ticket"/>
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<!-- MULTI COMPANY TICKET LIKE -->
<record id="mail_test_ticket_mc_rule" model="ir.rule">
<field name="name">Mail Test Ticket Multi Company</field>
<field name="model_id" ref="test_mail.model_mail_test_ticket_mc"/>
@ -106,16 +137,26 @@
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="mail_test_ticket_partner_rule" model="ir.rule">
<field name="name">Mail Test Ticket Multi Company Partner</field>
<field name="model_id" ref="test_mail.model_mail_test_ticket_partner"/>
<field name="global" eval="True"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<record id="mail_test_ticket_partner_rule_portal" model="ir.rule">
<field name="name">Portal Mail Test Ticket Multi Company Partner</field>
<field name="model_id" ref="test_mail.model_mail_test_ticket_partner"/>
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<!-- PROJECT-LIKE -->
<!-- MAIL.TEST.CONTAINER(.*) (PROJECT-LIKE) -->
<record id="mail_test_container_rule_portal" model="ir.rule">
<field name="name">Portal Mail Test Container</field>
<field name="model_id" ref="test_mail.model_mail_test_container"/>
<field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<!-- MULTI COMPANY PROJECT LIKE -->
<record id="mail_test_container_mc_rule" model="ir.rule">
<field name="name">Mail Test Container Multi Company</field>
<field name="model_id" ref="test_mail.model_mail_test_container_mc"/>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
import { describe, expect, test } from "@odoo/hoot";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { openView, start, startServer } from "@mail/../tests/mail_test_helpers";
describe.current.tags("mobile");
defineTestMailModels();
test("horizontal scroll applies only to the content, not to the whole controller", async () => {
const pyEnv = await startServer();
pyEnv["mail.activity.type"].create([
{ name: "Email" },
{ name: "Call" },
{ name: "Upload document" },
]);
await start();
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"]],
});
const o_view_controller = document.querySelector(".o_view_controller");
const o_content = o_view_controller.querySelector(".o_content");
const o_cp_item = document.querySelector(".o_breadcrumb .active");
const initialXCpItem = o_cp_item.getBoundingClientRect().x;
const o_header_cell = o_content.querySelector(".o_activity_type_cell");
const initialXHeaderCell = o_header_cell.getBoundingClientRect().x;
expect(o_view_controller).toHaveClass("o_action_delegate_scroll");
expect(o_view_controller).toHaveStyle({ overflow: "hidden" });
expect(o_content).toHaveStyle({ overflow: "auto" });
expect(o_content.scrollLeft).toBe(0);
o_content.scrollLeft = 100;
expect(o_content.scrollLeft).toBe(100);
expect(o_header_cell.getBoundingClientRect().x).toBeLessThan(initialXHeaderCell);
expect(o_cp_item).toHaveRect({ x: initialXCpItem });
});

View file

@ -1,676 +0,0 @@
/** @odoo-module **/
import ActivityRenderer from '@mail/js/views/activity/activity_renderer';
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import testUtils from 'web.test_utils';
import { click, insertText } from "@web/../tests/utils";
import { legacyExtraNextTick, patchWithCleanup} from "@web/../tests/helpers/utils";
import { doAction } from "@web/../tests/webclient/helpers";
import { session } from '@web/session';
let serverData;
let pyEnv;
QUnit.module('test_mail', {}, function () {
QUnit.module('activity view', {
async beforeEach() {
pyEnv = await startServer();
const mailTemplateIds = pyEnv['mail.template'].create([{ name: "Template1" }, { name: "Template2" }]);
// reset incompatible setup
pyEnv['mail.activity.type'].unlink(pyEnv['mail.activity.type'].search([]));
const mailActivityTypeIds = pyEnv['mail.activity.type'].create([
{ name: "Email", mail_template_ids: mailTemplateIds },
{ name: "Call" },
{ name: "Call for Demo" },
{ name: "To Do" },
]);
const resUsersId1 = pyEnv['res.users'].create({ display_name: 'first user' });
const mailActivityIds = pyEnv['mail.activity'].create([
{
display_name: "An activity",
date_deadline: moment().add(3, "days").format("YYYY-MM-DD"), // now
can_write: true,
state: "planned",
activity_type_id: mailActivityTypeIds[0],
mail_template_ids: mailTemplateIds,
user_id: resUsersId1,
},
{
display_name: "An activity",
date_deadline: moment().format("YYYY-MM-DD"), // now
can_write: true,
state: "today",
activity_type_id: mailActivityTypeIds[0],
mail_template_ids: mailTemplateIds,
user_id: resUsersId1,
},
{
res_model: 'mail.test.activity',
display_name: "An activity",
date_deadline: moment().subtract(2, "days").format("YYYY-MM-DD"), // now
can_write: true,
state: "overdue",
activity_type_id: mailActivityTypeIds[1],
user_id: resUsersId1,
},
]);
pyEnv['mail.test.activity'].create([
{ name: 'Meeting Room Furnitures', activity_ids: [mailActivityIds[0]] },
{ name: 'Office planning', activity_ids: [mailActivityIds[1], mailActivityIds[2]] },
]);
serverData = {
views: {
'mail.test.activity,false,activity':
'<activity string="MailTestActivity">' +
'<templates>' +
'<div t-name="activity-box">' +
'<field name="name"/>' +
'</div>' +
'</templates>' +
'</activity>',
'mail.test.activity,false,form':
'<form string="MailTestActivity">' +
'<field name="name"/>' +
'</form>',
},
};
}
});
var activityDateFormat = function (date) {
return date.toLocaleDateString(moment().locale(), { day: 'numeric', month: 'short' });
};
QUnit.test('activity view: simple activity rendering', async function (assert) {
assert.expect(15);
const mailTestActivityIds = pyEnv['mail.test.activity'].search([]);
const mailActivityTypeIds = pyEnv['mail.activity.type'].search([]);
const { click , env, openView } = await start({
serverData,
});
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"], [false, "form"]],
});
patchWithCleanup(env.services.action, {
doAction(action, options) {
assert.deepEqual(action, {
context: {
default_res_id: mailTestActivityIds[1],
default_res_model: "mail.test.activity",
default_activity_type_id: mailActivityTypeIds[2],
},
res_id: false,
res_model: "mail.activity",
target: "new",
type: "ir.actions.act_window",
view_mode: "form",
view_type: "form",
views: [[false, "form"]]
},
"should do a do_action with correct parameters");
options.onClose();
return Promise.resolve();
},
});
const $activity = $(document.querySelector('.o_activity_view'));
assert.containsOnce($activity, 'table',
'should have a table');
var $th1 = $activity.find('table thead tr:first th:nth-child(2)');
assert.containsOnce($th1, 'span:first:contains(Email)', 'should contain "Email" in header of first column');
assert.containsOnce($th1, '.o_legacy_kanban_counter', 'should contain a progressbar in header of first column');
assert.hasAttrValue($th1.find('.o_kanban_counter_progress .progress-bar:first'), 'data-bs-original-title', '1 Planned',
'the counter progressbars should be correctly displayed');
assert.hasAttrValue($th1.find('.o_kanban_counter_progress .progress-bar:nth-child(2)'), 'data-bs-original-title', '1 Today',
'the counter progressbars should be correctly displayed');
var $th2 = $activity.find('table thead tr:first th:nth-child(3)');
assert.containsOnce($th2, 'span:first:contains(Call)', 'should contain "Call" in header of second column');
assert.hasAttrValue($th2.find('.o_kanban_counter_progress .progress-bar:nth-child(3)'), 'data-bs-original-title', '1 Overdue',
'the counter progressbars should be correctly displayed');
assert.containsNone($activity, 'table thead tr:first th:nth-child(4) .o_kanban_counter',
'should not contain a progressbar in header of 3rd column');
assert.ok($activity.find('table tbody tr:first td:first:contains(Office planning)').length,
'should contain "Office planning" in first colum of first row');
assert.ok($activity.find('table tbody tr:nth-child(2) td:first:contains(Meeting Room Furnitures)').length,
'should contain "Meeting Room Furnitures" in first colum of second row');
var today = activityDateFormat(new Date());
assert.ok($activity.find('table tbody tr:first td:nth-child(2).today .o_closest_deadline:contains(' + today + ')').length,
'should contain an activity for today in second cell of first line ' + today);
var td = 'table tbody tr:nth-child(1) td.o_activity_empty_cell';
assert.containsN($activity, td, 2, 'should contain an empty cell as no activity scheduled yet.');
// schedule an activity (this triggers a do_action)
await testUtils.fields.editAndTrigger($activity.find(td + ':first'), null, ['mouseenter', 'click']);
assert.containsOnce($activity, 'table tfoot tr .o_record_selector',
'should contain search more selector to choose the record to schedule an activity for it');
// Ensure that the form view is opened in edit mode
await click(document.querySelector(".o_activity_record"));
const $form = $(document.querySelector('.o_form_view'));
assert.containsOnce($form, '.o_form_editable',
'Form view should be opened in edit mode');
});
QUnit.test('activity view: no content rendering', async function (assert) {
assert.expect(2);
const { openView, pyEnv } = await start({
serverData,
});
// reset incompatible setup
pyEnv['mail.activity.type'].unlink(pyEnv['mail.activity.type'].search([]));
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"]],
});
const $activity = $(document);
assert.containsOnce($activity, '.o_view_nocontent',
"should display the no content helper");
assert.strictEqual($activity.find('.o_view_nocontent .o_view_nocontent_empty_folder').text().trim(),
"No data to display",
"should display the no content helper text");
});
QUnit.test('activity view: batch send mail on activity', async function (assert) {
assert.expect(6);
const mailTestActivityIds = pyEnv['mail.test.activity'].search([]);
const mailTemplateIds = pyEnv['mail.template'].search([]);
const { openView } = await start({
serverData,
mockRPC: function(route, args) {
if (args.method === 'activity_send_mail') {
assert.step(JSON.stringify(args.args));
return Promise.resolve(true);
}
},
});
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"]],
});
const $activity = $(document);
assert.notOk($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show').length,
'dropdown shouldn\'t be displayed');
testUtils.dom.click($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) i.fa-ellipsis-v'));
assert.ok($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show').length,
'dropdown should have appeared');
testUtils.dom.click($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show .o_send_mail_template:contains(Template2)'));
assert.notOk($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show').length,
'dropdown shouldn\'t be displayed');
testUtils.dom.click($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) i.fa-ellipsis-v'));
testUtils.dom.click($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show .o_send_mail_template:contains(Template1)'));
assert.verifySteps([
`[[${mailTestActivityIds[0]},${mailTestActivityIds[1]}],${mailTemplateIds[1]}]`, // send mail template 1 on mail.test.activity 1 and 2
`[[${mailTestActivityIds[0]},${mailTestActivityIds[1]}],${mailTemplateIds[0]}]`, // send mail template 2 on mail.test.activity 1 and 2
]);
});
QUnit.test('activity view: activity widget', async function (assert) {
assert.expect(16);
const mailActivityTypeIds = pyEnv['mail.activity.type'].search([]);
const [mailTestActivityId2] = pyEnv['mail.test.activity'].search([['name', '=', 'Office planning']]);
const [mailTemplateId1] = pyEnv['mail.template'].search([['name', '=', 'Template1']]);
const { env, openView } = await start({
mockRPC: function (route, args) {
if (args.method === 'activity_send_mail') {
assert.deepEqual([[mailTestActivityId2], mailTemplateId1], args.args, "Should send template related to mailTestActivity2");
assert.step('activity_send_mail');
// random value returned in order for the mock server to know that this route is implemented.
return true;
}
if (args.method === 'action_feedback_schedule_next') {
assert.deepEqual(
[pyEnv['mail.activity'].search([['state', '=', 'overdue']])],
args.args,
"Should execute action_feedback_schedule_next only on the overude activity"
);
assert.equal(args.kwargs.feedback, "feedback2");
assert.step('action_feedback_schedule_next');
return Promise.resolve({ serverGeneratedAction: true });
}
},
serverData,
});
await openView({
res_model: 'mail.test.activity',
views: [[false, 'activity']],
});
patchWithCleanup(env.services.action, {
doAction(action) {
if (action.serverGeneratedAction) {
assert.step('serverGeneratedAction');
} else if (action.res_model === 'mail.compose.message') {
assert.deepEqual({
default_model: 'mail.test.activity',
default_res_id: mailTestActivityId2,
default_template_id: mailTemplateId1,
default_use_template: true,
force_email: true
}, action.context);
assert.step("do_action_compose");
} else if (action.res_model === 'mail.activity') {
assert.deepEqual({
"default_activity_type_id": mailActivityTypeIds[1],
"default_res_id": mailTestActivityId2,
"default_res_model": 'mail.test.activity',
}, action.context);
assert.step("do_action_activity");
} else {
assert.step("Unexpected action");
}
return Promise.resolve();
},
});
await testUtils.dom.click(document.querySelector('.today .o_closest_deadline'));
assert.hasClass(document.querySelector('.today .dropdown-menu.o_activity'), 'show', "dropdown should be displayed");
assert.ok(document.querySelector('.o_activity_color_today').textContent.includes('Today'), "Title should be today");
assert.ok([...document.querySelectorAll('.today .o_activity_title_entry')].filter(el => el.textContent.includes('Template1')).length,
"Template1 should be available");
assert.ok([...document.querySelectorAll('.today .o_activity_title_entry')].filter(el => el.textContent.includes('Template2')).length,
"Template2 should be available");
await testUtils.dom.click(document.querySelector('.o_activity_title_entry[data-activity-id="2"] .o_activity_template_preview'));
await testUtils.dom.click(document.querySelector('.o_activity_title_entry[data-activity-id="2"] .o_activity_template_send'));
await testUtils.dom.click(document.querySelector('.overdue .o_closest_deadline'));
assert.notOk(document.querySelector('.overdue .o_activity_template_preview'),
"No template should be available");
await testUtils.dom.click(document.querySelector('.overdue .o_schedule_activity'));
await testUtils.dom.click(document.querySelector('.overdue .o_closest_deadline'));
await testUtils.dom.click(document.querySelector('.overdue .o_mark_as_done'));
document.querySelector('.overdue #activity_feedback').value = "feedback2";
await testUtils.dom.click(document.querySelector('.overdue .o_activity_popover_done_next'));
assert.verifySteps([
"do_action_compose",
"activity_send_mail",
"do_action_activity",
"action_feedback_schedule_next",
"serverGeneratedAction"
]);
});
QUnit.test("activity view: no group_by_menu and no comparison_menu", async function (assert) {
assert.expect(4);
serverData.actions = {
1: {
id: 1,
name: "MailTestActivity Action",
res_model: "mail.test.activity",
type: "ir.actions.act_window",
views: [[false, "activity"]],
},
};
const mockRPC = (route, args) => {
if (args.method === "get_activity_data") {
assert.strictEqual(
args.kwargs.context.lang,
"zz_ZZ",
"The context should have been passed"
);
}
};
patchWithCleanup(session.user_context, { lang: "zz_ZZ" });
const { webClient } = await start({ serverData, mockRPC });
await doAction(webClient, 1);
assert.containsN(
document.body,
".o_search_options .dropdown button:visible",
2,
"only two elements should be available in view search"
);
assert.isVisible(
document.querySelector(".o_search_options .dropdown.o_filter_menu > button"),
"filter should be available in view search"
);
assert.isVisible(
document.querySelector(".o_search_options .dropdown.o_favorite_menu > button"),
"favorites should be available in view search"
);
});
QUnit.test('activity view: search more to schedule an activity for a record of a respecting model', async function (assert) {
assert.expect(5);
const mailTestActivityId1 = pyEnv['mail.test.activity'].create({ name: 'MailTestActivity 3' });
Object.assign(serverData.views, {
'mail.test.activity,false,list': '<tree string="MailTestActivity"><field name="name"/></tree>',
});
const { env, openView } = await start({
mockRPC(route, args) {
if (args.method === 'name_search') {
args.kwargs.name = "MailTestActivity";
}
},
serverData,
});
await openView({
res_model: 'mail.test.activity',
views: [[false, 'activity']],
});
patchWithCleanup(env.services.action, {
doAction(action, options) {
assert.step('doAction');
var expectedAction = {
context: {
default_res_id: mailTestActivityId1,
default_res_model: "mail.test.activity",
},
name: "Schedule Activity",
res_id: false,
res_model: "mail.activity",
target: "new",
type: "ir.actions.act_window",
view_mode: "form",
views: [[false, "form"]],
};
assert.deepEqual(action, expectedAction,
"should execute an action with correct params");
options.onClose();
return Promise.resolve();
},
});
const activity = $(document);
assert.containsOnce(activity, 'table tfoot tr .o_record_selector',
'should contain search more selector to choose the record to schedule an activity for it');
await testUtils.dom.click(activity.find('table tfoot tr .o_record_selector'));
// search create dialog
var $modal = $('.modal-lg');
assert.strictEqual($modal.find('.o_data_row').length, 3, "all mail.test.activity should be available to select");
// select a record to schedule an activity for it (this triggers a do_action)
await testUtils.dom.click($modal.find('.o_data_row:last .o_data_cell'));
assert.verifySteps(['doAction']);
});
QUnit.test("Activity view: discard an activity creation dialog", async function (assert) {
assert.expect(2);
serverData.actions = {
1: {
id: 1,
name: "MailTestActivity Action",
res_model: "mail.test.activity",
type: "ir.actions.act_window",
views: [[false, "activity"]],
},
};
Object.assign(serverData.views, {
'mail.activity,false,form':
`<form>
<field name="display_name"/>
<footer>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>`,
});
const mockRPC = (route, args) => {
if (args.method === "check_access_rights") {
return true;
}
};
const { webClient } = await start({ serverData, mockRPC });
await doAction(webClient, 1);
await testUtils.dom.click(
document.querySelector(".o_activity_view .o_data_row .o_activity_empty_cell")
);
await legacyExtraNextTick();
assert.containsOnce($, ".modal.o_technical_modal", "Activity Modal should be opened");
await testUtils.dom.click($('.modal.o_technical_modal button[special="cancel"]'));
await legacyExtraNextTick();
assert.containsNone($, ".modal.o_technical_modal", "Activity Modal should be closed");
});
QUnit.test('Activity view: many2one_avatar_user widget in activity view', async function (assert) {
assert.expect(3);
const [mailTestActivityId1] = pyEnv['mail.test.activity'].search([['name', '=', 'Meeting Room Furnitures']]);
const resUsersId1 = pyEnv['res.users'].create({
display_name: "first user",
avatar_128: "Atmaram Bhide",
});
pyEnv['mail.test.activity'].write([mailTestActivityId1], { activity_user_id: resUsersId1 });
Object.assign(serverData.views, {
'mail.test.activity,false,activity':
`<activity string="MailTestActivity">
<templates>
<div t-name="activity-box">
<field name="activity_user_id" widget="many2one_avatar_user"/>
<field name="name"/>
</div>
</templates>
</activity>`,
});
serverData.actions = {
1: {
id: 1,
name: 'MailTestActivity Action',
res_model: 'mail.test.activity',
type: 'ir.actions.act_window',
views: [[false, 'activity']],
}
};
const { webClient } = await start({ serverData });
await doAction(webClient, 1);
await legacyExtraNextTick();
assert.containsN(document.body, '.o_m2o_avatar', 2);
assert.containsOnce(document.body, `tr[data-res-id=${mailTestActivityId1}] .o_m2o_avatar > img[data-src="/web/image/res.users/${resUsersId1}/avatar_128"]`,
"should have m2o avatar image");
assert.containsNone(document.body, '.o_m2o_avatar > span',
"should not have text on many2one_avatar_user if onlyImage node option is passed");
});
QUnit.test("Activity view: on_destroy_callback doesn't crash", async function (assert) {
assert.expect(3);
patchWithCleanup(ActivityRenderer.prototype, {
setup() {
this._super();
owl.onMounted(() => {
assert.step('mounted');
});
owl.onWillUnmount(() => {
assert.step('willUnmount');
});
}
});
const { openView } = await start({
serverData,
});
await openView({
res_model: 'mail.test.activity',
views: [[false, 'activity']],
});
// force the unmounting of the activity view by opening another one
await openView({
res_model: 'mail.test.activity',
views: [[false, 'form']],
});
assert.verifySteps([
'mounted',
'willUnmount'
]);
});
QUnit.test("Schedule activity dialog uses the same search view as activity view", async function (assert) {
assert.expect(8);
pyEnv['mail.test.activity'].unlink(pyEnv['mail.test.activity'].search([]));
Object.assign(serverData.views, {
"mail.test.activity,false,list": `<list><field name="name"/></list>`,
"mail.test.activity,false,search": `<search/>`,
'mail.test.activity,1,search': `<search/>`,
});
function mockRPC(route, args) {
if (args.method === "get_views") {
assert.step(JSON.stringify(args.kwargs.views));
}
}
const { webClient , click } = await start({ serverData, mockRPC });
// open an activity view (with default search arch)
await doAction(webClient, {
name: 'Dashboard',
res_model: 'mail.test.activity',
type: 'ir.actions.act_window',
views: [[false, 'activity']],
});
assert.verifySteps([
'[[false,"activity"],[false,"search"]]',
])
// click on "Schedule activity"
await click(document.querySelector(".o_activity_view .o_record_selector"));
assert.verifySteps([
'[[false,"list"],[false,"search"]]',
])
// open an activity view (with search arch 1)
await doAction(webClient, {
name: 'Dashboard',
res_model: 'mail.test.activity',
type: 'ir.actions.act_window',
views: [[false, 'activity']],
search_view_id: [1,"search"],
});
assert.verifySteps([
'[[false,"activity"],[1,"search"]]',
])
// click on "Schedule activity"
await click(document.querySelector(".o_activity_view .o_record_selector"));
assert.verifySteps([
'[[false,"list"],[1,"search"]]',
]);
});
QUnit.test('Activity view: apply progressbar filter', async function (assert) {
assert.expect(9);
serverData.actions = {
1: {
id: 1,
name: 'MailTestActivity Action',
res_model: 'mail.test.activity',
type: 'ir.actions.act_window',
views: [[false, 'activity']],
}
};
const { webClient } = await start({ serverData });
await doAction(webClient, 1);
assert.containsNone(document.querySelector('.o_activity_view thead'),
'.o_activity_filter_planned,.o_activity_filter_today,.o_activity_filter_overdue,.o_activity_filter___false',
"should not have active filter");
assert.containsNone(document.querySelector('.o_activity_view tbody'),
'.o_activity_filter_planned,.o_activity_filter_today,.o_activity_filter_overdue,.o_activity_filter___false',
"should not have active filter");
assert.strictEqual(document.querySelector('.o_activity_view tbody .o_activity_record').textContent,
'Office planning', "'Office planning' should be first record");
assert.containsOnce(document.querySelector('.o_activity_view tbody'), '.planned',
"other records should be available");
await testUtils.dom.click(document.querySelector('.o_kanban_counter_progress .progress-bar[data-filter="planned"]'));
assert.containsOnce(document.querySelector('.o_activity_view thead'), '.o_activity_filter_planned',
"planned should be active filter");
assert.containsN(document.querySelector('.o_activity_view tbody'), '.o_activity_filter_planned', 5,
"planned should be active filter");
assert.strictEqual(document.querySelector('.o_activity_view tbody .o_activity_record').textContent,
'Meeting Room Furnitures', "'Office planning' should be first record");
const tr = document.querySelectorAll('.o_activity_view tbody tr')[1];
assert.hasClass(tr.querySelectorAll('td')[1], 'o_activity_empty_cell',
"other records should be hidden");
assert.containsNone(document.querySelector('.o_activity_view tbody'), 'planned',
"other records should be hidden");
});
QUnit.test("Activity view: luxon in renderingContext", async function (assert) {
Object.assign(serverData.views, {
"mail.test.activity,false,activity": `
<activity string="MailTestActivity">
<templates>
<div t-name="activity-box">
<t t-if="luxon">
<span class="luxon">luxon</span>
</t>
</div>
</templates>
</activity>`,
});
const { openView } = await start({
serverData,
});
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"]],
});
assert.containsN(document.body, ".luxon", 2);
});
QUnit.test('update activity view after creating multiple activities', async function (assert) {
assert.expect(9);
pyEnv['mail.test.activity'].create({ name: 'MailTestActivity 3' });
Object.assign(serverData.views, {
'mail.test.activity,false,list': '<tree string="MailTestActivity"><field name="name"/><field name="activity_ids" widget="list_activity"/></tree>',
'mail.activity,false,form': '<form><field name="activity_type_id"/></form>'
});
const { openView } = await start({
mockRPC(route, args) {
if (args.method === 'name_search') {
args.kwargs.name = "MailTestActivity";
}
},
serverData,
});
await openView({
res_model: 'mail.test.activity',
views: [[false, 'activity']],
});
await click("table tfoot tr .o_record_selector");
await click(".o_list_renderer table tbody tr:nth-child(2) td:nth-child(2) .o_ActivityButtonView")
await click(".o-main-components-container .o_PopoverManager .o_ActivityListView .o_ActivityListView_addActivityButton");
await insertText('.o_field_many2one_selection .o_input_dropdown .dropdown input[id=activity_type_id]', "test1");
await click(".o_field_many2one_selection .o_input_dropdown .dropdown input[id=activity_type_id]");
await click('.o-autocomplete--dropdown-menu li:nth-child(1) .dropdown-item');
await click(".modal-footer .o_cp_buttons .o_form_buttons_edit .btn-primary");
await click(".modal-footer .o_form_button_cancel");
await click("table tbody tr:nth-child(1) td:nth-child(6) .o_mail_activity .o_activity_btn .o_closest_deadline");
});
});

View file

@ -0,0 +1,252 @@
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { beforeEach, describe, test, expect } from "@odoo/hoot";
import { queryOne, waitUntil } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import {
click,
contains,
openFormView,
registerArchs,
start,
startServer,
patchUiSize,
SIZES,
dragenterFiles,
dropFiles,
} from "@mail/../tests/mail_test_helpers";
import { browser } from "@web/core/browser/browser";
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineTestMailModels();
let popoutIframe, popoutWindow;
beforeEach(() => {
popoutIframe = document.createElement("iframe");
popoutWindow = {
closed: false,
get document() {
const doc = popoutIframe.contentDocument;
if (!doc) {
return undefined;
}
const originalWrite = doc.write;
doc.write = (content) => {
// This avoids duplicating the test script in the popoutWindow
const sanitizedContent = content.replace(
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
""
);
originalWrite.call(doc, sanitizedContent);
};
return doc;
},
close: () => {
popoutWindow.closed = true;
popoutIframe.remove(popoutAttachmentViewBody());
},
};
});
patchWithCleanup(browser, {
open: () => {
popoutWindow.closed = false;
queryOne(".o_popout_holder").append(popoutIframe);
return popoutWindow;
},
});
function popoutAttachmentViewBody() {
return popoutWindow.document.querySelector(".o-mail-PopoutAttachmentView");
}
async function popoutIsEmpty() {
await animationFrame();
expect(popoutAttachmentViewBody()).toBe(null);
}
async function popoutContains(selector) {
await animationFrame();
await waitUntil(() => popoutAttachmentViewBody());
const target = popoutAttachmentViewBody().querySelector(selector);
expect(target).toBeDisplayed();
return target;
}
async function popoutClick(selector) {
const target = await popoutContains(selector);
click(target);
}
test("Attachment view popout controls test", async () => {
/*
* This test makes sure that the attachment view controls are working in the following cases:
* - Inside the popout window
* - After closing the popout window
*/
const pyEnv = await startServer();
const recordId = pyEnv["mail.test.simple.main.attachment"].create({
display_name: "first partner",
message_attachment_count: 2,
});
pyEnv["ir.attachment"].create([
{
mimetype: "image/jpeg",
res_id: recordId,
res_model: "mail.test.simple.main.attachment",
},
{
mimetype: "application/pdf",
res_id: recordId,
res_model: "mail.test.simple.main.attachment",
},
]);
registerArchs({
"mail.test.simple.main.attachment,false,form": `
<form string="Test document">
<div class="o_popout_holder"/>
<sheet>
<field name="name"/>
</sheet>
<div class="o_attachment_preview"/>
<chatter/>
</form>`,
});
patchUiSize({ size: SIZES.XXL });
await start();
await openFormView("mail.test.simple.main.attachment", recordId);
await click(".o_attachment_preview .o_attachment_control");
await animationFrame();
expect(".o_attachment_preview").not.toBeVisible();
await popoutClick(".o_move_next");
await popoutContains("img");
await popoutClick(".o_move_previous");
await popoutContains("iframe");
popoutWindow.close();
await contains(".o_attachment_preview:not(.d-none)");
expect(".o_attachment_preview").toBeVisible();
await click(".o_attachment_preview .o_move_next");
await contains(".o_attachment_preview img");
await click(".o_attachment_preview .o_move_previous");
await contains(".o_attachment_preview iframe");
await click(".o_attachment_preview .o_attachment_control");
await animationFrame();
expect(".o_attachment_preview").not.toBeVisible();
});
test("Chatter main attachment: can change from non-viewable to viewable", async () => {
const pyEnv = await startServer();
const recordId = pyEnv['mail.test.simple.main.attachment'].create({});
const irAttachmentId = pyEnv['ir.attachment'].create({
mimetype: 'text/plain',
name: "Blah.txt",
res_id: recordId,
res_model: 'mail.test.simple.main.attachment',
});
pyEnv['mail.message'].create({
attachment_ids: [irAttachmentId],
model: 'mail.test.simple.main.attachment',
res_id: recordId,
});
pyEnv['mail.test.simple.main.attachment'].write([recordId], {message_main_attachment_id : irAttachmentId});
registerArchs({
"mail.test.simple.main.attachment,false,form": `
<form string="Test document">
<sheet>
<field name="name"/>
</sheet>
<div class="o_attachment_preview"/>
<chatter/>
</form>`,
});
patchUiSize({ size: SIZES.XXL });
await start();
await openFormView("mail.test.simple.main.attachment", recordId);
// Add a PDF file
const pdfFile = new File([new Uint8Array(1)], "text.pdf", { type: "application/pdf" });
await dragenterFiles(".o-mail-Chatter", [pdfFile]);
await dropFiles(".o-Dropzone", [pdfFile]);
await contains(".o_attachment_preview");
await contains(".o-mail-Attachment > iframe", { count: 0 }); // The viewer tries to display the text file not the PDF
// Switch to the PDF file in the viewer
await click(".o_move_next");
await contains(".o-mail-Attachment > iframe"); // There should be iframe for PDF viewer
});
test.skip("Attachment view / chatter popout across multiple records test", async () => {
// skip because test has race conditions: https://runbot.odoo.com/odoo/runbot.build.error/109795
const pyEnv = await startServer();
const recordIds = pyEnv["mail.test.simple.main.attachment"].create([
{
display_name: "first partner",
message_attachment_count: 1,
},
{
display_name: "second partner",
message_attachment_count: 0,
},
{
display_name: "third partner",
message_attachment_count: 1,
},
]);
pyEnv["ir.attachment"].create([
{
mimetype: "image/jpeg",
res_id: recordIds[0],
res_model: "mail.test.simple.main.attachment",
},
{
mimetype: "application/pdf",
res_id: recordIds[2],
res_model: "mail.test.simple.main.attachment",
},
]);
registerArchs({
"mail.test.simple.main.attachment,false,form": `
<form string="Test document">
<div class="o_popout_holder"/>
<sheet>
<field name="name"/>
</sheet>
<div class="o_attachment_preview"/>
<chatter/>
</form>`,
});
async function navigateRecords() {
/**
* It should be called on the first record of recordIds
* The popout window should be open
* It navigates recordIds as 0 -> 1 -> 2 -> 0 -> 2
*/
await animationFrame();
expect(".o_attachment_preview").not.toBeVisible();
await popoutContains("img");
await click(".o_pager_next");
await popoutIsEmpty();
await click(".o_pager_next");
await popoutContains("iframe");
await click(".o_pager_next");
await popoutContains("img");
await click(".o_pager_previous");
await popoutContains("iframe");
popoutWindow.close();
await contains(".o_attachment_preview:not(.d-none)");
}
patchUiSize({ size: SIZES.XXL });
await start();
await openFormView("mail.test.simple.main.attachment", recordIds[0], {
resIds: recordIds,
});
await click(".o_attachment_preview .o_attachment_control");
await navigateRecords();
await openFormView("mail.test.simple.main.attachment", recordIds[0], {
resIds: recordIds,
});
await click("button i[title='Pop out Attachments']");
await navigateRecords();
});

View file

@ -0,0 +1,197 @@
import {
click,
contains,
inputFiles,
insertText,
listenStoreFetch,
openFormView,
patchUiSize,
registerArchs,
SIZES,
start,
startServer,
triggerHotkey,
waitStoreFetch,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { MockServer, onRpc } from "@web/../tests/web_test_helpers";
import { mail_data } from "@mail/../tests/mock_server/mail_mock_server";
describe.current.tags("desktop");
defineTestMailModels();
test("Send message button activation (access rights dependent)", async () => {
const pyEnv = await startServer();
registerArchs({
"mail.test.multi.company,false,form": `
<form string="Simple">
<sheet>
<field name="name"/>
</sheet>
<chatter/>
</form>
`,
"mail.test.multi.company.read,false,form": `
<form string="Simple">
<sheet>
<field name="name"/>
</sheet>
<chatter/>
</form>
`,
});
let userAccess = {};
listenStoreFetch("mail.thread", {
async onRpc(request) {
const { params } = await request.json();
if (params.fetch_params.some((fetchParam) => fetchParam[0] === "mail.thread")) {
const res = await mail_data.bind(MockServer.current)(request);
res["mail.thread"][0].hasWriteAccess = userAccess.hasWriteAccess;
res["mail.thread"][0].hasReadAccess = userAccess.hasReadAccess;
return res;
}
},
});
await start();
const simpleId = pyEnv["mail.test.multi.company"].create({ name: "Test MC Simple" });
const simpleMcId = pyEnv["mail.test.multi.company.read"].create({
name: "Test MC Readonly with Activities",
});
async function assertSendButton(
enabled,
activities,
msg,
model = null,
resId = null,
hasReadAccess = false,
hasWriteAccess = false
) {
userAccess = { hasReadAccess, hasWriteAccess };
await openFormView(model, resId);
if (resId) {
await waitStoreFetch("mail.thread");
}
if (enabled) {
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Send message" });
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Log note" });
if (activities) {
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Activity" });
}
} else {
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Send message" });
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Log note" });
if (activities) {
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Activity" });
}
}
}
await assertSendButton(
true,
false,
"Record, all rights",
"mail.test.multi.company",
simpleId,
true,
true
);
await assertSendButton(
true,
true,
"Record, all rights",
"mail.test.multi.company.read",
simpleId,
true,
true
);
await assertSendButton(
false,
false,
"Record, no write access",
"mail.test.multi.company",
simpleId,
true
);
await assertSendButton(
true,
true,
"Record, read access but model accept post with read only access",
"mail.test.multi.company.read",
simpleMcId,
true
);
await assertSendButton(false, false, "Record, no rights", "mail.test.multi.company", simpleId);
await assertSendButton(false, true, "Record, no rights", "mail.test.multi.company.read", simpleMcId);
// Note that rights have no impact on send button for draft record (chatter.isTemporary=true)
await assertSendButton(true, false, "Draft record", "mail.test.multi.company");
await assertSendButton(true, true, "Draft record", "mail.test.multi.company.read");
});
test("basic chatter rendering with a model without activities", async () => {
const pyEnv = await startServer();
const recordId = pyEnv["mail.test.simple"].create({ name: "new record" });
registerArchs({
"mail.test.simple,false,form": `
<form string="Records">
<sheet>
<field name="name"/>
</sheet>
<chatter/>
</form>
`,
});
await start();
await openFormView("mail.test.simple", recordId);
await contains(".o-mail-Chatter");
await contains(".o-mail-Chatter-topbar");
await contains("button[aria-label='Attach files']");
await contains("button", { count: 0, text: "Activities" });
await contains(".o-mail-Followers");
await contains(".o-mail-Thread");
});
test("opened attachment box should remain open after adding a new attachment", async (assert) => {
const pyEnv = await startServer();
const recordId = pyEnv["mail.test.simple.main.attachment"].create({});
const attachmentId = pyEnv["ir.attachment"].create({
mimetype: "image/jpeg",
res_id: recordId,
res_model: "mail.test.simple.main.attachment",
});
pyEnv["mail.message"].create({
attachment_ids: [attachmentId],
model: "mail.test.simple.main.attachment",
res_id: recordId,
});
onRpc("/mail/thread/data", async (request) => {
await new Promise((resolve) => setTimeout(resolve, 1)); // need extra time for useEffect
});
patchUiSize({ size: SIZES.XXL });
await start();
await openFormView("mail.test.simple.main.attachment", recordId, {
arch: `
<form>
<sheet>
<field name="name"/>
</sheet>
<div class="o_attachment_preview" />
<chatter reload_on_post="True" reload_on_attachment="True"/>
</form>`,
});
await contains(".o_attachment_preview");
await click(".o-mail-Chatter-attachFiles");
await contains(".o-mail-AttachmentBox");
await click("button", { text: "Send message" });
await inputFiles(".o-mail-Composer .o_input_file", [
new File(["image"], "testing.jpeg", { type: "image/jpeg" }),
]);
await click(".o-mail-Composer-send:enabled");
await contains(".o_move_next");
await click("button", { text: "Send message" });
await insertText(".o-mail-Composer-input", "test");
triggerHotkey("control+Enter");
await contains(".o-mail-Message-body", { text: "test" });
await contains(".o-mail-AttachmentBox .o-mail-AttachmentImage", { count: 2 });
});

View file

@ -1,77 +0,0 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('Chatter');
QUnit.test('Send message button activation (access rights dependent)', async function (assert) {
const pyEnv = await startServer();
const view = `<form string="Simple">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>`;
let userAccess = {};
const { openView } = await start({
serverData: {
views: {
'mail.test.multi.company,false,form': view,
'mail.test.multi.company.read,false,form': view,
}
},
async mockRPC(route, args, performRPC) {
const res = await performRPC(route, args);
if (route === '/mail/thread/data') {
// mimic user with custom access defined in userAccess variable
const { thread_model } = args;
Object.assign(res, userAccess);
res['canPostOnReadonly'] = thread_model === 'mail.test.multi.company.read';
}
return res;
},
});
const resSimpleId1 = pyEnv['mail.test.multi.company'].create({ name: 'Test MC Simple' });
const resSimpleMCId1 = pyEnv['mail.test.multi.company.read'].create({ name: 'Test MC Readonly' });
async function assertSendButton(enabled, msg,
model = null, resId = null,
hasReadAccess = false, hasWriteAccess = false) {
userAccess = { hasReadAccess, hasWriteAccess };
await openView({
res_id: resId,
res_model: model,
views: [[false, 'form']],
});
const details = `hasReadAccess: ${hasReadAccess}, hasWriteAccess: ${hasWriteAccess}, model: ${model}, resId: ${resId}`;
if (enabled) {
assert.containsNone(document.body, '.o_ChatterTopbar_buttonSendMessage:disabled',
`${msg}: send message button must not be disabled (${details}`);
} else {
assert.containsOnce(document.body, '.o_ChatterTopbar_buttonSendMessage:disabled',
`${msg}: send message button must be disabled (${details})`);
}
}
const enabled = true, disabled = false;
await assertSendButton(enabled, 'Record, all rights', 'mail.test.multi.company', resSimpleId1, true, true);
await assertSendButton(enabled, 'Record, all rights', 'mail.test.multi.company.read', resSimpleId1, true, true);
await assertSendButton(disabled, 'Record, no write access', 'mail.test.multi.company', resSimpleId1, true);
await assertSendButton(enabled, 'Record, read access but model accept post with read only access',
'mail.test.multi.company.read', resSimpleMCId1, true);
await assertSendButton(disabled, 'Record, no rights', 'mail.test.multi.company', resSimpleId1);
await assertSendButton(disabled, 'Record, no rights', 'mail.test.multi.company.read', resSimpleMCId1);
// Note that rights have no impact on send button for draft record (chatter.isTemporary=true)
await assertSendButton(enabled, 'Draft record', 'mail.test.multi.company');
await assertSendButton(enabled, 'Draft record', 'mail.test.multi.company.read');
});
});

View file

@ -1,5 +0,0 @@
/** @odoo-module **/
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
addModelNamesToFetch(['mail.test.track.all', 'mail.test.activity', 'mail.test.multi.company', 'mail.test.multi.company.read']);

View file

@ -1,48 +0,0 @@
/** @odoo-module **/
import { start } from "@mail/../tests/helpers/test_utils";
import { prepareTarget } from "web.test_utils";
QUnit.module("test_mail", () => {
QUnit.module("activity view mobile");
QUnit.test('horizontal scroll applies only to the content, not to the whole controller', async (assert) => {
const viewPort = prepareTarget();
viewPort.style.position = "initial";
viewPort.style.width = "initial";
const { openView } = await start();
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"]],
});
const o_view_controller = document.querySelector(".o_view_controller");
const o_content = o_view_controller.querySelector(".o_content");
const o_cp_buttons = o_view_controller.querySelector(".o_control_panel .o_cp_buttons");
const initialXCpBtn = o_cp_buttons.getBoundingClientRect().x;
const o_header_cell = o_content.querySelector(".o_activity_type_cell");
const initialXHeaderCell = o_header_cell.getBoundingClientRect().x;
assert.hasClass(o_view_controller, "o_action_delegate_scroll",
"the 'o_view_controller' should be have the 'o_action_delegate_scroll'.");
assert.strictEqual(window.getComputedStyle(o_view_controller).overflow,"hidden",
"the view controller should have overflow hidden");
assert.strictEqual(window.getComputedStyle(o_content).overflow,"auto",
"the view content should have the overflow auto");
assert.strictEqual(o_content.scrollLeft, 0, "the o_content should not have scroll value");
// Horizontal scroll
o_content.scrollLeft = 100;
assert.strictEqual(o_content.scrollLeft, 100, "the o_content should be 100 due to the overflow auto");
assert.ok(o_header_cell.getBoundingClientRect().x < initialXHeaderCell,
"the gantt header cell x position value should be lower after the scroll");
assert.strictEqual(o_cp_buttons.getBoundingClientRect().x, initialXCpBtn,
"the btn x position of the control panel button should be the same after the scroll");
viewPort.style.position = "";
viewPort.style.width = "";
});
});

View file

@ -0,0 +1,6 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestActivity extends models.ServerModel {
_name = "mail.test.activity";
_inherit = ["mail.thread"];
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestMultiCompany extends models.ServerModel {
_name = "mail.test.multi.company";
}

View file

@ -0,0 +1,6 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestMultiCompanyRead extends models.ServerModel {
_name = "mail.test.multi.company.read";
_mail_post_access = "read";
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestProperties extends models.ServerModel {
_name = "mail.test.properties";
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestSimple extends models.ServerModel {
_name = "mail.test.simple";
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestSimpleMainAttachment extends models.ServerModel {
_name = "mail.test.simple.main.attachment";
}

View file

@ -0,0 +1,10 @@
import { fields, models } from "@web/../tests/web_test_helpers";
export class MailTestTrackAll extends models.ServerModel {
_name = "mail.test.track.all";
_inherit = ["mail.thread"];
float_field_with_digits = fields.Float({
digits: [10, 8],
});
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class ResCurrency extends models.ServerModel {
_name = "res.currency";
}

View file

@ -0,0 +1,73 @@
import {
click,
contains,
openFormView,
registerArchs,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { asyncStep, onRpc, waitForSteps } from "@web/../tests/web_test_helpers";
/**
* Open a chat window when clicking on an avatar many2one / many2many properties.
*/
async function testPropertyFieldAvatarOpenChat(propertyType) {
const pyEnv = await startServer();
registerArchs({
"mail.test.properties,false,form": `
<form string="Form With Avatar Users">
<sheet>
<field name="name"/>
<field name="parent_id"/>
<field name="properties"/>
</sheet>
<chatter/>
</form>
`,
});
onRpc("mail.test.properties", "has_access", () => true);
onRpc("res.users", "read", () => {
asyncStep("read res.users");
return [{ id: userId, partner_id: [partnerId, "Partner Test"] }];
});
onRpc("res.users", "search_read", () => [{ id: userId, name: "User Test" }]);
await start();
const partnerId = pyEnv["res.partner"].create({ name: "Partner Test" });
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
const propertyDefinition = {
type: propertyType,
comodel: "res.users",
name: "user",
string: "user",
};
const parentId = pyEnv["mail.test.properties"].create({
name: "Parent",
definition_properties: [propertyDefinition],
});
const childId = pyEnv["mail.test.properties"].create({
name: "Test",
parent_id: parentId,
properties: [{ ...propertyDefinition, value: [userId] }],
});
await openFormView("mail.test.properties", childId);
await waitForSteps([]);
await click(
propertyType === "many2one" ? ".o_field_property_many2one_value img" : ".o_m2m_avatar"
);
await waitForSteps(["read res.users"]);
await contains(".o-mail-ChatWindow", { text: "Partner Test" });
}
describe.current.tags("desktop");
defineTestMailModels();
test("Properties fields: many2one avatar open chat on click", async () => {
await testPropertyFieldAvatarOpenChat("many2one");
});
test("Properties fields: m2m avatar list open chat on click", async () => {
await testPropertyFieldAvatarOpenChat("many2many");
});

View file

@ -0,0 +1,129 @@
import { start, startServer } from "@mail/../tests/mail_test_helpers";
import { click, contains } from "@mail/../tests/mail_test_helpers_contains";
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { asyncStep, mockService, waitForSteps } from "@web/../tests/web_test_helpers";
import { serializeDate, today } from "@web/core/l10n/dates";
describe.current.tags("desktop");
defineTestMailModels();
// Avoid problem around midnight (Ex.: tomorrow activities become today activities when reaching midnight)
beforeEach(() => mockDate("2023-04-08 10:00:00", 0));
test("menu with no records", async () => {
await start();
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Activities'])");
await contains(".o-mail-ActivityMenu", {
text: "Congratulations, you're done with your activities.",
});
});
test("do not show empty text when at least some future activities", async () => {
const tomorrow = today().plus({ days: 1 });
const pyEnv = await startServer();
const activityId = pyEnv["mail.test.activity"].create({});
pyEnv["mail.activity"].create([
{
date_deadline: serializeDate(tomorrow),
res_id: activityId,
res_model: "mail.test.activity",
},
]);
await start();
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Activities'])");
await contains(".o-mail-ActivityMenu", {
count: 0,
text: "Congratulations, you're done with your activities.",
});
});
test("activity menu widget: activity menu with 2 models", async () => {
const tomorrow = today().plus({ days: 1 });
const yesterday = today().plus({ days: -1 });
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({});
const activityIds = pyEnv["mail.test.activity"].create([{}, {}, {}, {}]);
pyEnv["mail.activity"].create([
{ res_id: partnerId, res_model: "res.partner", date_deadline: serializeDate(today()) },
{
res_id: activityIds[0],
res_model: "mail.test.activity",
date_deadline: serializeDate(today()),
},
{
date_deadline: serializeDate(tomorrow),
res_id: activityIds[1],
res_model: "mail.test.activity",
},
{
date_deadline: serializeDate(tomorrow),
res_id: activityIds[2],
res_model: "mail.test.activity",
},
{
date_deadline: serializeDate(yesterday),
res_id: activityIds[3],
res_model: "mail.test.activity",
},
]);
await start();
await contains(".o_menu_systray i[aria-label='Activities']");
await contains(".o-mail-ActivityMenu-counter");
await contains(".o-mail-ActivityMenu-counter", { text: "5" });
const actionChecks = {
context: {
force_search_count: 1,
search_default_filter_activities_my: 1,
search_default_activities_overdue: 1,
search_default_activities_today: 1,
},
domain: [["active", "in", [true, false]]],
};
mockService("action", {
doAction(action) {
Object.entries(actionChecks).forEach(([key, value]) => {
if (Array.isArray(value) || typeof value === "object") {
expect(action[key]).toEqual(value);
} else {
expect(action[key]).toBe(value);
}
});
asyncStep("do_action:" + action.name);
},
});
await click(".o_menu_systray i[aria-label='Activities']");
await contains(".o-mail-ActivityMenu");
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", { count: 2 });
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", {
contains: [
["div[name='activityTitle']", { text: "res.partner" }],
["span", { text: "0 Late" }],
["span", { text: "1 Today" }],
["span", { text: "0 Future" }],
],
});
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", {
contains: [
["div[name='activityTitle']", { text: "mail.test.activity" }],
["span", { text: "1 Late" }],
["span", { text: "1 Today" }],
["span", { text: "2 Future" }],
],
});
actionChecks.res_model = "res.partner";
await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "res.partner" });
await contains(".o-mail-ActivityMenu", { count: 0 });
await click(".o_menu_systray i[aria-label='Activities']");
actionChecks.res_model = "mail.test.activity";
await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "mail.test.activity" });
await waitForSteps(["do_action:res.partner", "do_action:mail.test.activity"]);
});
test("activity menu widget: close on messaging menu click", async () => {
await start();
await click(".o_menu_systray i[aria-label='Activities']");
await contains(".o-mail-ActivityMenu");
await click(".o_menu_systray i[aria-label='Messages']");
await contains(".o-mail-ActivityMenu", { count: 0 });
});

View file

@ -1,165 +0,0 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import session from 'web.session';
import { date_to_str } from 'web.time';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('test_mail', {}, function () {
QUnit.module('systray_activity_menu_tests.js', {
async beforeEach() {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailTestActivityIds = pyEnv['mail.test.activity'].create([{}, {}, {}, {}]);
pyEnv['mail.activity'].create([
{ res_id: resPartnerId1, res_model: 'res.partner' },
{ res_id: mailTestActivityIds[0], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[1], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[2], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(yesterday), res_id: mailTestActivityIds[3], res_model: 'mail.test.activity' },
]);
},
});
QUnit.test('activity menu widget: menu with no records', async function (assert) {
assert.expect(1);
const { click } = await start({
mockRPC: function (route, args) {
if (args.method === 'systray_get_activities') {
return Promise.resolve([]);
}
},
});
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsOnce(document.body, '.o_ActivityMenuView_noActivity');
});
QUnit.test('activity menu widget: activity menu with 2 models', async function (assert) {
assert.expect(10);
const { click, env } = await start();
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsOnce(document.body, '.o_ActivityMenuView', 'should contain an instance of widget');
assert.ok(document.querySelectorAll('.o_ActivityMenuView_activityGroup').length);
assert.containsOnce(document.body, '.o_ActivityMenuView_counter', "widget should have notification counter");
assert.strictEqual(parseInt(document.querySelector('.o_ActivityMenuView_counter').innerText), 5, "widget should have 5 notification counter");
var context = {};
patchWithCleanup(env.services.action, {
doAction(action) {
assert.deepEqual(action.context, context, "wrong context value");
},
});
// case 1: click on "late"
context = {
force_search_count: 1,
search_default_activities_overdue: 1,
};
assert.containsOnce(document.body, '.o_ActivityMenuView_dropdownMenu.show', 'ActivityMenu should be open');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="overdue"]');
assert.containsNone(document.body, '.show', 'ActivityMenu should be closed');
// case 2: click on "today"
context = {
force_search_count: 1,
search_default_activities_today: 1,
};
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="today"]');
// case 3: click on "future"
context = {
force_search_count: 1,
search_default_activities_upcoming_all: 1,
};
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="upcoming_all"]');
// case 4: click anywere else
context = {
force_search_count: 1,
search_default_activities_overdue: 1,
search_default_activities_today: 1,
};
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroups > div[data-model_name="mail.test.activity"]');
});
QUnit.test('activity menu widget: activity view icon', async function (assert) {
assert.expect(14);
patchWithCleanup(session, { uid: 10 });
const { click, env } = await start();
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsN(document.body, '.o_ActivityMenuView_activityGroupActionButton', 2,
"widget should have 2 activity view icons");
var first = document.querySelector('.o_ActivityMenuView_activityGroupActionButton[data-model_name="res.partner"]');
var second = document.querySelector('.o_ActivityMenuView_activityGroupActionButton[data-model_name="mail.test.activity"]');
assert.ok(first, "should have activity action linked to 'res.partner'");
assert.hasClass(first, 'fa-clock-o', "should display the activity action icon");
assert.ok(second, "should have activity action linked to 'mail.test.activity'");
assert.hasClass(second, 'fa-clock-o', "should display the activity action icon");
patchWithCleanup(env.services.action, {
doAction(action) {
if (action.name) {
assert.ok(action.domain, "should define a domain on the action");
assert.deepEqual(action.domain, [["activity_ids.user_id", "=", 10]],
"should set domain to user's activity only");
assert.step('do_action:' + action.name);
} else {
assert.step('do_action:' + action);
}
},
});
assert.hasClass(document.querySelector('.o-dropdown-menu'), 'show',
"dropdown should be expanded");
await click('.o_ActivityMenuView_activityGroupActionButton[data-model_name="mail.test.activity"]');
assert.containsNone(document.body, '.o-dropdown-menu',
"dropdown should be collapsed");
// click on the "res.partner" activity icon
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupActionButton[data-model_name="res.partner"]');
assert.verifySteps([
'do_action:mail.test.activity',
'do_action:res.partner'
]);
});
QUnit.test('activity menu widget: close on messaging menu click', async function (assert) {
assert.expect(2);
const { click } = await start();
await click('.dropdown-toggle[title="Activities"]');
assert.hasClass(
document.querySelector('.o_ActivityMenuView_dropdownMenu'),
'show',
"activity menu should be shown after click on itself"
);
await click(`.o_MessagingMenu_toggler`);
assert.containsNone(
document.body,
'.o_ActivityMenuView_dropdownMenu',
"activity menu should be hidden after click on messaging menu"
);
});
});

View file

@ -0,0 +1,32 @@
import { contains, mailModels } from "@mail/../tests/mail_test_helpers";
import { MailTestActivity } from "@test_mail/../tests/mock_server/models/mail_test_activity";
import { MailTestMultiCompany } from "@test_mail/../tests/mock_server/models/mail_test_multi_company";
import { MailTestMultiCompanyRead } from "@test_mail/../tests/mock_server/models/mail_test_multi_company_read";
import { MailTestProperties } from "@test_mail/../tests/mock_server/models/mail_test_properties";
import { MailTestSimpleMainAttachment } from "./mock_server/models/mail_test_simple_main_attachment";
import { MailTestSimple } from "@test_mail/../tests/mock_server/models/mail_test_simple";
import { MailTestTrackAll } from "@test_mail/../tests/mock_server/models/mail_test_track_all";
import { defineModels, defineParams } from "@web/../tests/web_test_helpers";
export const testMailModels = {
...mailModels,
MailTestActivity,
MailTestMultiCompany,
MailTestMultiCompanyRead,
MailTestProperties,
MailTestSimpleMainAttachment,
MailTestSimple,
MailTestTrackAll,
};
export function defineTestMailModels() {
defineParams({ suite: "test_mail" }, "replace");
defineModels(testMailModels);
}
export async function editSelect(selector, value) {
await contains(selector);
const el = document.querySelector(selector);
el.value = value;
el.dispatchEvent(new Event("change"));
}

View file

@ -0,0 +1,69 @@
import { registry } from "@web/core/registry";
const setPager = value => [
{
content: "Click Pager",
trigger: ".o_pager_value:first()",
run: "click",
},
{
content: "Change pager to display lines " + value,
trigger: "input.o_pager_value",
run: `edit ${value} && click body`,
},
{
trigger: `.o_pager_value:contains('${value}')`,
},
]
const checkRows = values => {
return {
trigger: '.o_activity_view',
run: () => {
const dataRow = document.querySelectorAll('.o_activity_view tbody .o_data_row .o_activity_record');
if (dataRow.length !== values.length) {
throw Error(`There should be ${values.length} activities`);
}
values.forEach((value, index) => {
if (dataRow[index].textContent !== value) {
throw Error(`Record does not match ${value} != ${dataRow[index]}`);
}
});
}
}
}
registry.category("web_tour.tours").add("mail_activity_view", {
steps: () => [
{
content: "Open the debug menu",
trigger: ".o_debug_manager button",
run: "click",
},
{
content: "Click the Set Defaults menu",
trigger: ".o-dropdown-item:contains(Open View)",
run: "click",
},
{
trigger: ".o_searchview_input",
run: "edit Test Activity View"
},
{
trigger: ".o_searchview_autocomplete .o-dropdown-item.focus",
content: "Validate search",
run: "click",
},
{
content: "Select Test Activity View",
trigger: `.o_data_row td:contains("Test Activity View")`,
run: "click",
},
checkRows(["Task 1", "Task 2", "Task 3"]),
...setPager("1-2"),
checkRows(["Task 2", "Task 3"]),
...setPager("3"),
checkRows(["Task 1"]),
],
})

View file

@ -0,0 +1,377 @@
import {
contains,
click,
insertText,
openFormView,
registerArchs,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { mockDate, mockTimeZone } from "@odoo/hoot-mock";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { editSelectMenu, patchWithCleanup } from "@web/../tests/web_test_helpers";
import { currencies } from "@web/core/currency";
const archs = {
"mail.test.track.all,false,form": `
<form>
<sheet>
<field name="boolean_field"/>
<field name="char_field"/>
<field name="date_field"/>
<field name="datetime_field"/>
<field name="float_field"/>
<field name="float_field_with_digits"/>
<field name="integer_field"/>
<field name="monetary_field"/>
<field name="many2one_field_id"/>
<field name="selection_field"/>
<field name="text_field"/>
</sheet>
<chatter/>
</form>
`,
};
describe.current.tags("desktop");
defineTestMailModels();
beforeEach(() => mockTimeZone(0));
test("basic rendering of tracking value (float type)", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({ float_field: 12.3 });
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=float_field] input", "45.67", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking");
await contains(".o-mail-Message-trackingField");
await contains(".o-mail-Message-trackingField", { text: "(Float)" });
await contains(".o-mail-Message-trackingOld");
await contains(".o-mail-Message-trackingOld", { text: "12.30" });
await contains(".o-mail-Message-trackingSeparator");
await contains(".o-mail-Message-trackingNew");
await contains(".o-mail-Message-trackingNew", { text: "45.67" });
});
test("rendering of tracked field of type float: from non-0 to 0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
float_field: 1,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=float_field] input", "0", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "1.000.00(Float)" });
});
test("rendering of tracked field of type float: from 0 to non-0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
float_field: 0,
float_field_with_digits: 0,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=float_field] input", "1.01", { replace: true });
await insertText("div[name=float_field_with_digits] input", "1.0001", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { count: 2 });
const [increasedPrecisionLine, defaultPrecisionLine] =
document.getElementsByClassName("o-mail-Message-tracking");
const expectedText = [
[defaultPrecisionLine, ["0.00", "1.01", "(Float)"]],
[increasedPrecisionLine, ["0.00000000", "1.00010000", "(Float)"]],
];
for (const [targetLine, [oldText, newText, fieldName]] of expectedText) {
await contains(".o-mail-Message-trackingOld", { target: targetLine, text: oldText });
await contains(".o-mail-Message-trackingNew", { target: targetLine, text: newText });
await contains(".o-mail-Message-trackingField", { target: targetLine, text: fieldName });
}
});
test("rendering of tracked field of type integer: from non-0 to 0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
integer_field: 1,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=integer_field] input", "0", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "10(Integer)" });
});
test("rendering of tracked field of type integer: from 0 to non-0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
integer_field: 0,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=integer_field] input", "1", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "01(Integer)" });
});
test("rendering of tracked field of type monetary: from non-0 to 0", async () => {
const pyEnv = await startServer();
const testCurrencyId = pyEnv["res.currency"].create({ name: "ECU", symbol: "§" });
// need to patch currencies as they're passed via cookies, not through the orm
patchWithCleanup(currencies, {
[testCurrencyId]: { digits: [69, 2], position: "after", symbol: "§" },
});
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
currency_id: testCurrencyId,
monetary_field: 1,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=monetary_field] input", "0", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "1.00 §0.00 §(Monetary)" });
});
test("rendering of tracked field of type monetary: from 0 to non-0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
monetary_field: 0,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=monetary_field] input", "1", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "0.001.00(Monetary)" });
});
test("rendering of tracked field of type boolean: from true to false", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
boolean_field: true,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click(".o_field_boolean input");
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "YesNo(Boolean)" });
});
test("rendering of tracked field of type boolean: from false to true", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click(".o_field_boolean input");
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "NoYes(Boolean)" });
});
test("rendering of tracked field of type char: from a string to empty string", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
char_field: "Marc",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=char_field] input", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "MarcNone(Char)" });
});
test("rendering of tracked field of type char: from empty string to a string", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
char_field: "",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=char_field] input", "Marc", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Char)" });
});
test("rendering of tracked field of type date: from no date to a set date", async () => {
mockDate("2018-12-01");
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
date_field: false,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("div[name=date_field] input");
await click(".o_datetime_button", { text: "14" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "None12/14/2018(Date)" });
});
test("rendering of tracked field of type date: from a set date to no date", async () => {
mockDate("2018-12-01");
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
date_field: "2018-12-14",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("div[name=date_field] button");
await insertText("div[name=date_field] input", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "12/14/2018None(Date)" });
});
test("rendering of tracked field of type datetime: from no date and time to a set date and time", async function () {
mockDate("2018-12-01", 3);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
datetime_field: false,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("div[name=datetime_field] input");
await click(".o_datetime_button", { text: "14" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "None12/14/2018 12:00:00(Datetime)" });
const [savedRecord] = pyEnv["mail.test.track.all"].search_read([
["id", "=", mailTestTrackAllId1],
]);
expect(savedRecord.datetime_field).toBe("2018-12-14 09:00:00");
});
test("rendering of tracked field of type datetime: from a set date and time to no date and time", async () => {
mockTimeZone(3);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
datetime_field: "2018-12-14 13:42:28 ",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("div[name=datetime_field] button");
await insertText("div[name=datetime_field] input", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "12/14/2018 16:42:28None(Datetime)" });
});
test("rendering of tracked field of type text: from some text to empty", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
text_field: "Marc",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=text_field] textarea", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "MarcNone(Text)" });
});
test("rendering of tracked field of type text: from empty to some text", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
text_field: "",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=text_field] textarea", "Marc", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Text)" });
});
test("rendering of tracked field of type selection: from a selection to no selection", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
selection_field: "first",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await editSelectMenu("div[name=selection_field] input", { value: "" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "firstNone(Selection)" });
});
test("rendering of tracked field of type selection: from no selection to a selection", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await editSelectMenu("div[name=selection_field] input", { value: "First" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "Nonefirst(Selection)" });
});
test("rendering of tracked field of type many2one: from having a related record to no related record", async () => {
const pyEnv = await startServer();
const resPartnerId1 = pyEnv["res.partner"].create({ name: "Marc" });
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
many2one_field_id: resPartnerId1,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText(".o_field_many2one_selection input", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "MarcNone(Many2one)" });
});
test("rendering of tracked field of type many2one: from no related record to having a related record", async () => {
const pyEnv = await startServer();
pyEnv["res.partner"].create({ name: "Marc" });
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("[name=many2one_field_id] input");
await click("[name=many2one_field_id] .o-autocomplete--dropdown-item", { text: "Marc" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Many2one)" });
});
test("Search message with filter in chatter", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId = pyEnv["mail.test.track.all"].create({});
pyEnv["mail.message"].create({
body: "Hermit",
model: "mail.test.track.all",
res_id: mailTestTrackAllId,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId);
await click("[name=many2one_field_id] input");
await click("[name=many2one_field_id] .o-autocomplete--dropdown-item", { text: "Hermit" });
await click(".o_form_button_save");
// Search message with filter
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "Hermit");
await click("button[title='Filter Messages']");
await click("span", { text: "Conversations" });
await contains(".o-mail-SearchMessageResult .o-mail-Message", { text: "Hermit" });
await click("button[title='Filter Messages']");
await click("span", { text: "Tracked Changes" });
await contains(".o-mail-SearchMessageResult .o-mail-Message", { text: "Hermit" });
await click("button[title='Filter Messages']");
await click("span", { text: "All" });
await contains(".o-mail-SearchMessageResult .o-mail-Message", { count: 2 });
});

View file

@ -1,438 +0,0 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { editInput, editSelect, selectDropdownItem, patchWithCleanup, patchTimeZone } from "@web/../tests/helpers/utils";
import session from 'web.session';
import testUtils from 'web.test_utils';
QUnit.module('test_mail', {}, function () {
QUnit.module('tracking_value_tests.js', {
beforeEach() {
const views = {
'mail.test.track.all,false,form':
`<form>
<sheet>
<field name="boolean_field"/>
<field name="char_field"/>
<field name="date_field"/>
<field name="datetime_field"/>
<field name="float_field"/>
<field name="integer_field"/>
<field name="monetary_field"/>
<field name="many2one_field_id"/>
<field name="selection_field"/>
<field name="text_field"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>`,
};
this.start = async ({ res_id }) => {
const { openFormView, ...remainder } = await start({
serverData: { views },
});
await openFormView(
{
res_model: 'mail.test.track.all',
res_id,
},
{
props: { mode: 'edit' },
},
);
return remainder;
};
patchWithCleanup(session, {
getTZOffset() {
return 0;
},
});
},
});
QUnit.test('basic rendering of tracking value (float type)', async function (assert) {
assert.expect(8);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 12.30 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=float_field] input', 45.67);
await click('.o_form_button_save');
assert.containsOnce(
document.body,
'.o_TrackingValue',
"should display a tracking value"
);
assert.containsOnce(
document.body,
'.o_TrackingValue_fieldName',
"should display the name of the tracked field"
);
assert.strictEqual(
document.querySelector('.o_TrackingValue_fieldName').textContent,
"(Float)",
"should display the correct tracked field name (Float)",
);
assert.containsOnce(
document.body,
'.o_TrackingValue_oldValue',
"should display the old value"
);
assert.strictEqual(
document.querySelector('.o_TrackingValue_oldValue').textContent,
"12.30",
"should display the correct old value (12.30)",
);
assert.containsOnce(
document.body,
'.o_TrackingValue_separator',
"should display the separator"
);
assert.containsOnce(
document.body,
'.o_TrackingValue_newValue',
"should display the new value"
);
assert.strictEqual(
document.querySelector('.o_TrackingValue_newValue').textContent,
"45.67",
"should display the correct new value (45.67)",
);
});
QUnit.test('rendering of tracked field of type float: from non-0 to 0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=float_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"1.000.00(Float)",
"should display the correct content of tracked field of type float: from non-0 to 0 (1.00 -> 0.00 (Float))"
);
});
QUnit.test('rendering of tracked field of type float: from 0 to non-0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=float_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"0.001.00(Float)",
"should display the correct content of tracked field of type float: from 0 to non-0 (0.00 -> 1.00 (Float))"
);
});
QUnit.test('rendering of tracked field of type integer: from non-0 to 0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ integer_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=integer_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"10(Integer)",
"should display the correct content of tracked field of type integer: from non-0 to 0 (1 -> 0 (Integer))"
);
});
QUnit.test('rendering of tracked field of type integer: from 0 to non-0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ integer_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=integer_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"01(Integer)",
"should display the correct content of tracked field of type integer: from 0 to non-0 (0 -> 1 (Integer))"
);
});
QUnit.test('rendering of tracked field of type monetary: from non-0 to 0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ monetary_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=monetary_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"1.000.00(Monetary)",
"should display the correct content of tracked field of type monetary: from non-0 to 0 (1.00 -> 0.00 (Monetary))"
);
});
QUnit.test('rendering of tracked field of type monetary: from 0 to non-0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ monetary_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=monetary_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"0.001.00(Monetary)",
"should display the correct content of tracked field of type monetary: from 0 to non-0 (0.00 -> 1.00 (Monetary))"
);
});
QUnit.test('rendering of tracked field of type boolean: from true to false', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ boolean_field: true });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
document.querySelector('.o_field_boolean input').click();
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"YesNo(Boolean)",
"should display the correct content of tracked field of type boolean: from true to false (True -> False (Boolean))"
);
});
QUnit.test('rendering of tracked field of type boolean: from false to true', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
document.querySelector('.o_field_boolean input').click();
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"NoYes(Boolean)",
"should display the correct content of tracked field of type boolean: from false to true (False -> True (Boolean))"
);
});
QUnit.test('rendering of tracked field of type char: from a string to empty string', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ char_field: 'Marc' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=char_field] input', '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"MarcNone(Char)",
"should display the correct content of tracked field of type char: from a string to empty string (Marc -> None (Char))"
);
});
QUnit.test('rendering of tracked field of type char: from empty string to a string', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ char_field: '' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=char_field] input', 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"NoneMarc(Char)",
"should display the correct content of tracked field of type char: from empty string to a string (None -> Marc (Char))"
);
});
QUnit.test('rendering of tracked field of type date: from no date to a set date', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ date_field: false });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('div[name=date_field] .o_datepicker .o_datepicker_input'), '12/14/2018', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"None12/14/2018(Date)",
"should display the correct content of tracked field of type date: from no date to a set date (None -> 12/14/2018 (Date))"
);
});
QUnit.test('rendering of tracked field of type date: from a set date to no date', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ date_field: '2018-12-14' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('div[name=date_field] .o_datepicker .o_datepicker_input'), '', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"12/14/2018None(Date)",
"should display the correct content of tracked field of type date: from a set date to no date (12/14/2018 -> None (Date))"
);
});
QUnit.test('rendering of tracked field of type datetime: from no date and time to a set date and time', async function (assert) {
assert.expect(2);
patchTimeZone(180);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ datetime_field: false });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('div[name=datetime_field] .o_datepicker .o_datepicker_input'), '12/14/2018 13:42:28', ['change']);
await click('.o_form_button_save');
const savedRecord = pyEnv.getData()["mail.test.track.all"].records.find(({id}) => id === mailTestTrackAllId1);
assert.strictEqual(savedRecord.datetime_field, '2018-12-14 10:42:28');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"None12/14/2018 13:42:28(Datetime)",
"should display the correct content of tracked field of type datetime: from no date and time to a set date and time (None -> 12/14/2018 13:42:28 (Datetime))"
);
});
QUnit.test('rendering of tracked field of type datetime: from a set date and time to no date and time', async function (assert) {
assert.expect(1);
patchTimeZone(180)
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ datetime_field: '2018-12-14 13:42:28 ' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('div[name=datetime_field] .o_datepicker .o_datepicker_input'), '', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"12/14/2018 16:42:28None(Datetime)",
"should display the correct content of tracked field of type datetime: from a set date and time to no date and time (12/14/2018 13:42:28 -> None (Datetime))"
);
});
QUnit.test('rendering of tracked field of type text: from some text to empty', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ text_field: 'Marc' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=text_field] textarea', '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"MarcNone(Text)",
"should display the correct content of tracked field of type text: from some text to empty (Marc -> None (Text))"
);
});
QUnit.test('rendering of tracked field of type text: from empty to some text', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ text_field: '' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=text_field] textarea', 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"NoneMarc(Text)",
"should display the correct content of tracked field of type text: from empty to some text (None -> Marc (Text))"
);
});
QUnit.test('rendering of tracked field of type selection: from a selection to no selection', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ selection_field: 'first' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editSelect(document.body, 'div[name=selection_field] select', false);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"firstNone(Selection)",
"should display the correct content of tracked field of type selection: from a selection to no selection (first -> None (Selection))"
);
});
QUnit.test('rendering of tracked field of type selection: from no selection to a selection', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editSelect(document.body, 'div[name=selection_field] select', '"first"');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"Nonefirst(Selection)",
"should display the correct content of tracked field of type selection: from no selection to a selection (None -> first (Selection))"
);
});
QUnit.test('rendering of tracked field of type many2one: from having a related record to no related record', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Marc' });
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ many2one_field_id: resPartnerId1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, ".o_field_many2one_selection input", '')
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"MarcNone(Many2one)",
"should display the correct content of tracked field of type many2one: from having a related record to no related record (Marc -> None (Many2one))"
);
});
QUnit.test('rendering of tracked field of type many2one: from no related record to having a related record', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['res.partner'].create({ display_name: 'Marc' });
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await selectDropdownItem(document.body, "many2one_field_id", "Marc")
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"NoneMarc(Many2one)",
"should display the correct content of tracked field of type many2one: from no related record to having a related record (None -> Marc (Many2one))"
);
});
});

View file

@ -1,24 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_controller_attachment
from . import test_controller_binary
from . import test_controller_thread
from . import test_invite
from . import test_ir_actions
from . import test_ir_attachment
from . import test_mail_activity
from . import test_mail_activity_mixin
from . import test_mail_activity_plan
from . import test_mail_alias
from . import test_mail_composer
from . import test_mail_composer_mixin
from . import test_mail_followers
from . import test_mail_gateway
from . import test_mail_flow
from . import test_mail_mail
from . import test_mail_management
from . import test_mail_message
from . import test_mail_message_security
from . import test_mail_mail
from . import test_mail_gateway
from . import test_mail_multicompany
from . import test_mail_push
from . import test_mail_scheduled_message
from . import test_mail_security
from . import test_mail_thread_internals
from . import test_mail_thread_mixins
from . import test_mail_template
from . import test_mail_template_preview
from . import test_message_management
from . import test_message_post
from . import test_message_track
from . import test_performance
from . import test_ui
from . import test_mail_management
from . import test_mail_security

View file

@ -1,15 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests.common import TransactionCase
class TestMailCommon(MailCommon):
""" Main entry point for functional tests. Kept to ease backward
compatibility. """
class TestRecipients(TransactionCase):
@classmethod
@ -25,13 +19,11 @@ class TestRecipients(TransactionCase):
'name': 'Valid Lelitre',
'email': 'valid.lelitre@agrolait.com',
'country_id': cls.env.ref('base.be').id,
'mobile': '0456001122',
'phone': False,
'phone': '0456001122',
})
cls.partner_2 = Partner.create({
'name': 'Valid Poilvache',
'email': 'valid.other@gmail.com',
'country_id': cls.env.ref('base.be').id,
'mobile': '+32 456 22 11 00',
'phone': False,
'phone': '+32 456 22 11 00',
})

View file

@ -0,0 +1,35 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
from odoo.addons.mail.tests.common_controllers import MailControllerAttachmentCommon
@odoo.tests.tagged("-at_install", "post_install", "mail_controller")
class TestAttachmentController(MailControllerAttachmentCommon):
def test_independent_attachment_delete(self):
"""Test access to delete an attachment whether or not limited `ownership_token` is sent"""
self._execute_subtests_delete(self.all_users, token=True, allowed=True)
self._execute_subtests_delete(self.user_admin, token=False, allowed=True)
self._execute_subtests_delete(
(self.guest, self.user_employee, self.user_portal, self.user_public),
token=False,
allowed=False,
)
def test_attachment_delete_linked_to_thread(self):
"""Test access to delete an attachment associated with a thread
whether or not limited `ownership_token` is sent"""
thread = self.env["mail.test.simple"].create({"name": "Test"})
self._execute_subtests_delete(self.all_users, token=True, allowed=True, thread=thread)
self._execute_subtests_delete(
(self.user_admin, self.user_employee),
token=False,
allowed=True,
thread=thread,
)
self._execute_subtests_delete(
(self.guest, self.user_portal, self.user_public),
token=False,
allowed=False,
thread=thread,
)

View file

@ -0,0 +1,47 @@
from odoo.addons.mail.tests.common_controllers import MailControllerBinaryCommon
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestPublicBinaryController(MailControllerBinaryCommon):
def test_avatar_no_public(self):
"""Test access to open a guest / partner avatar who hasn't sent a message on a
public record."""
for source in (self.guest_2, self.user_employee_nopartner.partner_id):
self._execute_subtests(
source, (
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
)
)
def test_avatar_private(self):
"""Test access to open a partner avatar who has sent a message on a private record."""
document = self.env["mail.test.simple.unfollow"].create({"name": "Test"})
self._post_message(document, self.user_employee_nopartner)
self._execute_subtests(
self.user_employee_nopartner.partner_id, (
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
)
)
def test_avatar_public(self):
"""Test access to open a guest avatar who has sent a message on a public record."""
document = self.env["mail.test.access.public"].create({"name": "Test"})
for author, source in ((self.guest_2, self.guest_2), (self.user_employee_nopartner, self.user_employee_nopartner.partner_id)):
self._post_message(document, author)
self._execute_subtests(
source,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
),
)

View file

@ -0,0 +1,167 @@
import json
from odoo import http
from odoo.addons.mail.tests.common_controllers import MailControllerThreadCommon
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged("-at_install", "post_install", "mail_controller")
class TestMessageController(MailControllerThreadCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_public_record = cls.env["mail.test.access.public"].create({"name": "Public Channel", "email": "john@test.be", "mobile": "+32455001122"})
@mute_logger("odoo.http")
def test_thread_attachment_hijack(self):
att = self.env["ir.attachment"].create({
"name": "arguments_for_firing_marc_demo",
"res_id": 0,
"res_model": "mail.compose.message",
})
self.authenticate(self.user_employee.login, self.user_employee.login)
record = self.env["mail.test.access.public"].create({"name": "Public Channel"})
record.with_user(self.user_employee).write({'name': 'updated'}) # can access, update, ...
# if this test breaks, it might be due to a change in /web/content, or the default rules for accessing an attachment. This is not an issue but it makes this test irrelevant.
self.assertFalse(self.url_open(f"/web/content/{att.id}").ok)
response = self.url_open(
url="/mail/message/post",
headers={"Content-Type": "application/json"}, # route called as demo
data=json.dumps(
{
"params": {
"post_data": {
"attachment_ids": [att.id], # demo does not have access to this attachment id
"body": "",
"message_type": "comment",
"partner_ids": [],
"subtype_xmlid": "mail.mt_comment",
},
"thread_id": record.id,
"thread_model": record._name,
}
},
),
)
self.assertNotIn(
"arguments_for_firing_marc_demo", response.text
) # demo should not be able to see the name of the document
def test_thread_partner_from_email_authenticated(self):
self.authenticate(self.user_employee.login, self.user_employee.login)
res3 = self.url_open(
url="/mail/partner/from_email",
data=json.dumps(
{
"params": {
"thread_model": self.test_public_record._name,
"thread_id": self.test_public_record.id,
"emails": ["john@test.be"],
},
}
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res3.status_code, 200)
self.assertEqual(
1,
self.env["res.partner"].search_count([('email', '=', "john@test.be"), ('phone', '=', "+32455001122")]),
"authenticated users can create a partner from an email",
)
# should not create another partner with same email
res4 = self.url_open(
url="/mail/partner/from_email",
data=json.dumps(
{
"params": {
"thread_model": self.test_public_record._name,
"thread_id": self.test_public_record.id,
"emails": ["john@test.be"],
},
}
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res4.status_code, 200)
self.assertEqual(
1,
self.env["res.partner"].search_count([('email', '=', "john@test.be")]),
"'mail/partner/from_email' does not create another user if there's already a user with matching email",
)
self.test_public_record.write({'email': 'john2@test.be'})
res5 = self.url_open(
url="/mail/message/post",
data=json.dumps(
{
"params": {
"thread_model": self.test_public_record._name,
"thread_id": self.test_public_record.id,
"post_data": {
"body": "test",
"partner_emails": ["john2@test.be"],
},
},
}
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res5.status_code, 200)
self.assertEqual(
1,
self.env["res.partner"].search_count([('email', '=', "john2@test.be"), ('phone', '=', "+32455001122")]),
"authenticated users can create a partner from an email from message_post",
)
# should not create another partner with same email
res6 = self.url_open(
url="/mail/message/post",
data=json.dumps(
{
"params": {
"thread_model": self.test_public_record._name,
"thread_id": self.test_public_record.id,
"post_data": {
"body": "test",
"partner_emails": ["john2@test.be"],
},
},
}
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res6.status_code, 200)
self.assertEqual(
1,
self.env["res.partner"].search_count([('email', '=', "john2@test.be")]),
"'mail/message/post' does not create another user if there's already a user with matching email",
)
def test_thread_post_archived_record(self):
self.authenticate(self.user_employee.login, self.user_employee.login)
archived_partner = self.env["res.partner"].create({"name": "partner", "active": False})
# 1. posting a message
data = self.make_jsonrpc_request("/mail/message/post", {
"thread_model": "res.partner",
"thread_id": archived_partner.id,
"post_data": {
"body": "A great message",
}
})
message = next(filter(lambda m: m["id"] == data["message_id"], data["store_data"]["mail.message"]))
self.assertEqual(["markup", "<p>A great message</p>"], message["body"])
# 2. attach a file
response = self.url_open(
"/mail/attachment/upload",
{
"csrf_token": http.Request.csrf_token(self),
"thread_id": archived_partner.id,
"thread_model": "res.partner",
},
files={"ufile": b""},
)
self.assertEqual(response.status_code, 200)

View file

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mail.tests.common import TestMailCommon
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('mail_followers')
class TestInvite(TestMailCommon):
class TestInvite(MailCommon):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_invite_email(self):
@ -16,18 +16,38 @@ class TestInvite(TestMailCommon):
'name': 'Valid Lelitre',
'email': 'valid.lelitre@agrolait.com'})
mail_invite = self.env['mail.wizard.invite'].with_context({
mail_invite = self.env['mail.followers.edit'].with_context({
'default_res_model': 'mail.test.simple',
'default_res_id': test_record.id
}).with_user(self.user_employee).create({
'partner_ids': [(4, test_partner.id), (4, self.user_admin.partner_id.id)],
'send_mail': True})
with self.mock_mail_gateway():
mail_invite.add_followers()
'default_res_ids': [test_record.id],
}).with_user(self.user_employee).create({'partner_ids': [(4, test_partner.id), (4, self.user_admin.partner_id.id)],
'notify': True})
with self.mock_mail_app(), self.mock_mail_gateway():
mail_invite.edit_followers()
# check added followers and that emails were sent
# Check added followers and that notifications are sent.
# Admin notification preference is inbox so the notification must be of inbox type
# while partner_employee must receive it by email.
self.assertEqual(test_record.message_partner_ids,
test_partner | self.user_admin.partner_id)
self.assertEqual(len(self._new_msgs), 1)
self.assertEqual(len(self._mails), 1)
self.assertSentEmail(self.partner_employee, [test_partner])
self.assertSentEmail(self.partner_employee, [self.partner_admin])
self.assertEqual(len(self._mails), 2)
self.assertNotSentEmail([self.partner_admin])
self.assertNotified(
self._new_msgs[0],
[{'partner': self.partner_admin, 'type': 'inbox', 'is_read': False}]
)
# Remove followers
mail_remove = self.env['mail.followers.edit'].with_context({
'default_res_model': 'mail.test.simple',
'default_res_ids': [test_record.id],
}).with_user(self.user_employee).create({
"operation": "remove",
'partner_ids': [(4, test_partner.id), (4, self.user_admin.partner_id.id)]})
with self.mock_mail_app(), self.mock_mail_gateway():
mail_remove.edit_followers()
# Check removed followers and that notifications are sent.
self.assertEqual(test_record.message_partner_ids, self.env["res.partner"])

View file

@ -2,13 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.base.tests.test_ir_actions import TestServerActionsBase
from odoo.addons.test_mail.tests.common import TestMailCommon
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('ir_actions')
class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
class TestServerActionsEmail(MailCommon, TestServerActionsBase):
def setUp(self):
super(TestServerActionsEmail, self).setUp()
@ -61,6 +61,17 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
self.action.with_context(self.context).run()
self.assertEqual(self.test_partner.message_partner_ids, self.env.ref('base.partner_admin') | random_partner)
def test_action_followers_warning(self):
self.test_partner.message_unsubscribe(self.test_partner.message_partner_ids.ids)
self.action.write({
'state': 'followers',
"followers_type": "generic",
"followers_partner_field_name": "user_id.name"
})
self.assertEqual(self.action.warning, "The field 'Salesperson > Name' is not a partner field.")
self.action.write({"followers_partner_field_name": "parent_id.child_ids"})
self.assertEqual(self.action.warning, False)
def test_action_message_post(self):
# initial state
self.assertEqual(len(self.test_partner.message_ids), 1,
@ -78,7 +89,10 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
with self.assertSinglePostNotifications(
[{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}],
message_info={'content': 'Hello %s' % self.test_partner.name,
'message_type': 'notification',
'mail_mail_values': {
'author_id': self.env.user.partner_id,
},
'message_type': 'auto_comment',
'subtype': 'mail.mt_comment',
}
):
@ -95,7 +109,7 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
with self.assertSinglePostNotifications(
[{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}],
message_info={'content': 'Hello %s' % self.test_partner.name,
'message_type': 'notification',
'message_type': 'auto_comment',
'subtype': 'mail.mt_note',
}
):
@ -117,6 +131,18 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
self.assertEqual(self.env['mail.activity'].search_count([]), before_count + 1)
self.assertEqual(self.env['mail.activity'].search_count([('summary', '=', 'TestNew')]), 1)
def test_action_next_activity_warning(self):
self.action.write({
'state': 'next_activity',
'activity_user_type': 'generic',
"activity_user_field_name": "user_id.name",
'activity_type_id': self.env.ref('mail.mail_activity_data_meeting').id,
'activity_summary': 'TestNew',
})
self.assertEqual(self.action.warning, "The field 'Salesperson > Name' is not a user field.")
self.action.write({"activity_user_field_name": "parent_id.user_id"})
self.assertEqual(self.action.warning, False)
def test_action_next_activity_due_date(self):
""" Make sure we don't crash if a due date is set without a type. """
self.action.write({
@ -132,3 +158,66 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
self.assertFalse(run_res, 'ir_actions_server: create next activity action correctly finished should return False')
self.assertEqual(self.env['mail.activity'].search_count([]), before_count + 1)
self.assertEqual(self.env['mail.activity'].search_count([('summary', '=', 'TestNew')]), 1)
def test_action_next_activity_from_x2m_user(self):
self.test_partner.user_ids = self.user_demo | self.user_admin
self.action.write({
'state': 'next_activity',
'activity_user_type': 'generic',
'activity_user_field_name': 'user_ids',
'activity_type_id': self.env.ref('mail.mail_activity_data_meeting').id,
'activity_summary': 'TestNew',
})
before_count = self.env['mail.activity'].search_count([])
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: create next activity action correctly finished should return False')
self.assertEqual(self.env['mail.activity'].search_count([]), before_count + 1)
self.assertRecordValues(
self.env['mail.activity'].search([('res_model', '=', 'res.partner'), ('res_id', '=', self.test_partner.id)]),
[{
'summary': 'TestNew',
'user_id': self.user_demo.id, # the first user found
}],
)
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
def test_action_send_mail_without_mail_thread(self):
""" Check running a server action to send an email with custom layout on a non mail.thread model """
no_thread_record = self.env['mail.test.nothread'].create({'name': 'Test NoMailThread', 'customer_id': self.test_partner.id})
no_thread_template = self._create_template(
'mail.test.nothread',
{
'email_from': 'someone@example.com',
'partner_to': '{{ object.customer_id.id }}',
'subject': 'About {{ object.name }}',
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'email_layout_xmlid': 'mail.mail_notification_layout',
}
)
# update action: send an email
self.action.write({
'mail_post_method': 'email',
'state': 'mail_post',
'model_id': self.env['ir.model'].search([('model', '=', 'mail.test.nothread')], limit=1).id,
'model_name': 'mail.test.nothread',
'template_id': no_thread_template.id,
})
with self.mock_mail_gateway(), self.mock_mail_app():
action_ctx = {
'active_model': 'mail.test.nothread',
'active_id': no_thread_record.id,
}
self.action.with_context(action_ctx).run()
mail = self.assertMailMail(
self.test_partner,
None,
content='Hello Test NoMailThread',
fields_values={
'email_from': 'someone@example.com',
'subject': 'About Test NoMailThread',
}
)
self.assertNotIn('Powered by', mail.body_html, 'Body should contain the notification layout')

View file

@ -0,0 +1,61 @@
import base64
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests import tagged, users
@tagged("ir_attachment")
class TestAttachment(MailCommon):
@users("employee")
def test_register_as_main_attachment(self):
""" Test 'register_as_main_attachment', especially the multi support """
records_model1 = self.env["mail.test.simple.main.attachment"].create([
{
"name": f"First model {idx}",
}
for idx in range(5)
])
records_model2 = self.env["mail.test.gateway.main.attachment"].create([
{
"name": f"Second model {idx}",
}
for idx in range(5)
])
record_nomain = self.env["mail.test.simple"].create({"name": "No Main Attachment"})
attachments = self.env["ir.attachment"].create([
{
"datas": base64.b64encode(b'AttContent'),
"name": f"AttachName_{record.name}.pdf",
"mimetype": "application/pdf",
"res_id": record.id,
"res_model": record._name,
}
for record in records_model1
] + [
{
"datas": base64.b64encode(b'AttContent'),
"name": f"AttachName_{record.name}.pdf",
"mimetype": "application/pdf",
"res_id": record.id,
"res_model": record._name,
}
for record in records_model2
] + [
{
"datas": base64.b64encode(b'AttContent'),
"name": "AttachName_free.pdf",
"mimetype": "application/pdf",
}, {
"datas": base64.b64encode(b'AttContent'),
"name": f"AttachName_{record_nomain.name}.pdf",
"mimetype": "application/pdf",
"res_id": record_nomain.id,
"res_model": record_nomain._name,
}
])
attachments.register_as_main_attachment()
for record, attachment in zip(records_model1, attachments[:5]):
self.assertEqual(record.message_main_attachment_id, attachment)
for record, attachment in zip(records_model2, attachments[5:10]):
self.assertEqual(record.message_main_attachment_id, attachment)

View file

@ -0,0 +1,730 @@
from datetime import date, datetime, timedelta, timezone
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from unittest.mock import patch
import pytz
import random
from odoo import fields, tests
from odoo.addons.mail.models.mail_activity import MailActivity
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_mail.tests.test_mail_activity import TestActivityCommon
from odoo.tests import tagged, users
from odoo.tools import mute_logger
@tagged('mail_activity', 'mail_activity_mixin')
class TestActivityMixin(TestActivityCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_utc = mail_new_test_user(
cls.env,
name='User UTC',
login='User UTC',
)
cls.user_utc.tz = 'UTC'
cls.user_australia = mail_new_test_user(
cls.env,
name='user Australia',
login='user Australia',
)
cls.user_australia.tz = 'Australia/Sydney'
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin(self):
self.user_employee.tz = self.user_admin.tz
with self.with_user('employee'):
self.test_record = self.env['mail.test.activity'].browse(self.test_record.id)
self.assertEqual(len(self.test_record.message_ids), 1)
self.assertEqual(self.test_record.env.user, self.user_employee)
now_utc = datetime.now(pytz.UTC)
now_user = now_utc.astimezone(pytz.timezone(self.env.user.tz or 'UTC'))
today_user = now_user.date()
# Test various scheduling of activities
act1 = self.test_record.activity_schedule(
'test_mail.mail_act_test_todo',
today_user + relativedelta(days=1),
user_id=self.user_admin.id)
self.assertEqual(act1.automated, True)
act_type = self.env.ref('test_mail.mail_act_test_todo')
self.assertEqual(self.test_record.activity_summary, act_type.summary)
self.assertEqual(self.test_record.activity_state, 'planned')
self.assertEqual(self.test_record.activity_user_id, self.user_admin)
act2 = self.test_record.activity_schedule(
'test_mail.mail_act_test_meeting',
today_user + relativedelta(days=-1),
user_id=self.user_employee.id,
)
self.assertEqual(self.test_record.activity_state, 'overdue')
# `activity_user_id` is defined as `fields.Many2one('res.users', 'Responsible User', related='activity_ids.user_id')`
# it therefore relies on the natural order of `activity_ids`, according to which activity comes first.
# As we just created the activity, its not yet in the right order.
# We force it by invalidating it so it gets fetched from database, in the right order.
self.test_record.invalidate_recordset(['activity_ids'])
self.assertEqual(self.test_record.activity_user_id, self.user_employee)
act3 = self.test_record.activity_schedule(
'test_mail.mail_act_test_todo',
today_user + relativedelta(days=3),
user_id=self.user_employee.id,
)
self.assertEqual(self.test_record.activity_state, 'overdue')
# `activity_user_id` is defined as `fields.Many2one('res.users', 'Responsible User', related='activity_ids.user_id')`
# it therefore relies on the natural order of `activity_ids`, according to which activity comes first.
# As we just created the activity, its not yet in the right order.
# We force it by invalidating it so it gets fetched from database, in the right order.
self.test_record.invalidate_recordset(['activity_ids'])
self.assertEqual(self.test_record.activity_user_id, self.user_employee)
self.test_record.invalidate_recordset()
self.assertEqual(self.test_record.activity_ids, act1 | act2 | act3)
# Perform todo activities for admin
self.test_record.activity_feedback(
['test_mail.mail_act_test_todo'],
user_id=self.user_admin.id,
feedback='Test feedback 1',
)
self.assertEqual(self.test_record.activity_ids, act2 | act3)
self.assertFalse(act1.active)
# Reschedule all activities, should update the record state
self.assertEqual(self.test_record.activity_state, 'overdue')
self.test_record.activity_reschedule(
['test_mail.mail_act_test_meeting', 'test_mail.mail_act_test_todo'],
date_deadline=today_user + relativedelta(days=3)
)
self.assertEqual(self.test_record.activity_state, 'planned')
# Perform todo activities for remaining people
self.test_record.activity_feedback(
['test_mail.mail_act_test_todo'],
feedback='Test feedback 2')
self.assertFalse(act3.active)
# Setting activities as done should delete them and post messages
self.assertEqual(self.test_record.activity_ids, act2)
self.assertEqual(len(self.test_record.message_ids), 3)
self.assertEqual(len(self.test_record.message_ids), 3)
feedback2, feedback1, _create_log = self.test_record.message_ids
self.assertEqual((feedback2 + feedback1).subtype_id, self.env.ref('mail.mt_activities'))
# Unlink meeting activities
self.test_record.activity_unlink(['test_mail.mail_act_test_meeting'])
# Canceling activities should simply remove them
self.assertEqual(self.test_record.activity_ids, self.env['mail.activity'])
self.assertEqual(len(self.test_record.message_ids), 3, 'Should not produce additional message')
self.assertFalse(self.test_record.activity_state)
self.assertFalse(act2.exists())
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin_not_only_automated(self):
# Schedule activity and create manual activity
act_type_todo = self.env.ref('test_mail.mail_act_test_todo')
auto_act = self.test_record.activity_schedule(
'test_mail.mail_act_test_todo',
date_deadline=date.today() + relativedelta(days=1),
)
man_act = self.env['mail.activity'].create({
'activity_type_id': act_type_todo.id,
'res_id': self.test_record.id,
'res_model_id': self.env['ir.model']._get_id(self.test_record._name),
'date_deadline': date.today() + relativedelta(days=1)
})
self.assertEqual(auto_act.automated, True)
self.assertEqual(man_act.automated, False)
# Test activity reschedule on not only automated activities
self.test_record.activity_reschedule(
['test_mail.mail_act_test_todo'],
date_deadline=date.today() + relativedelta(days=2),
only_automated=False
)
self.assertEqual(auto_act.date_deadline, date.today() + relativedelta(days=2))
self.assertEqual(man_act.date_deadline, date.today() + relativedelta(days=2))
# Test activity feedback on not only automated activities
self.test_record.activity_feedback(
['test_mail.mail_act_test_todo'],
feedback='Test feedback',
only_automated=False
)
self.assertEqual(self.test_record.activity_ids, self.env['mail.activity'])
self.assertFalse(auto_act.active)
self.assertFalse(man_act.active)
# Test activity unlink on not only automated activities
auto_act = self.test_record.activity_schedule(
'test_mail.mail_act_test_todo',
)
man_act = self.env['mail.activity'].create({
'activity_type_id': act_type_todo.id,
'res_id': self.test_record.id,
'res_model_id': self.env['ir.model']._get_id(self.test_record._name)
})
self.test_record.activity_unlink(['test_mail.mail_act_test_todo'], only_automated=False)
self.assertEqual(self.test_record.activity_ids, self.env['mail.activity'])
self.assertFalse(auto_act.exists())
self.assertFalse(man_act.exists())
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin_archive(self):
rec = self.test_record.with_user(self.user_employee)
new_act = rec.activity_schedule(
'test_mail.mail_act_test_todo',
user_id=self.user_admin.id,
)
self.assertEqual(rec.activity_ids, new_act)
rec.action_archive()
self.assertEqual(rec.active, False)
self.assertEqual(rec.activity_ids, new_act)
rec.action_unarchive()
self.assertEqual(rec.active, True)
self.assertEqual(rec.activity_ids, new_act)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin_archive_user(self):
"""
Test when archiving an user, we unlink all his related activities
"""
test_users = self.env['res.users']
for i in range(5):
test_users += mail_new_test_user(self.env, name=f'test_user_{i}', login=f'test_password_{i}')
for user in test_users:
self.test_record.activity_schedule(user_id=user.id)
archived_users = self.env['res.users'].browse(x.id for x in random.sample(test_users, 2)) # pick 2 users to archive
archived_users.action_archive()
active_users = test_users - archived_users
# archive user with company disabled
user_admin = self.user_admin
user_employee_c2 = self.user_employee_c2
self.assertIn(self.company_2, user_admin.company_ids)
self.test_record.env['ir.rule'].create({
'model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'domain_force': "[('company_id', 'in', company_ids)]"
})
self.test_record.activity_schedule(user_id=user_employee_c2.id)
user_employee_c2.with_user(user_admin).with_context(
allowed_company_ids=(user_admin.company_ids - self.company_2).ids
).action_archive()
archived_users += user_employee_c2
self.assertFalse(any(archived_users.mapped('active')), "Users should be archived.")
# activities of active users shouldn't be touched, each has exactly 1 activity present
activities = self.env['mail.activity'].search([('user_id', 'in', active_users.ids)])
self.assertEqual(len(activities), 3, "We should have only 3 activities in total linked to our active users")
self.assertEqual(activities.mapped('user_id'), active_users,
"We should have 3 different users linked to the activities of the active users")
# ensure the user's activities are removed
activities = self.env['mail.activity'].search([('user_id', 'in', archived_users.ids)])
self.assertFalse(activities, "Activities of archived users should be deleted.")
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin_reschedule_user(self):
rec = self.test_record.with_user(self.user_employee)
rec.activity_schedule(
'test_mail.mail_act_test_todo',
user_id=self.user_admin.id)
self.assertEqual(rec.activity_ids[0].user_id, self.user_admin)
# reschedule its own should not alter other's activities
rec.activity_reschedule(
['test_mail.mail_act_test_todo'],
user_id=self.user_employee.id,
new_user_id=self.user_employee.id)
self.assertEqual(rec.activity_ids[0].user_id, self.user_admin)
rec.activity_reschedule(
['test_mail.mail_act_test_todo'],
user_id=self.user_admin.id,
new_user_id=self.user_employee.id)
self.assertEqual(rec.activity_ids[0].user_id, self.user_employee)
@users('employee')
def test_feedback_w_attachments(self):
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
activity = self.env['mail.activity'].create({
'activity_type_id': 1,
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
attachments = self.env['ir.attachment'].create([{
'name': 'test',
'res_name': 'test',
'res_model': 'mail.activity',
'res_id': activity.id,
'datas': 'test',
}, {
'name': 'test2',
'res_name': 'test',
'res_model': 'mail.activity',
'res_id': activity.id,
'datas': 'testtest',
}])
# Checking if the attachment has been forwarded to the message
# when marking an activity as "Done"
activity.action_feedback()
activity_message = test_record.message_ids[0]
self.assertEqual(set(activity_message.attachment_ids.ids), set(attachments.ids))
for attachment in attachments:
self.assertEqual(attachment.res_id, activity_message.id)
self.assertEqual(attachment.res_model, activity_message._name)
@users('employee')
def test_feedback_chained_current_date(self):
frozen_now = datetime(2021, 10, 10, 14, 30, 15)
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
first_activity = self.env['mail.activity'].create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_chained_1').id,
'date_deadline': frozen_now + relativedelta(days=-2),
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
first_activity_id = first_activity.id
with freeze_time(frozen_now):
first_activity.action_feedback(feedback='Done')
self.assertFalse(first_activity.active)
# check chained activity
new_activity = test_record.activity_ids
self.assertNotEqual(new_activity.id, first_activity_id)
self.assertEqual(new_activity.summary, 'Take the second step.')
self.assertEqual(new_activity.date_deadline, frozen_now.date() + relativedelta(days=10))
@users('employee')
def test_feedback_chained_previous(self):
self.env.ref('test_mail.mail_act_test_chained_2').sudo().write({'delay_from': 'previous_activity'})
frozen_now = datetime(2021, 10, 10, 14, 30, 15)
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
first_activity = self.env['mail.activity'].create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_chained_1').id,
'date_deadline': frozen_now + relativedelta(days=-2),
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
first_activity_id = first_activity.id
with freeze_time(frozen_now):
first_activity.action_feedback(feedback='Done')
self.assertFalse(first_activity.active)
# check chained activity
new_activity = test_record.activity_ids
self.assertNotEqual(new_activity.id, first_activity_id)
self.assertEqual(new_activity.summary, 'Take the second step.')
self.assertEqual(new_activity.date_deadline, frozen_now.date() + relativedelta(days=8),
'New deadline should take into account original activity deadline, not current date')
def test_mail_activity_state(self):
"""Create 3 activity for 2 different users in 2 different timezones.
User UTC (+0h)
User Australia (+11h)
Today datetime: 1/1/2020 16h
Activity 1 & User UTC
1/1/2020 - 16h UTC -> The state is today
Activity 2 & User Australia
1/1/2020 - 16h UTC
2/1/2020 - 1h Australia -> State is overdue
Activity 3 & User UTC
1/1/2020 - 23h UTC -> The state is today
"""
record = self.env['mail.test.activity'].create({'name': 'Record'})
with freeze_time(datetime(2020, 1, 1, 16)):
today_utc = datetime.today()
activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': 1,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': record.id,
'date_deadline': today_utc,
'user_id': self.user_utc.id,
})
activity_2 = activity_1.copy()
activity_2.user_id = self.user_australia
activity_3 = activity_1.copy()
activity_3.date_deadline += relativedelta(hours=7)
self.assertEqual(activity_1.state, 'today')
self.assertEqual(activity_2.state, 'overdue')
self.assertEqual(activity_3.state, 'today')
@users('employee')
def test_mail_activity_mixin_search_activity_user_id_false(self):
"""Test the search method on the "activity_user_id" when searching for non-set user"""
MailTestActivity = self.env['mail.test.activity']
test_records = self.test_record | self.test_record_2
self.assertFalse(test_records.activity_ids)
self.assertEqual(MailTestActivity.search([('activity_user_id', '=', False)]), test_records)
self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': self.test_record.id,
})
self.assertEqual(MailTestActivity.search([('activity_user_id', '!=', True)]), self.test_record_2)
def test_mail_activity_mixin_search_exception_decoration(self):
"""Test the search on "activity_exception_decoration".
Domain ('activity_exception_decoration', '!=', False) should only return
records that have at least one warning/danger activity.
"""
record_warning, record_normal, _ = self.test_record, self.test_record_2, self.env['mail.test.activity'].create({'name': 'No activities'})
record_warning.activity_schedule('mail.mail_activity_data_warning', user_id=self.env.user.id)
record_normal.activity_schedule('test_mail.mail_act_test_todo', user_id=self.env.user.id)
records = self.env['mail.test.activity'].search([('activity_exception_decoration', '!=', False)])
self.assertEqual(records, record_warning)
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
def test_mail_activity_mixin_search_state_basic(self):
"""Test the search method on the "activity_state".
Test all the operators and also test the case where the "activity_state" is
different because of the timezone. There's also a tricky case for which we
"reverse" the domain for performance purpose.
"""
# Create some records without activity schedule on it for testing
self.env['mail.test.activity'].create([
{'name': 'Record %i' % record_i}
for record_i in range(5)
])
origin_1, origin_2 = self.env['mail.test.activity'].search([], limit=2)
activity_type = self.env.ref('test_mail.mail_act_test_todo')
with freeze_time(datetime(2020, 1, 1, 16)):
today_utc = datetime.today()
origin_1_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': activity_type.id,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_1.id,
'date_deadline': today_utc,
'user_id': self.user_utc.id,
})
origin_1_activity_2 = origin_1_activity_1.copy()
origin_1_activity_2.user_id = self.user_australia
origin_1_activity_3 = origin_1_activity_1.copy()
origin_1_activity_3.date_deadline += relativedelta(hours=8)
self.assertEqual(origin_1_activity_1.state, 'today')
self.assertEqual(origin_1_activity_2.state, 'overdue')
self.assertEqual(origin_1_activity_3.state, 'today')
origin_2_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': activity_type.id,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_2.id,
'date_deadline': today_utc + relativedelta(hours=8),
'user_id': self.user_utc.id,
})
origin_2_activity_2 = origin_2_activity_1.copy()
origin_2_activity_2.user_id = self.user_australia
origin_2_activity_3 = origin_2_activity_1.copy()
origin_2_activity_3.date_deadline -= relativedelta(hours=8)
origin_2_activity_4 = origin_2_activity_1.copy()
origin_2_activity_4.date_deadline = datetime(2020, 1, 2, 0, 0, 0)
self.assertEqual(origin_2_activity_1.state, 'planned')
self.assertEqual(origin_2_activity_2.state, 'today')
self.assertEqual(origin_2_activity_3.state, 'today')
self.assertEqual(origin_2_activity_4.state, 'planned')
all_activity_mixin_record = self.env['mail.test.activity'].search([])
result = self.env['mail.test.activity'].search([('activity_state', '=', 'today')])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state == 'today'))
result = self.env['mail.test.activity'].search([('activity_state', 'in', ('today', 'overdue'))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state in ('today', 'overdue')))
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('today'))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state != 'today'))
result = self.env['mail.test.activity'].search([('activity_state', '=', False)])
self.assertTrue(len(result) >= 3, "There is more than 3 records without an activity schedule on it")
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: not p.activity_state))
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('planned', 'overdue', 'today'))])
self.assertTrue(len(result) >= 3, "There is more than 3 records without an activity schedule on it")
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: not p.activity_state))
# test tricky case when the domain will be reversed in the search method
# because of falsy value
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('today', False))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state not in ('today', False)))
result = self.env['mail.test.activity'].search([('activity_state', 'in', ('today', False))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state in ('today', False)))
# Check that activity done are not taken into account by group and search by activity_state.
Model = self.env['mail.test.activity']
search_params = {
'domain': [('id', 'in', (origin_1 | origin_2).ids), ('activity_state', '=', 'overdue')]}
read_group_params = {
'domain': [('id', 'in', (origin_1 | origin_2).ids)],
'groupby': ['activity_state'],
'aggregates': ['__count'],
}
self.assertEqual(Model.search(**search_params), origin_1)
self.assertEqual(
{(e['activity_state'], e['__count']) for e in Model.formatted_read_group(**read_group_params)},
{('today', 1), ('overdue', 1)})
origin_1_activity_2.action_feedback(feedback='Done')
self.assertFalse(Model.search(**search_params))
self.assertEqual(
{(e['activity_state'], e['__count']) for e in Model.formatted_read_group(**read_group_params)},
{('today', 2)})
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
def test_mail_activity_mixin_search_state_different_day_but_close_time(self):
"""Test the case where there's less than 24 hours between the deadline and now_tz,
but one day of difference (e.g. 23h 01/01/2020 & 1h 02/02/2020). So the state
should be "planned" and not "today". This case was tricky to implement in SQL
that's why it has its own test.
"""
# Create some records without activity schedule on it for testing
self.env['mail.test.activity'].create([
{'name': 'Record %i' % record_i}
for record_i in range(5)
])
origin_1 = self.env['mail.test.activity'].search([], limit=1)
with freeze_time(datetime(2020, 1, 1, 23)):
today_utc = datetime.today()
origin_1_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': 1,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_1.id,
'date_deadline': today_utc + relativedelta(hours=2),
'user_id': self.user_utc.id,
})
self.assertEqual(origin_1_activity_1.state, 'planned')
result = self.env['mail.test.activity'].search([('activity_state', '=', 'today')])
self.assertNotIn(origin_1, result, 'The activity state miss calculated during the search')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_my_activity_flow_employee(self):
Activity = self.env['mail.activity']
date_today = date.today()
Activity.create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
'date_deadline': date_today,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': self.test_record.id,
'user_id': self.user_admin.id,
})
Activity.create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_call').id,
'date_deadline': date_today + relativedelta(days=1),
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': self.test_record.id,
'user_id': self.user_employee.id,
})
test_record_1 = self.env['mail.test.activity'].with_context(self._test_context).create({'name': 'Test 1'})
test_record_1_late_activity = Activity.create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
'date_deadline': date_today,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': test_record_1.id,
'user_id': self.user_employee.id,
})
with self.with_user('employee'):
record = self.env['mail.test.activity'].search([('my_activity_date_deadline', '=', date_today)])
self.assertEqual(test_record_1, record)
test_record_1_late_activity._action_done()
record = self.env['mail.test.activity'].with_context(active_test=False).search([
('my_activity_date_deadline', '=', date_today)
])
self.assertFalse(record, "Should not find record if the only late activity is done")
@users('employee')
def test_record_unlink(self):
test_record = self.test_record.with_user(self.env.user)
act1 = test_record.activity_schedule(summary='Active', user_id=self.env.uid)
act2 = test_record.activity_schedule(summary='Archived', active=False, user_id=self.env.uid)
test_record.unlink()
self.assertFalse((act1 + act2).exists(), 'Removing records should remove activities, even archived')
@users('employee')
def test_record_unlinked_orphan_activities(self):
"""Test the fix preventing error on corrupted database where activities without related record are present."""
test_record = self.env['mail.test.activity'].with_context(
self._test_context).create({'name': 'Test'}).with_user(self.user_employee)
act = test_record.activity_schedule("test_mail.mail_act_test_todo", summary='Orphan activity')
act.action_done()
# Delete the record while preventing the cascade deletion of the activity to simulate a corrupted database
with patch.object(MailActivity, 'unlink', lambda self: None):
test_record.unlink()
self.assertTrue(act.exists())
self.assertFalse(act.active)
self.assertFalse(test_record.exists())
self.env.invalidate_all()
self.assertEqual(
self.env['mail.activity'].with_user(self.user_admin).with_context(active_test=False).search(
[('active', '=', False)]), act,
'Should consider unassigned activity on removed record = access without crash'
)
self.env.invalidate_all()
_dummy = act.with_user(self.user_admin).read(['summary'])
@tests.tagged('mail_activity', 'mail_activity_mixin')
class TestORM(TestActivityCommon):
"""Test for read_progress_bar"""
def test_groupby_activity_state_progress_bar_behavior(self):
""" Test activity_state groupby logic on mail.test.lead when 'activity_state'
is present multiple times in the groupby field list. """
lead_timedelta_setup = [0, 0, -2, -2, -2, 2]
leads = self.env["mail.test.lead"].create([
{"name": f"CRM Lead {i}"}
for i in range(1, len(lead_timedelta_setup) + 1)
])
with freeze_time("2025-05-21 10:00:00"):
self.env["mail.activity"].create([
{
"date_deadline": datetime.now(timezone.utc) + timedelta(days=delta_days),
"res_id": lead.id,
"res_model_id": self.env["ir.model"]._get_id("mail.test.lead"),
"summary": f"Test activity for CRM lead {lead.id}",
"user_id": self.env.user.id,
} for lead, delta_days in zip(leads, lead_timedelta_setup)
])
# grouping by 'activity_state' and 'activity_state' as the progress bar
domain = [("name", "!=", "")]
groupby = "activity_state"
progress_bar = {
"field": "activity_state",
"colors": {
"overdue": "danger",
"today": "warning",
"planned": "success",
},
}
progressbars = self.env["mail.test.lead"].read_progress_bar(
domain=domain, group_by=groupby, progress_bar=progress_bar,
)
self.assertEqual(len(progressbars), 3)
expected_progressbars = {
"overdue": {"overdue": 3, "today": 0, "planned": 0},
"today": {"overdue": 0, "today": 2, "planned": 0},
"planned": {"overdue": 0, "today": 0, "planned": 1},
}
self.assertEqual(dict(progressbars), expected_progressbars)
def test_week_grouping(self):
"""The labels associated to each record in read_progress_bar should match
the ones from read_group, even in edge cases like en_US locale on sundays
"""
MailTestActivityCtx = self.env['mail.test.activity'].with_context({"lang": "en_US"})
# Don't mistake fields date and date_deadline:
# * date is just a random value
# * date_deadline defines activity_state
with freeze_time("2024-09-24 10:00:00"):
self.env['mail.test.activity'].create({
'date': '2021-05-02',
'name': "Yesterday, all my troubles seemed so far away",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Make another test super asap (yesterday)",
date_deadline=fields.Date.context_today(MailTestActivityCtx) - timedelta(days=7),
user_id=self.env.uid,
)
self.env['mail.test.activity'].create({
'date': '2021-05-09',
'name': "Things we said today",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Make another test asap",
date_deadline=fields.Date.context_today(MailTestActivityCtx),
user_id=self.env.uid,
)
self.env['mail.test.activity'].create({
'date': '2021-05-16',
'name': "Tomorrow Never Knows",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Make a test tomorrow",
date_deadline=fields.Date.context_today(MailTestActivityCtx) + timedelta(days=7),
user_id=self.env.uid,
)
domain = [('date', "!=", False)]
groupby = "date:week"
progress_bar = {
'field': 'activity_state',
'colors': {
"overdue": 'danger',
"today": 'warning',
"planned": 'success',
}
}
# call read_group to compute group names
groups = MailTestActivityCtx.formatted_read_group(domain, groupby=[groupby])
progressbars = MailTestActivityCtx.read_progress_bar(domain, group_by=groupby, progress_bar=progress_bar)
self.assertEqual(len(groups), 3)
self.assertEqual(len(progressbars), 3)
# format the read_progress_bar result to get a dictionary under this
# format: {activity_state: group_name}; the original format
# (after read_progress_bar) is {group_name: {activity_state: count}}
pg_groups = {
next(state for state, count in data.items() if count): group_name
for group_name, data in progressbars.items()
}
self.assertEqual(groups[0][groupby][0], pg_groups["overdue"])
self.assertEqual(groups[1][groupby][0], pg_groups["today"])
self.assertEqual(groups[2][groupby][0], pg_groups["planned"])

View file

@ -0,0 +1,410 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo import fields
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common_activity import ActivityScheduleCase
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form, tagged, users
from odoo.tools.misc import format_date
@tagged('mail_activity', 'mail_activity_plan')
class TestActivitySchedule(ActivityScheduleCase):
""" Test plan and activity schedule
- activity scheduling on a single record and in batch
- plan scheduling on a single record and in batch
- plan creation and consistency
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# add some triggered and suggested next activitities
cls.test_type_1, cls.test_type_2, cls.test_type_3 = cls.env['mail.activity.type'].create([
{'name': 'TestAct1', 'res_model': 'mail.test.activity',},
{'name': 'TestAct2', 'res_model': 'mail.test.activity',},
{'name': 'TestAct3', 'res_model': 'mail.test.activity',},
])
cls.test_type_1.write({
'chaining_type': 'trigger',
'delay_count': 2,
'delay_from': 'current_date',
'delay_unit': 'days',
'triggered_next_type_id': cls.test_type_2.id,
})
cls.test_type_2.write({
'chaining_type': 'suggest',
'delay_count': 3,
'delay_unit': 'weeks',
'suggested_next_type_ids': [(4, cls.test_type_1.id), (4, cls.test_type_3.id)],
})
# prepare plans
cls.plan_party = cls.env['mail.activity.plan'].create({
'name': 'Test Plan A Party',
'res_model': 'mail.test.activity',
'template_ids': [
(0, 0, {
'activity_type_id': cls.activity_type_todo.id,
'delay_count': 1,
'delay_from': 'before_plan_date',
'delay_unit': 'days',
'responsible_type': 'on_demand',
'sequence': 10,
'summary': 'Book a place',
}), (0, 0, {
'activity_type_id': cls.activity_type_todo.id,
'delay_count': 1,
'delay_from': 'after_plan_date',
'delay_unit': 'weeks',
'responsible_id': cls.user_admin.id,
'responsible_type': 'other',
'sequence': 20,
'summary': 'Invite special guest',
}),
],
})
cls.plan_onboarding = cls.env['mail.activity.plan'].create({
'name': 'Test Onboarding',
'res_model': 'mail.test.activity',
'template_ids': [
(0, 0, {
'activity_type_id': cls.activity_type_todo.id,
'delay_count': 3,
'delay_from': 'before_plan_date',
'delay_unit': 'days',
'responsible_id': cls.user_admin.id,
'responsible_type': 'other',
'sequence': 10,
'summary': 'Plan training',
}), (0, 0, {
'activity_type_id': cls.activity_type_todo.id,
'delay_count': 2,
'delay_from': 'after_plan_date',
'delay_unit': 'weeks',
'responsible_id': cls.user_admin.id,
'responsible_type': 'other',
'sequence': 20,
'summary': 'Training',
}),
]
})
# test records
cls.reference_now = fields.Datetime.from_string('2023-09-30 14:00:00')
cls.test_records = cls.env['mail.test.activity'].create([
{
'date': cls.reference_now + timedelta(days=(idx - 10)),
'email_from': f'customer.activity.{idx}@test.example.com',
'name': f'test_record_{idx}'
} for idx in range(5)
])
# some big dict comparisons
cls.maxDiff = None
@users('employee')
def test_activity_schedule(self):
""" Test schedule of an activity on a single or multiple records. """
test_records_all = [self.test_records[0], self.test_records[:3]]
# sanity check: new activity created without specifying activiy type
# will have default type of the available activity type with the lowest sequence, then lowest id
self.assertTrue(self.activity_type_todo.sequence < self.activity_type_call.sequence)
for test_idx, test_case in enumerate(['mono', 'multi']):
test_records = test_records_all[test_idx].with_env(self.env)
with self.subTest(test_case=test_case, test_records=test_records):
# 1. SCHEDULE ACTIVITIES
with freeze_time(self.reference_now):
form = self._instantiate_activity_schedule_wizard(test_records)
form.summary = 'Write specification'
form.note = '<p>Useful link ...</p>'
form.activity_user_id = self.user_admin
with self._mock_activities():
form.save().action_schedule_activities()
for record in test_records:
self.assertActivityCreatedOnRecord(record, {
'activity_type_id': self.activity_type_todo,
'automated': False,
'date_deadline': self.reference_now.date() + timedelta(days=4), # activity type delay
'note': '<p>Useful link ...</p>',
'summary': 'Write specification',
'user_id': self.user_admin,
})
# 2. LOG DONE ACTIVITIES
with freeze_time(self.reference_now):
form = self._instantiate_activity_schedule_wizard(test_records)
form.activity_type_id = self.activity_type_call
form.activity_user_id = self.user_admin
with self._mock_activities(), freeze_time(self.reference_now):
form.save().with_context(
mail_activity_quick_update=True
).action_schedule_activities_done()
for record in test_records:
self.assertActivityDoneOnRecord(record, self.activity_type_call)
# 3. CONTINUE WITH SCHEDULE ACTIVITIES
# implies deadline addition on top of previous activities
with freeze_time(self.reference_now):
form = self._instantiate_activity_schedule_wizard(test_records)
form.activity_type_id = self.activity_type_call
form.activity_user_id = self.user_admin
with self._mock_activities():
form.save().with_context(
mail_activity_quick_update=True
).action_schedule_activities()
for record in test_records:
self.assertActivityCreatedOnRecord(record, {
'activity_type_id': self.activity_type_call,
'automated': False,
'date_deadline': self.reference_now.date() + timedelta(days=1), # activity call delay
'note': False,
'summary': 'TodoSumCallSummary',
'user_id': self.user_admin,
})
# global activity creation from tests
self.assertEqual(len(self.test_records[0].activity_ids), 4)
self.assertEqual(len(self.test_records[1].activity_ids), 2)
self.assertEqual(len(self.test_records[2].activity_ids), 2)
self.assertEqual(len(self.test_records[3].activity_ids), 0)
self.assertEqual(len(self.test_records[4].activity_ids), 0)
@users('admin')
def test_activity_schedule_rights_upload(self):
user = mail_new_test_user(
self.env,
groups='base.group_public',
login='bert',
name='Bert Tartignole',
)
demo_record = self.env['mail.test.access'].create({'access': 'admin', 'name': 'Record'})
form = self._instantiate_activity_schedule_wizard(demo_record)
form.activity_type_id = self.env.ref('test_mail.mail_act_test_upload_document')
with self.assertRaises(UserError):
form.activity_user_id = user
form.save()
@users('employee')
def test_activity_schedule_norecord(self):
""" Test scheduling free activities, supported if assigned user. """
scheduler = self._instantiate_activity_schedule_wizard(None)
self.assertEqual(scheduler.activity_type_id, self.activity_type_todo)
with self._mock_activities():
scheduler.save().action_schedule_activities()
self.assertActivityValues(self._new_activities, {
'res_id': False,
'res_model': False,
'summary': 'TodoSummary',
'user_id': self.user_employee,
})
# cannot scheduler unassigned personal activities
scheduler = self._instantiate_activity_schedule_wizard(None)
scheduler = scheduler.save()
with self.assertRaises(ValidationError):
scheduler.activity_user_id = False
def test_plan_copy(self):
"""Test plan copy"""
copied_plan = self.plan_onboarding.copy()
self.assertEqual(copied_plan.name, f'{self.plan_onboarding.name} (copy)')
self.assertEqual(len(copied_plan.template_ids), len(self.plan_onboarding.template_ids))
@users('employee')
def test_plan_mode(self):
""" Test the plan_mode that allows to preselect a compatible plan. """
test_record = self.test_records[0].with_env(self.env)
context = {
'active_id': test_record.id,
'active_ids': test_record.ids,
'active_model': test_record._name
}
plan_mode_context = {**context, 'plan_mode': True}
with Form(self.env['mail.activity.schedule'].with_context(context)) as form:
self.assertFalse(form.plan_id)
with Form(self.env['mail.activity.schedule'].with_context(plan_mode_context)) as form:
self.assertEqual(form.plan_id, self.plan_party)
# should select only model-plans
self.plan_party.res_model = 'res.partner'
with Form(self.env['mail.activity.schedule'].with_context(plan_mode_context)) as form:
self.assertEqual(form.plan_id, self.plan_onboarding)
@users('admin')
def test_plan_next_activities(self):
""" Test that next activities are displayed correctly. """
test_plan = self.env['mail.activity.plan'].create({
'name': 'Test Plan',
'res_model': 'mail.test.activity',
'template_ids': [
(0, 0, {'activity_type_id': self.test_type_1.id}),
(0, 0, {'activity_type_id': self.test_type_2.id}),
(0, 0, {'activity_type_id': self.test_type_3.id}),
],
})
# Assert expected next activities
expected_next_activities = [['TestAct2'], ['TestAct1', 'TestAct3'], []]
for template, expected_names in zip(test_plan.template_ids, expected_next_activities, strict=True):
self.assertEqual(template.next_activity_ids.mapped('name'), expected_names)
# Test the plan summary
with self.subTest(test_case='Check plan summary'), \
freeze_time(self.reference_now):
form = self._instantiate_activity_schedule_wizard(self.test_records[0])
form.plan_id = test_plan
expected_values = [
{'description': 'TestAct1', 'deadline': datetime(2023, 9, 30).date()},
{'description': 'TestAct2', 'deadline': datetime(2023, 10, 21).date()},
{'description': 'TestAct2', 'deadline': datetime(2023, 9, 30).date()},
{'description': 'TestAct1', 'deadline': datetime(2023, 10, 2).date()},
{'description': 'TestAct3', 'deadline': datetime(2023, 9, 30).date()},
{'description': 'TestAct3', 'deadline': datetime(2023, 9, 30).date()},
]
for line, expected in zip(form.plan_schedule_line_ids._records, expected_values):
with self.subTest(line=line, expected_values=expected):
self.assertEqual(line['line_description'], expected['description'])
self.assertEqual(line['line_date_deadline'], expected['deadline'])
@users('employee')
def test_plan_schedule(self):
""" Test schedule of a plan on a single or multiple records. """
test_records_all = [self.test_records[0], self.test_records[:3]]
for test_idx, test_case in enumerate(['mono', 'multi']):
test_records = test_records_all[test_idx].with_env(self.env)
with self.subTest(test_case=test_case, test_records=test_records), \
freeze_time(self.reference_now):
# No plan_date specified (-> self.reference_now is used), No responsible specified
form = self._instantiate_activity_schedule_wizard(test_records)
self.assertFalse(form.plan_schedule_line_ids)
form.plan_id = self.plan_onboarding
expected_values = [
{'description': 'Plan training', 'deadline': datetime(2023, 9, 27).date()},
{'description': 'Training', 'deadline': datetime(2023, 10, 14).date()},
]
for line, expected in zip(form.plan_schedule_line_ids._records, expected_values):
self.assertEqual(line['line_description'], expected['description'])
self.assertEqual(line['line_date_deadline'], expected['deadline'])
self.assertTrue(form._get_modifier('plan_on_demand_user_id', 'invisible'))
form.plan_id = self.plan_party
expected_values = [
{'description': 'Book a place', 'deadline': datetime(2023, 9, 29).date()},
{'description': 'Invite special guest', 'deadline': datetime(2023, 10, 7).date()},
]
for line, expected in zip(form.plan_schedule_line_ids._records, expected_values):
self.assertEqual(line['line_description'], expected['description'])
self.assertEqual(line['line_date_deadline'], expected['deadline'])
self.assertFalse(form._get_modifier('plan_on_demand_user_id', 'invisible'))
with self._mock_activities():
form.save().action_schedule_plan()
self.assertPlanExecution(
self.plan_party, test_records,
expected_deadlines=[(self.reference_now + relativedelta(days=-1)).date(),
(self.reference_now + relativedelta(days=7)).date()])
# plan_date specified, responsible specified
plan_date = self.reference_now.date() + relativedelta(days=14)
responsible_id = self.user_admin
form = self._instantiate_activity_schedule_wizard(test_records)
form.plan_id = self.plan_party
form.plan_date = plan_date
form.plan_on_demand_user_id = self.env['res.users']
self.assertTrue(form.has_error)
self.assertIn(f'No responsible specified for {self.activity_type_todo.name}: Book a place',
form.error)
form.plan_on_demand_user_id = responsible_id
self.assertFalse(form.has_error)
deadline_1 = plan_date + relativedelta(days=-1)
deadline_2 = plan_date + relativedelta(days=7)
expected_values = [
{'description': 'Book a place', 'deadline': deadline_1},
{'description': 'Invite special guest', 'deadline': deadline_2},
]
for line, expected in zip(form.plan_schedule_line_ids._records, expected_values):
self.assertEqual(line['line_description'], expected['description'])
self.assertEqual(line['line_date_deadline'], expected['deadline'])
with self._mock_activities():
form.save().action_schedule_plan()
self.assertPlanExecution(
self.plan_party, test_records,
expected_deadlines=[plan_date + relativedelta(days=-1),
plan_date + relativedelta(days=7)],
expected_responsible=responsible_id)
@users('admin')
def test_plan_setup_model_consistency(self):
""" Test the model consistency of a plan.
Model consistency between activity_type - activity_template - plan:
- a plan is restricted to a model
- a plan contains activity plan templates which can be limited to some model
through activity type
"""
# Setup independent activities type to avoid interference with existing data
activity_type_1, activity_type_2, activity_type_3 = self.env['mail.activity.type'].create([
{'name': 'Todo'},
{'name': 'Call'},
{'name': 'Partner-specific', 'res_model': 'res.partner'},
])
test_plan = self.env['mail.activity.plan'].create({
'name': 'Test Plan',
'res_model': 'mail.test.activity',
'template_ids': [
(0, 0, {'activity_type_id': activity_type_1.id}),
(0, 0, {'activity_type_id': activity_type_2.id})
],
})
# ok, all activities generic
test_plan.res_model = 'res.partner'
test_plan.res_model = 'mail.test.activity'
with self.assertRaises(
ValidationError,
msg='Cannot set activity type to res.partner as linked to a plan of another model'):
activity_type_1.res_model = 'res.partner'
activity_type_1.res_model = 'mail.test.activity'
with self.assertRaises(
ValidationError,
msg='Cannot set plan to res.partner as using activities linked to another model'):
test_plan.res_model = 'res.partner'
with self.assertRaises(
ValidationError,
msg='Cannot create activity template for res.partner as linked to a plan of another model'):
self.env['mail.activity.plan.template'].create({
'activity_type_id': activity_type_3.id,
'plan_id': test_plan.id,
})
@users('admin')
def test_plan_setup_validation(self):
""" Test plan consistency. """
plan = self.env['mail.activity.plan'].create({
'name': 'test',
'res_model': 'mail.test.activity',
})
template = self.env['mail.activity.plan.template'].create({
'activity_type_id': self.activity_type_todo.id,
'plan_id': plan.id,
'responsible_type': 'other',
'responsible_id': self.user_admin.id,
})
template.responsible_type = 'on_demand'
self.assertFalse(template.responsible_id)
with self.assertRaises(
ValidationError, msg='When selecting responsible "other", you must specify a responsible.'):
template.responsible_type = 'other'
template.write({'responsible_type': 'other', 'responsible_id': self.user_admin})

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
from odoo.addons.test_mail.tests.common import TestRecipients
from odoo.exceptions import AccessError
from odoo.tests import tagged
from odoo.tests.common import users
@tagged('mail_composer_mixin')
class TestMailComposerMixin(TestMailCommon, TestRecipients):
class TestMailComposerMixin(MailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestMailComposerMixin, cls).setUpClass()
# ensure employee can create partners, necessary for templates
cls.user_employee.write({
'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)],
})
super().setUpClass()
cls.mail_template = cls.env['mail.template'].create({
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
@ -30,6 +27,22 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
'customer_id': cls.partner_1.id,
})
# Enable group-based template management
cls.env['ir.config_parameter'].set_param('mail.restrict.template.rendering', True)
# User without the group "mail.group_mail_template_editor"
cls.user_rendering_restricted = mail_new_test_user(
cls.env,
company_id=cls.company_admin.id,
groups='base.group_user',
login='user_rendering_restricted',
name='Code Template Restricted User',
notification_type='inbox',
signature='--\nErnest'
)
cls.user_rendering_restricted.group_ids -= cls.env.ref('mail.group_mail_template_editor')
cls.user_employee.group_ids += cls.env.ref('mail.group_mail_template_editor')
cls._activate_multi_lang(
layout_arch_db='<body><t t-out="message.body"/> English Layout for <t t-esc="model_description"/></body>',
lang_code='es_ES',
@ -41,19 +54,86 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
def test_content_sync(self):
""" Test updating template updates the dynamic fields accordingly. """
source = self.test_record.with_env(self.env)
template = self.mail_template.with_env(self.env)
template_void = template.copy()
template_void.write({
'body_html': '<p><br /></p>',
'lang': False,
'subject': False,
})
composer = self.env['mail.test.composer.mixin'].create({
'name': 'Invite',
'template_id': template.id,
'source_ids': [(4, source.id)],
})
self.assertEqual(composer.body, template.body_html)
self.assertTrue(composer.body_has_template_value)
self.assertEqual(composer.lang, template.lang)
self.assertEqual(composer.subject, template.subject)
# check rendering
body = composer._render_field('body', source.ids)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
subject = composer._render_field('subject', source.ids)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}')
# manual values > template default values
composer.write({
'body': '<p>CustomBody for <t t-out="object.name"/></p>',
'subject': 'CustomSubject for {{ object.name }}',
})
self.assertFalse(composer.body_has_template_value)
body = composer._render_field('body', source.ids)[source.id]
self.assertEqual(body, f'<p>CustomBody for {source.name}</p>')
subject = composer._render_field('subject', source.ids)[source.id]
self.assertEqual(subject, f'CustomSubject for {source.name}')
# template with void values: should not force void (TODO)
composer.template_id = template_void.id
self.assertEqual(composer.body, '<p>CustomBody for <t t-out="object.name"/></p>')
self.assertFalse(composer.body_has_template_value)
self.assertEqual(composer.lang, template.lang)
self.assertEqual(composer.subject, 'CustomSubject for {{ object.name }}')
# reset template TOOD should reset
composer.write({'template_id': False})
self.assertFalse(composer.body)
self.assertFalse(composer.body_has_template_value)
self.assertFalse(composer.lang)
self.assertFalse(composer.subject)
@users("user_rendering_restricted")
def test_mail_composer_mixin_render_lang(self):
""" Test _render_lang when rendering is involved, depending on template
editor rights. """
source = self.test_record.with_env(self.env)
composer = self.env['mail.test.composer.mixin'].create({
'description': '<p>Description for <t t-esc="object.name"/></p>',
'name': 'Invite',
'template_id': self.mail_template.id,
'source_ids': [(4, source.id)],
})
self.assertEqual(composer.body, self.mail_template.body_html)
self.assertEqual(composer.subject, self.mail_template.subject)
self.assertFalse(composer.lang, 'Fixme: lang is not propagated currently')
subject = composer._render_field('subject', source.ids)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}')
body = composer._render_field('body', source.ids)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
# _render_lang should be ok when content is the same as template
rendered = composer._render_lang(source.ids)
self.assertEqual(rendered, {source.id: self.partner_1.lang})
# _render_lang should crash when content is dynamic and not coming from template
composer.lang = " {{ 'en_US' }}"
with self.assertRaises(AccessError):
rendered = composer._render_lang(source.ids)
# _render_lang should not crash when content is not coming from template
# but not dynamic and/or is actually the default computed based on partner
for lang_value, expected in [
(False, self.partner_1.lang), ("", self.partner_1.lang), ("fr_FR", "fr_FR")
]:
with self.subTest(lang_value=lang_value):
composer.lang = lang_value
rendered = composer._render_lang(source.ids)
self.assertEqual(rendered, {source.id: expected})
@users("employee")
def test_rendering_custom(self):
@ -84,7 +164,6 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
source = self.test_record.with_env(self.env)
composer = self.env['mail.test.composer.mixin'].create({
'description': '<p>Description for <t t-esc="object.name"/></p>',
'lang': '{{ object.customer_id.lang }}',
'name': 'Invite',
'template_id': self.mail_template.id,
'source_ids': [(4, source.id)],
@ -103,11 +182,22 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
# ask for dynamic language computation
subject = composer._render_field('subject', source.ids, compute_lang=True)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}',
'Fixme: translations are not done, as taking composer translations and not template one')
self.assertEqual(subject, f'SpanishSubject for {source.name}',
'Translation comes from the template, as both values equal')
body = composer._render_field('body', source.ids, compute_lang=True)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>',
'Fixme: translations are not done, as taking composer translations and not template one'
)
self.assertEqual(body, f'<p>SpanishBody for {source.name}</p>',
'Translation comes from the template, as both values equal')
description = composer._render_field('description', source.ids)[source.id]
self.assertEqual(description, f'<p>Description for {source.name}</p>')
# check default computation when 'lang' is void -> actually rerouted to template lang
composer.lang = False
subject = composer._render_field('subject', source.ids, compute_lang=True)[source.id]
self.assertEqual(subject, f'SpanishSubject for {source.name}',
'Translation comes from the template, as both values equal')
# check default computation when 'lang' is void in both -> main customer lang
self.mail_template.lang = False
subject = composer._render_field('subject', source.ids, compute_lang=True)[source.id]
self.assertEqual(subject, f'SpanishSubject for {source.name}',
'Translation comes from customer lang, being default when no value is rendered')

Some files were not shown because too many files have changed in this diff Show more