summaryrefslogtreecommitdiff
path: root/addons/test_mail/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/test_mail/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/test_mail/tests')
-rw-r--r--addons/test_mail/tests/__init__.py20
-rw-r--r--addons/test_mail/tests/common.py71
-rw-r--r--addons/test_mail/tests/test_invite.py33
-rw-r--r--addons/test_mail/tests/test_ir_actions.py43
-rw-r--r--addons/test_mail/tests/test_mail_activity.py434
-rw-r--r--addons/test_mail/tests/test_mail_channel.py586
-rw-r--r--addons/test_mail/tests/test_mail_channel_partner.py214
-rw-r--r--addons/test_mail/tests/test_mail_composer.py842
-rw-r--r--addons/test_mail/tests/test_mail_followers.py498
-rw-r--r--addons/test_mail/tests/test_mail_gateway.py1273
-rw-r--r--addons/test_mail/tests/test_mail_mail.py141
-rw-r--r--addons/test_mail/tests/test_mail_message.py591
-rw-r--r--addons/test_mail/tests/test_mail_template.py169
-rw-r--r--addons/test_mail/tests/test_mail_thread_internals.py253
-rw-r--r--addons/test_mail/tests/test_mail_tools.py83
-rw-r--r--addons/test_mail/tests/test_message_management.py120
-rw-r--r--addons/test_mail/tests/test_message_post.py397
-rw-r--r--addons/test_mail/tests/test_message_track.py353
-rw-r--r--addons/test_mail/tests/test_performance.py1021
-rw-r--r--addons/test_mail/tests/test_ui.py10
20 files changed, 7152 insertions, 0 deletions
diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py
new file mode 100644
index 00000000..c2bbe943
--- /dev/null
+++ b/addons/test_mail/tests/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+
+from . import test_invite
+from . import test_ir_actions
+from . import test_mail_activity
+from . import test_mail_composer
+from . import test_mail_followers
+from . import test_mail_message
+from . import test_mail_mail
+from . import test_mail_channel
+from . import test_mail_channel_partner
+from . import test_mail_gateway
+from . import test_mail_thread_internals
+from . import test_mail_template
+from . import test_mail_tools
+from . import test_message_management
+from . import test_message_post
+from . import test_message_track
+from . import test_performance
+from . import test_ui
diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py
new file mode 100644
index 00000000..c556a988
--- /dev/null
+++ b/addons/test_mail/tests/common.py
@@ -0,0 +1,71 @@
+# -*- 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 SavepointCase
+
+
+class TestMailCommon(MailCommon):
+ """ Main entry point for functional tests. """
+
+ @classmethod
+ def _create_channel_listener(cls):
+ cls.channel_listen = cls.env['mail.channel'].with_context(cls._test_context).create({'name': 'Listener'})
+
+ @classmethod
+ def _create_records_for_batch(cls, model, count):
+ # TDE note: to be cleaned in master
+ records = cls.env[model]
+ partners = cls.env['res.partner']
+ country_id = cls.env.ref('base.be').id
+
+ partners = cls.env['res.partner'].with_context(**cls._test_context).create([{
+ 'name': 'Partner_%s' % (x),
+ 'email': '_test_partner_%s@example.com' % (x),
+ 'country_id': country_id,
+ 'mobile': '047500%02d%02d' % (x, x)
+ } for x in range(count)])
+
+ records = cls.env[model].with_context(**cls._test_context).create([{
+ 'name': 'Test_%s' % (x),
+ 'customer_id': partners[x].id,
+ } for x in range(count)])
+
+ cls.records = cls._reset_mail_context(records)
+ cls.partners = partners
+ return cls.records, cls.partners
+
+
+class TestMailMultiCompanyCommon(MailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailMultiCompanyCommon, cls).setUpClass()
+ cls.company_2 = cls.env['res.company'].create({
+ 'name': 'Second Test Company',
+ })
+
+
+class TestRecipients(SavepointCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestRecipients, cls).setUpClass()
+ Partner = cls.env['res.partner'].with_context({
+ 'mail_create_nolog': True,
+ 'mail_create_nosubscribe': True,
+ 'mail_notrack': True,
+ 'no_reset_password': True,
+ })
+ cls.partner_1 = Partner.create({
+ 'name': 'Valid Lelitre',
+ 'email': 'valid.lelitre@agrolait.com',
+ 'country_id': cls.env.ref('base.be').id,
+ 'mobile': '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',
+ })
diff --git a/addons/test_mail/tests/test_invite.py b/addons/test_mail/tests/test_invite.py
new file mode 100644
index 00000000..c4e91a86
--- /dev/null
+++ b/addons/test_mail/tests/test_invite.py
@@ -0,0 +1,33 @@
+# -*- 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.tools import mute_logger
+
+
+class TestInvite(TestMailCommon):
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_invite_email(self):
+ test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+ test_partner = self.env['res.partner'].with_context(self._test_context).create({
+ 'name': 'Valid Lelitre',
+ 'email': 'valid.lelitre@agrolait.com'})
+
+ mail_invite = self.env['mail.wizard.invite'].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()
+
+ # check added followers and that emails were sent
+ self.assertEqual(test_record.message_partner_ids,
+ test_partner | self.user_admin.partner_id)
+ self.assertEqual(test_record.message_follower_ids.mapped('channel_id'),
+ self.env['mail.channel'])
+ self.assertSentEmail(self.partner_employee, [test_partner])
+ self.assertSentEmail(self.partner_employee, [self.partner_admin])
+ self.assertEqual(len(self._mails), 2)
diff --git a/addons/test_mail/tests/test_ir_actions.py b/addons/test_mail/tests/test_ir_actions.py
new file mode 100644
index 00000000..d67124b2
--- /dev/null
+++ b/addons/test_mail/tests/test_ir_actions.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# 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
+
+
+class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
+
+ def test_action_email(self):
+ email_template = self._create_template('res.partner', {'partner_to': '%s' % self.test_partner.id})
+ self.action.write({'state': 'email', 'template_id': email_template.id})
+ self.action.with_context(self.context).run()
+ # check an email is waiting for sending
+ mail = self.env['mail.mail'].sudo().search([('subject', '=', 'About TestingPartner')])
+ self.assertEqual(len(mail), 1)
+ # check email content
+ self.assertEqual(mail.body, '<p>Hello TestingPartner</p>')
+
+ def test_action_followers(self):
+ self.test_partner.message_unsubscribe(self.test_partner.message_partner_ids.ids)
+ random_partner = self.env['res.partner'].create({'name': 'Thierry Wololo'})
+ self.action.write({
+ 'state': 'followers',
+ 'partner_ids': [(4, self.env.ref('base.partner_admin').id), (4, random_partner.id)],
+ 'channel_ids': [(4, self.env.ref('mail.channel_all_employees').id)]
+ })
+ self.action.with_context(self.context).run()
+ self.assertEqual(self.test_partner.message_partner_ids, self.env.ref('base.partner_admin') | random_partner)
+ self.assertEqual(self.test_partner.message_channel_ids, self.env.ref('mail.channel_all_employees'))
+
+ def test_action_next_activity(self):
+ self.action.write({
+ 'state': 'next_activity',
+ 'activity_user_type': 'specific',
+ '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.assertEqual(self.env['mail.activity'].search_count([('summary', '=', 'TestNew')]), 1)
diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py
new file mode 100644
index 00000000..e6d4b003
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_activity.py
@@ -0,0 +1,434 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import date, datetime, timedelta
+from dateutil.relativedelta import relativedelta
+from unittest.mock import patch
+from unittest.mock import DEFAULT
+
+import pytz
+
+from odoo import fields, exceptions, tests
+from odoo.addons.test_mail.tests.common import TestMailCommon
+from odoo.addons.test_mail.models.test_mail_models import MailTestActivity
+from odoo.tools import mute_logger
+from odoo.tests.common import Form
+
+
+class TestActivityCommon(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestActivityCommon, cls).setUpClass()
+ cls.test_record = cls.env['mail.test.activity'].with_context(cls._test_context).create({'name': 'Test'})
+ # reset ctx
+ cls._reset_mail_context(cls.test_record)
+
+
+@tests.tagged('mail_activity')
+class TestActivityRights(TestActivityCommon):
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_activity_security_user_access_other(self):
+ activity = self.test_record.with_user(self.user_employee).activity_schedule(
+ 'test_mail.mail_act_test_todo',
+ user_id=self.user_admin.id)
+ self.assertTrue(activity.can_write)
+ activity.write({'user_id': self.user_employee.id})
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_activity_security_user_access_own(self):
+ activity = self.test_record.with_user(self.user_employee).activity_schedule(
+ 'test_mail.mail_act_test_todo')
+ self.assertTrue(activity.can_write)
+ activity.write({'user_id': self.user_admin.id})
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_activity_security_user_noaccess_automated(self):
+ def _employee_crash(*args, **kwargs):
+ """ If employee is test employee, consider he has no access on document """
+ recordset = args[0]
+ if recordset.env.uid == self.user_employee.id:
+ raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
+ return DEFAULT
+
+ with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
+ activity = self.test_record.activity_schedule(
+ 'test_mail.mail_act_test_todo',
+ user_id=self.user_employee.id)
+
+ activity2 = self.test_record.activity_schedule('test_mail.mail_act_test_todo')
+ activity2.write({'user_id': self.user_employee.id})
+
+ def test_activity_security_user_noaccess_manual(self):
+ def _employee_crash(*args, **kwargs):
+ """ If employee is test employee, consider he has no access on document """
+ recordset = args[0]
+ if recordset.env.uid == self.user_employee.id:
+ raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
+ return DEFAULT
+
+ # cannot create activities for people that cannot access record
+ with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
+ with self.assertRaises(exceptions.UserError):
+ activity = self.env['mail.activity'].create({
+ '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,
+ 'user_id': self.user_employee.id,
+ })
+
+ # cannot create activities if no access to the document
+ with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
+ with self.assertRaises(exceptions.AccessError):
+ activity = self.test_record.with_user(self.user_employee).activity_schedule(
+ 'test_mail.mail_act_test_todo',
+ user_id=self.user_admin.id)
+
+
+@tests.tagged('mail_activity')
+class TestActivityFlow(TestActivityCommon):
+
+ def test_activity_flow_employee(self):
+ with self.with_user('employee'):
+ test_record = self.env['mail.test.activity'].browse(self.test_record.id)
+ self.assertEqual(test_record.activity_ids, self.env['mail.activity'])
+
+ # employee record an activity and check the deadline
+ self.env['mail.activity'].create({
+ 'summary': 'Test Activity',
+ 'date_deadline': date.today() + relativedelta(days=1),
+ 'activity_type_id': self.env.ref('mail.mail_activity_data_email').id,
+ 'res_model_id': self.env['ir.model']._get(test_record._name).id,
+ 'res_id': test_record.id,
+ })
+ self.assertEqual(test_record.activity_summary, 'Test Activity')
+ self.assertEqual(test_record.activity_state, 'planned')
+
+ test_record.activity_ids.write({'date_deadline': date.today() - relativedelta(days=1)})
+ test_record.invalidate_cache() # TDE note: should not have to do it I think
+ self.assertEqual(test_record.activity_state, 'overdue')
+
+ test_record.activity_ids.write({'date_deadline': date.today()})
+ test_record.invalidate_cache() # TDE note: should not have to do it I think
+ self.assertEqual(test_record.activity_state, 'today')
+
+ # activity is done
+ test_record.activity_ids.action_feedback(feedback='So much feedback')
+ self.assertEqual(test_record.activity_ids, self.env['mail.activity'])
+ self.assertEqual(test_record.message_ids[0].subtype_id, self.env.ref('mail.mt_activities'))
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_activity_notify_other_user(self):
+ self.user_admin.notification_type = 'email'
+ rec = self.test_record.with_user(self.user_employee)
+ with self.assertSinglePostNotifications(
+ [{'partner': self.partner_admin, 'type': 'email'}],
+ message_info={'content': 'assigned you an activity', 'subtype': 'mail.mt_note', 'message_type': 'user_notification'}):
+ activity = rec.activity_schedule(
+ 'test_mail.mail_act_test_todo',
+ user_id=self.user_admin.id)
+ self.assertEqual(activity.create_uid, self.user_employee)
+ self.assertEqual(activity.user_id, self.user_admin)
+
+ def test_activity_notify_same_user(self):
+ self.user_employee.notification_type = 'email'
+ rec = self.test_record.with_user(self.user_employee)
+ with self.assertNoNotifications():
+ activity = rec.activity_schedule(
+ 'test_mail.mail_act_test_todo',
+ user_id=self.user_employee.id)
+ self.assertEqual(activity.create_uid, self.user_employee)
+ self.assertEqual(activity.user_id, self.user_employee)
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_activity_dont_notify_no_user_change(self):
+ self.user_employee.notification_type = 'email'
+ activity = self.test_record.activity_schedule('test_mail.mail_act_test_todo', user_id=self.user_employee.id)
+ with self.assertNoNotifications():
+ activity.with_user(self.user_admin).write({'user_id': self.user_employee.id})
+ self.assertEqual(activity.user_id, self.user_employee)
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_activity_summary_sync(self):
+ """ Test summary from type is copied on activities if set (currently only in form-based onchange) """
+ ActivityType = self.env['mail.activity.type']
+ email_activity_type = ActivityType.create({
+ 'name': 'email',
+ 'summary': 'Email Summary',
+ })
+ call_activity_type = ActivityType.create({'name': 'call'})
+ with Form(self.env['mail.activity'].with_context(default_res_model_id=self.env.ref('base.model_res_partner'))) as ActivityForm:
+ ActivityForm.res_model_id = self.env.ref('base.model_res_partner')
+
+ ActivityForm.activity_type_id = call_activity_type
+ # activity summary should be empty
+ self.assertEqual(ActivityForm.summary, False)
+
+ ActivityForm.activity_type_id = email_activity_type
+ # activity summary should be replaced with email's default summary
+ self.assertEqual(ActivityForm.summary, email_activity_type.summary)
+
+ ActivityForm.activity_type_id = call_activity_type
+ # activity summary remains unchanged from change of activity type as call activity doesn't have default summary
+ self.assertEqual(ActivityForm.summary, email_activity_type.summary)
+
+ def test_action_feedback_attachment(self):
+ Partner = self.env['res.partner']
+ Activity = self.env['mail.activity']
+ Attachment = self.env['ir.attachment']
+ Message = self.env['mail.message']
+
+ partner = self.env['res.partner'].create({
+ 'name': 'Tester',
+ })
+
+ activity = Activity.create({
+ 'summary': 'Test',
+ 'activity_type_id': 1,
+ 'res_model_id': self.env.ref('base.model_res_partner').id,
+ 'res_id': partner.id,
+ })
+
+ attachments = Attachment
+ attachments += Attachment.create({
+ 'name': 'test',
+ 'res_name': 'test',
+ 'res_model': 'mail.activity',
+ 'res_id': activity.id,
+ 'datas': 'test',
+ })
+ attachments += Attachment.create({
+ '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 = Message.search([], order='id desc', limit=1)
+ 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)
+
+
+@tests.tagged('mail_activity')
+class TestActivityMixin(TestActivityCommon):
+
+ @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(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))
+ 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_cache(['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_cache(['activity_ids'])
+ self.assertEqual(self.test_record.activity_user_id, self.user_employee)
+
+ self.test_record.invalidate_cache(ids=self.test_record.ids)
+ 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',)
+ self.assertEqual(self.test_record.activity_ids, act2 | act3)
+
+ # 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')
+
+ # 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), 2)
+ self.assertEqual(self.test_record.message_ids.mapped('subtype_id'), self.env.ref('mail.mt_activities'))
+
+ # Perform 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), 2)
+
+ @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.toggle_active()
+ self.assertEqual(rec.active, False)
+ self.assertEqual(rec.activity_ids, self.env['mail.activity'])
+ rec.toggle_active()
+ self.assertEqual(rec.active, True)
+ self.assertEqual(rec.activity_ids, self.env['mail.activity'])
+
+ @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)
+
+ @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_1 = 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_2 = 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'})
+ activity_3 = 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)
+
+class TestReadProgressBar(tests.TransactionCase):
+ """Test for read_progress_bar"""
+
+ 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
+ """
+ model = 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
+ model.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(model) - timedelta(days=7),
+ )
+ model.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(model),
+ )
+ model.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(model) + timedelta(days=7),
+ )
+
+ 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 = model.read_group(domain, fields=['date'], groupby=[groupby])
+ progressbars = model.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], pg_groups["overdue"])
+ self.assertEqual(groups[1][groupby], pg_groups["today"])
+ self.assertEqual(groups[2][groupby], pg_groups["planned"])
+
diff --git a/addons/test_mail/tests/test_mail_channel.py b/addons/test_mail/tests/test_mail_channel.py
new file mode 100644
index 00000000..e3357027
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_channel.py
@@ -0,0 +1,586 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import tagged, Form
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.addons.test_mail.tests.common import TestMailCommon
+from odoo.exceptions import AccessError, ValidationError, UserError
+from odoo.tools import mute_logger, formataddr
+
+
+@tagged('mail_channel')
+class TestChannelAccessRights(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestChannelAccessRights, cls).setUpClass()
+ Channel = cls.env['mail.channel'].with_context(cls._test_context)
+
+ cls.user_public = mail_new_test_user(cls.env, login='bert', groups='base.group_public', name='Bert Tartignole')
+ cls.user_portal = mail_new_test_user(cls.env, login='chell', groups='base.group_portal', name='Chell Gladys')
+
+ # Pigs: base group for tests
+ cls.group_pigs = Channel.create({
+ 'name': 'Pigs',
+ 'public': 'groups',
+ 'group_public_id': cls.env.ref('base.group_user').id})
+ # Jobs: public group
+ cls.group_public = Channel.create({
+ 'name': 'Jobs',
+ 'description': 'NotFalse',
+ 'public': 'public'})
+ # Private: private group
+ cls.group_private = Channel.create({
+ 'name': 'Private',
+ 'public': 'private'})
+
+ @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
+ def test_access_rights_public(self):
+ # Read public group -> ok
+ self.group_public.with_user(self.user_public).read()
+
+ # Read Pigs -> ko, restricted to employees
+ with self.assertRaises(AccessError):
+ self.group_pigs.with_user(self.user_public).read()
+
+ # Read a private group when being a member: ok
+ self.group_private.write({'channel_partner_ids': [(4, self.user_public.partner_id.id)]})
+ self.group_private.with_user(self.user_public).read()
+
+ # Create group: ko, no access rights
+ with self.assertRaises(AccessError):
+ self.env['mail.channel'].with_user(self.user_public).create({'name': 'Test'})
+
+ # Update group: ko, no access rights
+ with self.assertRaises(AccessError):
+ self.group_public.with_user(self.user_public).write({'name': 'Broutouschnouk'})
+
+ # Unlink group: ko, no access rights
+ with self.assertRaises(AccessError):
+ self.group_public.with_user(self.user_public).unlink()
+
+ @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models', 'odoo.models.unlink')
+ def test_access_rights_groups(self):
+ # Employee read employee-based group: ok
+ self.group_pigs.with_user(self.user_employee).read()
+
+ # Employee can create a group
+ self.env['mail.channel'].with_user(self.user_employee).create({'name': 'Test'})
+
+ # Employee update employee-based group: ok
+ self.group_pigs.with_user(self.user_employee).write({'name': 'modified'})
+
+ # Employee unlink employee-based group: ok
+ self.group_pigs.with_user(self.user_employee).unlink()
+
+ # Employee cannot read a private group
+ with self.assertRaises(AccessError):
+ self.group_private.with_user(self.user_employee).read()
+
+ # Employee cannot write on private
+ with self.assertRaises(AccessError):
+ self.group_private.with_user(self.user_employee).write({'name': 're-modified'})
+
+ @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
+ def test_access_rights_followers_ko(self):
+ # self.group_private.name has been put in the cache during the setup as sudo
+ # It must therefore be removed from the cache in other to validate the fact user_portal can't read it.
+ self.group_private.invalidate_cache(['name'])
+ with self.assertRaises(AccessError):
+ self.group_private.with_user(self.user_portal).name
+
+ def test_access_rights_followers_portal(self):
+ # Do: Chell is added into Pigs members and browse it -> ok for messages, ko for partners (no read permission)
+ self.group_private.write({'channel_partner_ids': [(4, self.user_portal.partner_id.id)]})
+ chell_pigs = self.group_private.with_user(self.user_portal)
+ trigger_read = chell_pigs.name
+ for message in chell_pigs.message_ids:
+ trigger_read = message.subject
+
+ with self.assertRaises(AccessError):
+ chell_pigs.message_partner_ids
+
+ for partner in self.group_private.message_partner_ids:
+ if partner.id == self.user_portal.partner_id.id:
+ # Chell can read her own partner record
+ continue
+ with self.assertRaises(AccessError):
+ trigger_read = partner.with_user(self.user_portal).name
+
+
+@tagged('mail_channel')
+class TestChannelFeatures(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestChannelFeatures, cls).setUpClass()
+ cls.test_channel = cls.env['mail.channel'].with_context(cls._test_context).create({
+ 'name': 'Test',
+ 'description': 'Description',
+ 'alias_name': 'test',
+ 'public': 'public',
+ })
+ cls.test_partner = cls.env['res.partner'].with_context(cls._test_context).create({
+ 'name': 'Test Partner',
+ 'email': 'test@example.com',
+ })
+
+ def _join_channel(self, channel, partners):
+ for partner in partners:
+ channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner.id})]})
+ channel.invalidate_cache()
+
+ def _leave_channel(self, channel, partners):
+ for partner in partners:
+ channel._action_unfollow(partner)
+
+ def test_channel_listeners(self):
+ self.assertEqual(self.test_channel.message_channel_ids, self.test_channel)
+ self.assertEqual(self.test_channel.message_partner_ids, self.env['res.partner'])
+ self.assertEqual(self.test_channel.channel_partner_ids, self.env['res.partner'])
+
+ self._join_channel(self.test_channel, self.test_partner)
+ self.assertEqual(self.test_channel.message_channel_ids, self.test_channel)
+ self.assertEqual(self.test_channel.message_partner_ids, self.env['res.partner'])
+ self.assertEqual(self.test_channel.channel_partner_ids, self.test_partner)
+
+ self._leave_channel(self.test_channel, self.test_partner)
+ self.assertEqual(self.test_channel.message_channel_ids, self.test_channel)
+ self.assertEqual(self.test_channel.message_partner_ids, self.env['res.partner'])
+ self.assertEqual(self.test_channel.channel_partner_ids, self.env['res.partner'])
+
+ def test_channel_post_nofollow(self):
+ self.test_channel.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
+ self.assertEqual(self.test_channel.message_channel_ids, self.test_channel)
+ self.assertEqual(self.test_channel.message_partner_ids, self.env['res.partner'])
+
+ @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ def test_channel_mailing_list_recipients(self):
+ """ Posting a message on a mailing list should send one email to all recipients """
+ self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'schlouby.fr')
+ self.test_channel.write({'email_send': True})
+ self.user_employee.write({'notification_type': 'email'})
+
+ # Subscribe an user without email. We shouldn't try to send email to them.
+ nomail = self.env['res.users'].create({
+ "login": "nomail",
+ "name": "No Mail",
+ "email": False,
+ "notification_type": "email",
+ })
+ self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner | nomail.partner_id)
+ with self.mock_mail_gateway():
+ self.test_channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment')
+ self.assertSentEmail(self.test_channel.env.user.partner_id, [self.partner_employee, self.test_partner])
+
+ @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ def test_channel_chat_recipients(self):
+ """ Posting a message on a chat should not send emails """
+ self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'schlouby.fr')
+ self.test_channel.write({'email_send': False})
+ self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner)
+ with self.mock_mail_gateway():
+ self.test_channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment')
+ self.assertNotSentEmail()
+ self.assertEqual(len(self._mails), 0)
+
+ @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ def test_channel_classic_recipients(self):
+ """ Posting a message on a classic channel should work like classic post """
+ self.test_channel.write({'alias_name': False})
+ self.test_channel.message_subscribe([self.user_employee.partner_id.id, self.test_partner.id])
+ with self.mock_mail_gateway():
+ self.test_channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment')
+ self.assertSentEmail(self.test_channel.env.user.partner_id, [self.test_partner])
+
+ def test_channel_creation(self):
+ """A user that create a private channel should be able to read it."""
+ channel_form = Form(self.env['mail.channel'].with_user(self.user_employee))
+ channel_form.name = 'Test private channel'
+ channel_form.public = 'private'
+ channel = channel_form.save()
+ self.assertEqual(channel.name, 'Test private channel', 'Must be able to read the created channel')
+
+ def test_channel_get(self):
+ current_user = self.env['res.users'].create({
+ "login": "adam",
+ "name": "Jonas",
+ })
+ current_user = current_user.with_user(current_user)
+ current_partner = current_user.partner_id
+ other_partner = self.test_partner
+
+ # `channel_get` should return a new channel the first time a partner is given
+ initial_channel_info = current_user.env['mail.channel'].channel_get(partners_to=other_partner.ids)
+ self.assertEqual(set(p['id'] for p in initial_channel_info['members']), {current_partner.id, other_partner.id})
+
+ # `channel_get` should return the existing channel every time the same partner is given
+ same_channel_info = current_user.env['mail.channel'].channel_get(partners_to=other_partner.ids)
+ self.assertEqual(same_channel_info['id'], initial_channel_info['id'])
+
+ # `channel_get` should return the existing channel when the current partner is given together with the other partner
+ together_channel_info = current_user.env['mail.channel'].channel_get(partners_to=(current_partner + other_partner).ids)
+ self.assertEqual(together_channel_info['id'], initial_channel_info['id'])
+
+ # `channel_get` should return a new channel the first time just the current partner is given,
+ # even if a channel containing the current partner together with other partners already exists
+ solo_channel_info = current_user.env['mail.channel'].channel_get(partners_to=current_partner.ids)
+ self.assertNotEqual(solo_channel_info['id'], initial_channel_info['id'])
+ self.assertEqual(set(p['id'] for p in solo_channel_info['members']), {current_partner.id})
+
+ # `channel_get` should return the existing channel every time the current partner is given
+ same_solo_channel_info = current_user.env['mail.channel'].channel_get(partners_to=current_partner.ids)
+ self.assertEqual(same_solo_channel_info['id'], solo_channel_info['id'])
+
+ def test_channel_seen(self):
+ """
+ In case of concurrent channel_seen RPC, ensure the oldest call has no effect.
+ """
+ self.test_channel.write({'channel_type': 'chat'})
+ self.test_channel.action_follow()
+ msg_1 = self._add_messages(self.test_channel, 'Body1', author=self.user_employee.partner_id,
+ channel_ids=[self.test_channel.id])
+ msg_2 = self._add_messages(self.test_channel, 'Body2', author=self.user_employee.partner_id,
+ channel_ids=[self.test_channel.id])
+ ChannelAsUser = self.test_channel.with_user(self.user_employee).browse(self.test_channel.id)
+
+ self.test_channel.channel_seen(msg_2.id)
+ self.assertEqual(
+ ChannelAsUser.channel_info()[0]['seen_partners_info'][0]['seen_message_id'],
+ msg_2.id,
+ "Last message id should have been updated"
+ )
+
+ self.test_channel.channel_seen(msg_1.id)
+ self.assertEqual(
+ ChannelAsUser.channel_info()[0]['seen_partners_info'][0]['seen_message_id'],
+ msg_2.id,
+ "Last message id should stay the same after mark channel as seen with an older message"
+ )
+
+ @mute_logger('odoo.models.unlink')
+ def test_channel_auto_unsubscribe_archived_or_deleted_users(self):
+ """Archiving / deleting a user should automatically unsubscribe related partner from private channels"""
+ test_channel_private = self.env['mail.channel'].with_context(self._test_context).create({
+ 'name': 'Winden caves',
+ 'description': 'Channel to travel through time',
+ 'public': 'private',
+ })
+ test_channel_group = self.env['mail.channel'].with_context(self._test_context).create({
+ 'name': 'Sic Mundus',
+ 'public': 'groups',
+ 'group_public_id': self.env.ref('base.group_user').id})
+
+ test_user = self.env['res.users'].create({
+ "login": "adam",
+ "name": "Jonas",
+ })
+ test_partner = test_user.partner_id
+ test_chat = self.env['mail.channel'].with_context(self._test_context).create({
+ 'name': 'test',
+ 'channel_type': 'chat',
+ 'public': 'private',
+ 'channel_partner_ids': [(4, self.user_employee.partner_id.id), (4, test_partner.id)],
+ })
+
+ self._join_channel(self.test_channel, self.user_employee.partner_id | test_partner)
+ self._join_channel(test_channel_private, self.user_employee.partner_id | test_partner)
+ self._join_channel(test_channel_group, self.user_employee.partner_id | test_partner)
+
+ # Unsubscribe archived user from the private channels, but not from public channels and not from chat
+ self.user_employee.active = False
+ self.assertEqual(test_channel_private.channel_partner_ids, test_partner)
+ self.assertEqual(test_channel_group.channel_partner_ids, test_partner)
+ self.assertEqual(self.test_channel.channel_partner_ids, self.user_employee.partner_id | test_partner)
+ self.assertEqual(test_chat.channel_partner_ids, self.user_employee.partner_id | test_partner)
+
+ # Unsubscribe deleted user from the private channels, but not from public channels and not from chat
+ test_user.unlink()
+ self.assertEqual(test_channel_private.channel_partner_ids, self.env['res.partner'])
+ self.assertEqual(test_channel_group.channel_partner_ids, self.env['res.partner'])
+ self.assertEqual(self.test_channel.channel_partner_ids, self.user_employee.partner_id | test_partner)
+ self.assertEqual(test_chat.channel_partner_ids, self.user_employee.partner_id | test_partner)
+
+ def test_channel_unfollow_should_also_unsubscribe_the_partner(self):
+ self.test_channel.message_subscribe(self.test_partner.ids)
+ self.test_channel._action_unfollow(self.test_partner)
+
+ self.assertFalse(self.test_channel.message_partner_ids)
+
+ def test_channel_unfollow_should_not_post_message_if_the_partner_has_been_removed(self):
+ '''
+ When a partner leaves a channel, the system will help post a message under
+ that partner's name in the channel to notify others if `email_sent` is set `False`.
+ The message should only be posted when the partner is still a member of the channel
+ before method `_action_unfollow()` is called.
+ If the partner has been removed earlier, no more messages will be posted
+ even if `_action_unfollow()` is called again.
+ '''
+ self.test_channel.write({'email_send': False})
+ self._join_channel(self.test_channel, self.test_partner)
+ self.test_channel.message_subscribe(self.partner_employee.ids)
+
+ # a message should be posted to notify others when a partner is about to leave
+ with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], {
+ 'message_type': 'notification',
+ 'subtype': 'mail.mt_comment',
+ }):
+ self.test_channel._action_unfollow(self.test_partner)
+
+ # no more messages should be posted if the partner has been removed before.
+ with self.assertNoNotifications():
+ self.test_channel._action_unfollow(self.test_partner)
+
+ def test_multi_company_chat(self):
+ company_A = self.env['res.company'].create({'name': 'Company A'})
+ company_B = self.env['res.company'].create({'name': 'Company B'})
+ test_user_1 = self.env['res.users'].create({
+ 'login': 'user1',
+ 'name': 'My First New User',
+ 'company_ids': [(6, 0, company_A.ids)],
+ 'company_id': company_A.id
+ })
+ test_user_2 = self.env['res.users'].create({
+ 'login': 'user2',
+ 'name': 'My Second New User',
+ 'company_ids': [(6, 0, company_B.ids)],
+ 'company_id': company_B.id
+ })
+ initial_channel_info = self.env['mail.channel'].with_user(test_user_1).with_context(allowed_company_ids=company_A.ids).channel_get(test_user_2.partner_id.ids)
+ self.assertTrue(initial_channel_info, 'should be able to chat with multi company user')
+
+ def test_multi_company_message_post_notifications(self):
+ company_1 = self.company_admin
+ company_2 = self.env['res.company'].create({'name': 'Company 2'})
+
+ # Company 1 and notification_type == "inbox"
+ user_1 = self.user_employee
+
+ # Company 1 and notification_type == "email"
+ user_2 = self.user_admin
+ user_2.notification_type = 'email'
+
+ user_3 = mail_new_test_user(
+ self.env, login='user3', email='user3@example.com', groups='base.group_user',
+ company_id=company_2.id, company_ids=[(6, 0, company_2.ids)],
+ name='user3', notification_type='inbox')
+
+ user_4 = mail_new_test_user(
+ self.env, login='user4', email='user4@example.com', groups='base.group_user',
+ company_id=company_2.id, company_ids=[(6, 0, company_2.ids)],
+ name='user4', notification_type='email')
+
+ partner_without_user = self.env['res.partner'].create({
+ 'name': 'Partner',
+ 'email': 'partner_test_123@example.com',
+ })
+ mail_channel = self.env['mail.channel'].with_user(user_1).create({
+ 'name': 'Channel',
+ 'channel_partner_ids': [
+ (4, user_1.partner_id.id),
+ (4, user_2.partner_id.id),
+ (4, user_3.partner_id.id),
+ (4, user_4.partner_id.id),
+ (4, partner_without_user.id),
+ ],
+ 'email_send': True,
+ })
+
+ mail_channel.invalidate_cache()
+ (user_1 | user_2 | user_3 | user_4).invalidate_cache()
+
+ with self.mock_mail_gateway():
+ mail_channel.with_user(user_1).with_company(company_1).message_post(
+ body='Test body message 1337',
+ channel_ids=mail_channel.ids,
+ )
+
+ self.assertSentEmail(user_1.partner_id, [user_2.partner_id])
+ self.assertSentEmail(user_1.partner_id, [user_4.partner_id])
+ self.assertEqual(len(self._mails), 3, 'Should have send only 3 emails to user 2, user 4 and the partner')
+
+ self.assertBusNotifications([(self.cr.dbname, 'mail.channel', mail_channel.id)])
+
+ # Should not create mail notifications for user 1 & 3
+ self.assertFalse(self.env['mail.notification'].search([('res_partner_id', '=', user_1.partner_id.id)]))
+ self.assertFalse(self.env['mail.notification'].search([('res_partner_id', '=', user_3.partner_id.id)]))
+
+ # Should create mail notifications for user 2 & 4
+ self.assertTrue(self.env['mail.notification'].search([('res_partner_id', '=', user_2.partner_id.id)]))
+ self.assertTrue(self.env['mail.notification'].search([('res_partner_id', '=', user_4.partner_id.id)]))
+
+ # Check that we did not send a "channel_seen" notifications
+ # for the users which receive the notifications by email
+ notification_seen_user_2 = self.env['bus.bus'].search([('create_uid', '=', user_2.id)])
+ self.assertFalse(notification_seen_user_2, 'Should not have sent a notification as user 2')
+ notification_seen_user_4 = self.env['bus.bus'].search([('create_uid', '=', user_4.id)])
+ self.assertFalse(notification_seen_user_4, 'Should not have sent a notification as user 4')
+
+
+@tagged('moderation', 'mail_channel')
+class TestChannelModeration(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestChannelModeration, cls).setUpClass()
+
+ cls.channel_1 = cls.env['mail.channel'].create({
+ 'name': 'Moderation_1',
+ 'email_send': True,
+ 'moderation': True,
+ 'channel_partner_ids': [(4, cls.partner_employee.id)],
+ 'moderator_ids': [(4, cls.user_employee.id)],
+ })
+
+ # ensure initial data
+ cls.user_employee_2 = mail_new_test_user(
+ cls.env, login='employee2', groups='base.group_user', company_id=cls.company_admin.id,
+ name='Enguerrand Employee2', notification_type='inbox', signature='--\nEnguerrand'
+ )
+ cls.partner_employee_2 = cls.user_employee_2.partner_id
+
+ cls.user_portal = cls._create_portal_user()
+
+ def test_moderator_consistency(self):
+ # moderators should be channel members
+ with self.assertRaises(ValidationError):
+ self.channel_1.write({'moderator_ids': [(4, self.user_admin.id)]})
+
+ # member -> moderator or
+ self.channel_1.write({'channel_partner_ids': [(4, self.partner_admin.id)]})
+ self.channel_1.write({'moderator_ids': [(4, self.user_admin.id)]})
+
+ # member -> moderator ko if no email
+ self.channel_1.write({'moderator_ids': [(3, self.partner_admin.id)]})
+ self.user_admin.write({'email': False})
+ with self.assertRaises(ValidationError):
+ self.channel_1.write({'moderator_ids': [(4, self.user_admin.id)]})
+
+ def test_moderation_consistency(self):
+ # moderation enabled channels are restricted to mailing lists
+ with self.assertRaises(ValidationError):
+ self.channel_1.write({'email_send': False})
+
+ # moderation enabled channels should always have moderators
+ with self.assertRaises(ValidationError):
+ self.channel_1.write({'moderator_ids': [(5, 0)]})
+
+ def test_moderation_count(self):
+ self.assertEqual(self.channel_1.moderation_count, 0)
+ self.channel_1.write({'moderation_ids': [
+ (0, 0, {'email': 'test0@example.com', 'status': 'allow'}),
+ (0, 0, {'email': 'test1@example.com', 'status': 'ban'})
+ ]})
+ self.assertEqual(self.channel_1.moderation_count, 2)
+
+ @mute_logger('odoo.addons.mail.models.mail_channel', 'odoo.models.unlink')
+ def test_send_guidelines(self):
+ self.channel_1.write({'channel_partner_ids': [(4, self.partner_portal.id), (4, self.partner_admin.id)]})
+ self.channel_1._update_moderation_email([self.partner_admin.email], 'ban')
+ with self.mock_mail_gateway():
+ self.channel_1.with_user(self.user_employee).send_guidelines()
+ for mail in self._new_mails:
+ self.assertEqual(mail.author_id, self.partner_employee)
+ self.assertEqual(mail.subject, 'Guidelines of channel %s' % self.channel_1.name)
+ self.assertEqual(mail.state, 'outgoing')
+ self.assertEqual(mail.email_from, self.user_employee.company_id.catchall_formatted)
+ self.assertEqual(self._new_mails.mapped('recipient_ids'), self.partner_employee | self.partner_portal)
+
+ def test_send_guidelines_crash(self):
+ self.channel_1.write({
+ 'channel_partner_ids': [(4, self.partner_admin.id)],
+ 'moderator_ids': [(4, self.user_admin.id), (3, self.user_employee.id)]
+ })
+ with self.assertRaises(UserError):
+ self.channel_1.with_user(self.user_employee).send_guidelines()
+
+ def test_update_moderation_email(self):
+ self.channel_1.write({'moderation_ids': [
+ (0, 0, {'email': 'test0@example.com', 'status': 'allow'}),
+ (0, 0, {'email': 'test1@example.com', 'status': 'ban'})
+ ]})
+ self.channel_1._update_moderation_email(['test0@example.com', 'test3@example.com'], 'ban')
+ self.assertEqual(len(self.channel_1.moderation_ids), 3)
+ self.assertTrue(all(status == 'ban' for status in self.channel_1.moderation_ids.mapped('status')))
+
+ def test_moderation_reset(self):
+ self.channel_2 = self.env['mail.channel'].create({
+ 'name': 'Moderation_1',
+ 'email_send': True,
+ 'moderation': True,
+ 'channel_partner_ids': [(4, self.partner_employee.id)],
+ 'moderator_ids': [(4, self.user_employee.id)],
+ })
+
+ self.msg_c1_1 = self._add_messages(self.channel_1, 'Body11', author=self.partner_admin, moderation_status='accepted')
+ self.msg_c1_2 = self._add_messages(self.channel_1, 'Body12', author=self.partner_admin, moderation_status='pending_moderation')
+ self.msg_c2_1 = self._add_messages(self.channel_2, 'Body21', author=self.partner_admin, moderation_status='pending_moderation')
+
+ self.assertEqual(self.env['mail.message'].search_count([
+ ('moderation_status', '=', 'pending_moderation'),
+ ('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id)
+ ]), 1)
+ self.channel_1.write({'moderation': False})
+ self.assertEqual(self.env['mail.message'].search_count([
+ ('moderation_status', '=', 'pending_moderation'),
+ ('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id)
+ ]), 0)
+ self.assertEqual(self.env['mail.message'].search_count([
+ ('moderation_status', '=', 'pending_moderation'),
+ ('model', '=', 'mail.channel'), ('res_id', '=', self.channel_2.id)
+ ]), 1)
+ self.channel_2.write({'moderation': False})
+ self.assertEqual(self.env['mail.message'].search_count([
+ ('moderation_status', '=', 'pending_moderation'),
+ ('model', '=', 'mail.channel'), ('res_id', '=', self.channel_2.id)
+ ]), 0)
+
+ @mute_logger('odoo.models.unlink')
+ def test_message_post(self):
+ email1 = 'test0@example.com'
+ email2 = 'test1@example.com'
+
+ self.channel_1._update_moderation_email([email1], 'ban')
+ self.channel_1._update_moderation_email([email2], 'allow')
+
+ msg_admin = self.channel_1.message_post(message_type='email', subtype_xmlid='mail.mt_comment', author_id=self.partner_admin.id)
+ msg_moderator = self.channel_1.message_post(message_type='comment', subtype_xmlid='mail.mt_comment', author_id=self.partner_employee.id)
+ msg_email1 = self.channel_1.message_post(message_type='comment', subtype_xmlid='mail.mt_comment', email_from=formataddr(("MyName", email1)))
+ msg_email2 = self.channel_1.message_post(message_type='email', subtype_xmlid='mail.mt_comment', email_from=email2)
+ msg_notif = self.channel_1.message_post()
+
+ messages = self.env['mail.message'].search([('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id)])
+ pending_messages = messages.filtered(lambda m: m.moderation_status == 'pending_moderation')
+ accepted_messages = messages.filtered(lambda m: m.moderation_status == 'accepted')
+
+ self.assertFalse(msg_email1)
+ self.assertEqual(msg_admin, pending_messages)
+ self.assertEqual(accepted_messages, msg_moderator | msg_email2 | msg_notif)
+ self.assertFalse(msg_admin.channel_ids)
+ self.assertEqual(msg_email2.channel_ids, self.channel_1)
+
+ def test_user_is_moderator(self):
+ self.assertTrue(self.user_employee.is_moderator)
+ self.assertFalse(self.user_employee_2.is_moderator)
+ self.channel_1.write({
+ 'channel_partner_ids': [(4, self.partner_employee_2.id)],
+ 'moderator_ids': [(4, self.user_employee_2.id)],
+ })
+ self.assertTrue(self.user_employee_2.is_moderator)
+
+ def test_user_moderation_counter(self):
+ self._add_messages(self.channel_1, 'B', moderation_status='pending_moderation', author=self.partner_employee_2)
+ self._add_messages(self.channel_1, 'B', moderation_status='accepted', author=self.partner_employee_2)
+ self._add_messages(self.channel_1, 'B', moderation_status='accepted', author=self.partner_employee)
+ self._add_messages(self.channel_1, 'B', moderation_status='pending_moderation', author=self.partner_employee)
+ self._add_messages(self.channel_1, 'B', moderation_status='accepted', author=self.partner_employee)
+
+ self.assertEqual(self.user_employee.moderation_counter, 2)
+ self.assertEqual(self.user_employee_2.moderation_counter, 0)
+
+ self.channel_1.write({
+ 'channel_partner_ids': [(4, self.partner_employee_2.id)],
+ 'moderator_ids': [(4, self.user_employee_2.id)]
+ })
+ self.assertEqual(self.user_employee.moderation_counter, 2)
+ self.assertEqual(self.user_employee_2.moderation_counter, 0)
diff --git a/addons/test_mail/tests/test_mail_channel_partner.py b/addons/test_mail/tests/test_mail_channel_partner.py
new file mode 100644
index 00000000..93dd7072
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_channel_partner.py
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+
+from odoo.tests.common import SavepointCase
+from odoo.exceptions import AccessError, UserError
+
+
+class TestMailSecurity(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.secret_group = cls.env['res.groups'].create({
+ 'name': 'secret group',
+ })
+ cls.user_1 = cls.env['res.users'].create({
+ 'name': 'User 1',
+ 'login': 'user_1',
+ 'email': '---',
+ 'groups_id': [(6, 0, [cls.secret_group.id, cls.env.ref('base.group_user').id])],
+ })
+ cls.user_2 = cls.env['res.users'].create({
+ 'name': 'User 2',
+ 'login': 'user_2',
+ 'email': '---',
+ 'groups_id': [(6, 0, [cls.secret_group.id, cls.env.ref('base.group_user').id])],
+ })
+ cls.user_3 = cls.env['res.users'].create({
+ 'name': 'User 3',
+ 'login': 'user_3',
+ 'email': '---',
+ })
+
+ cls.private_channel_1 = cls.env['mail.channel'].create({
+ 'name': 'Secret channel',
+ 'public': 'private',
+ 'channel_type': 'channel',
+ })
+ cls.group_channel_1 = cls.env['mail.channel'].create({
+ 'name': 'Group channel',
+ 'public': 'groups',
+ 'channel_type': 'channel',
+ 'group_public_id': cls.secret_group.id,
+ })
+ cls.public_channel_1 = cls.env['mail.channel'].create({
+ 'name': 'Public channel of user 1',
+ 'public': 'public',
+ 'channel_type': 'channel',
+ })
+ cls.private_channel_1.channel_last_seen_partner_ids.unlink()
+ cls.group_channel_1.channel_last_seen_partner_ids.unlink()
+ cls.public_channel_1.channel_last_seen_partner_ids.unlink()
+
+ ###########################
+ # PRIVATE CHANNEL & BASIC #
+ ###########################
+
+ def test_channel_acls_01(self):
+ """Test access on private channel."""
+ res = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)])
+ self.assertFalse(res)
+
+ # User 1 can join private channel with SUDO
+ self.private_channel_1.with_user(self.user_1).sudo().action_follow()
+ res = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)])
+ self.assertEqual(res.partner_id, self.user_1.partner_id)
+
+ # User 2 can not join private channel
+ with self.assertRaises(AccessError):
+ self.private_channel_1.with_user(self.user_2).action_follow()
+
+ # But user 2 can join public channel
+ self.public_channel_1.with_user(self.user_2).action_follow()
+ res = self.env['mail.channel.partner'].search([('channel_id', '=', self.public_channel_1.id)])
+ self.assertEqual(res.partner_id, self.user_2.partner_id)
+
+ # User 2 can not create a `mail.channel.partner` to join the private channel
+ with self.assertRaises(AccessError):
+ self.env['mail.channel.partner'].with_user(self.user_2).create({
+ 'partner_id': self.user_2.partner_id.id,
+ 'channel_id': self.private_channel_1.id,
+ })
+
+ # User 2 can not write on `mail.channel.partner` to join the private channel
+ channel_partner = self.env['mail.channel.partner'].with_user(self.user_2).search([('partner_id', '=', self.user_2.partner_id.id)])[0]
+ with self.assertRaises(AccessError):
+ channel_partner.channel_id = self.private_channel_1.id
+ with self.assertRaises(AccessError):
+ channel_partner.write({'channel_id': self.private_channel_1.id})
+
+ # But with SUDO, User 2 can
+ channel_partner.sudo().channel_id = self.private_channel_1.id
+
+ # User 2 can not write on the `partner_id` of `mail.channel.partner`
+ # of an other partner to join a private channel
+ channel_partner_1 = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id), ('partner_id', '=', self.user_1.partner_id.id)])
+ with self.assertRaises(AccessError):
+ channel_partner_1.with_user(self.user_2).partner_id = self.user_2.partner_id
+ self.assertEqual(channel_partner_1.partner_id, self.user_1.partner_id)
+
+ # but with SUDO he can...
+ channel_partner_1.with_user(self.user_2).sudo().partner_id = self.user_2.partner_id
+ self.assertEqual(channel_partner_1.partner_id, self.user_2.partner_id)
+
+ def test_channel_acls_03(self):
+ """Test invitation in private channel part 1 (invite using crud methods)."""
+ self.private_channel_1.with_user(self.user_1).sudo().action_follow()
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)])
+ self.assertEqual(len(channel_partners), 1)
+
+ # User 2 is not in the private channel, he can not invite user 3
+ with self.assertRaises(AccessError):
+ self.env['mail.channel.partner'].with_user(self.user_2).create({
+ 'partner_id': self.user_3.partner_id.id,
+ 'channel_id': self.private_channel_1.id,
+ })
+
+ # User 1 is in the private channel, he can invite other users
+ self.env['mail.channel.partner'].with_user(self.user_1).create({
+ 'partner_id': self.user_3.partner_id.id,
+ 'channel_id': self.private_channel_1.id,
+ })
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_3.partner_id)
+
+ # But User 3 can not write on the `mail.channel.partner` of other user
+ channel_partner_1 = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id), ('partner_id', '=', self.user_1.partner_id.id)])
+ channel_partner_3 = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id), ('partner_id', '=', self.user_3.partner_id.id)])
+ channel_partner_3.with_user(self.user_3).custom_channel_name = 'Test'
+ with self.assertRaises(AccessError):
+ channel_partner_1.with_user(self.user_2).custom_channel_name = 'Blabla'
+ self.assertNotEqual(channel_partner_1.custom_channel_name, 'Blabla')
+
+ def test_channel_acls_04(self):
+ """Test invitation in private channel part 2 (use `invite` action)."""
+ self.private_channel_1.with_user(self.user_1).sudo().action_follow()
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
+
+ # User 2 is not in the channel, he can not invite user 3
+ with self.assertRaises(AccessError):
+ self.private_channel_1.with_user(self.user_2).channel_invite([self.user_3.partner_id.id])
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
+
+ # User 1 is in the channel, he can not invite user 3
+ self.private_channel_1.with_user(self.user_1).channel_invite([self.user_3.partner_id.id])
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_3.partner_id)
+
+ def test_channel_acls_05(self):
+ """Test kick/leave channel."""
+ self.private_channel_1.with_user(self.user_1).sudo().action_follow()
+ self.private_channel_1.with_user(self.user_3).sudo().action_follow()
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)])
+ self.assertEqual(len(channel_partners), 2)
+
+ # User 2 is not in the channel, he can not kick user 1
+ with self.assertRaises(AccessError):
+ channel_partners.with_user(self.user_2).unlink()
+
+ # User 3 is in the channel, he can kick user 1
+ channel_partners.with_user(self.user_3).unlink()
+
+ #################
+ # GROUP CHANNEL #
+ #################
+ def test_channel_acls_06(self):
+ """Test basics on group channel."""
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)])
+ self.assertFalse(channel_partners)
+
+ # user 1 is in the group, he can join the channel
+ self.group_channel_1.with_user(self.user_1).action_follow()
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
+
+ # user 3 is not in the group, he can not join
+ with self.assertRaises(AccessError):
+ self.group_channel_1.with_user(self.user_3).action_follow()
+
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)])
+ with self.assertRaises(AccessError):
+ channel_partners.with_user(self.user_3).partner_id = self.user_3.partner_id
+
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
+
+ # user 1 can not invite user 3 because he's not in the group
+ with self.assertRaises(UserError):
+ self.group_channel_1.with_user(self.user_1).channel_invite([self.user_3.partner_id.id])
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
+
+ # but user 2 is in the group and can be invited by user 1
+ self.group_channel_1.with_user(self.user_1).channel_invite([self.user_2.partner_id.id])
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id)
+
+ ##################
+ # PUBLIC CHANNEL #
+ ##################
+ def test_channel_acls_07(self):
+ """Test basics on public channel."""
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.public_channel_1.id)])
+ self.assertFalse(channel_partners)
+
+ self.public_channel_1.with_user(self.user_1).action_follow()
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.public_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id)
+
+ self.public_channel_1.with_user(self.user_2).action_follow()
+ channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.public_channel_1.id)])
+ self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id)
diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py
new file mode 100644
index 00000000..6c60ea3d
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_composer.py
@@ -0,0 +1,842 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+
+from unittest.mock import patch
+
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.addons.test_mail.models.test_mail_models import MailTestTicket
+from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
+from odoo.tests import tagged
+from odoo.tests.common import users, Form
+from odoo.tools import mute_logger
+
+@tagged('mail_composer')
+class TestMailComposer(TestMailCommon, TestRecipients):
+ """ Test Composer internals """
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailComposer, cls).setUpClass()
+ cls._init_mail_gateway()
+
+ # ensure employee can create partners, necessary for templates
+ cls.user_employee.write({
+ 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)],
+ })
+
+ cls.user_employee_2 = mail_new_test_user(
+ cls.env, login='employee2', groups='base.group_user',
+ notification_type='email', email='eglantine@example.com',
+ name='Eglantine Employee', signature='--\nEglantine')
+ cls.partner_employee_2 = cls.user_employee_2.partner_id
+
+ cls.test_record = cls.env['mail.test.ticket'].with_context(cls._test_context).create({
+ 'name': 'TestRecord',
+ 'customer_id': cls.partner_1.id,
+ 'user_id': cls.user_employee_2.id,
+ })
+ cls.test_records, cls.test_partners = cls._create_records_for_batch('mail.test.ticket', 2)
+
+ cls.test_report = cls.env['ir.actions.report'].create({
+ 'name': 'Test Report on mail test ticket',
+ 'model': 'mail.test.ticket',
+ 'report_type': 'qweb-pdf',
+ 'report_name': 'test_mail.mail_test_ticket_test_template',
+ })
+ cls.test_record_report = cls.test_report._render_qweb_pdf(cls.test_report.ids)
+
+ cls.test_from = '"John Doe" <john@example.com>'
+
+ cls.mail_server = cls.env['ir.mail_server'].create({
+ 'name': 'Dummy Test Server',
+ 'smtp_host': 'smtp.pizza.moc',
+ 'smtp_port': 17,
+ 'smtp_encryption': 'ssl',
+ 'sequence': 666,
+ })
+
+ cls.template = cls.env['mail.template'].create({
+ 'name': 'TestTemplate',
+ 'subject': 'TemplateSubject ${object.name}',
+ 'body_html': '<p>TemplateBody ${object.name}</p>',
+ 'partner_to': '${object.customer_id.id if object.customer_id else ""}',
+ 'email_to': '${(object.email_from if not object.customer_id else "") | safe}',
+ 'email_from': '${(object.user_id.email_formatted or user.email_formatted) | safe}',
+ 'model_id': cls.env['ir.model']._get('mail.test.ticket').id,
+ 'mail_server_id': cls.mail_server.id,
+ 'auto_delete': True,
+ })
+
+ def _generate_attachments_data(self, count):
+ return [{
+ 'name': '%02d.txt' % x,
+ 'datas': base64.b64encode(b'Att%02d' % x),
+ } for x in range(count)]
+
+ def _get_web_context(self, records, add_web=True, **values):
+ """ Helper to generate composer context. Will make tests a bit less
+ verbose.
+
+ :param add_web: add web context, generally making noise especially in
+ mass mail mode (active_id/ids both present in context)
+ """
+ base_context = {
+ 'default_model': records._name,
+ }
+ if len(records) == 1:
+ base_context['default_composition_mode'] = 'comment'
+ base_context['default_res_id'] = records.id
+ else:
+ base_context['default_composition_mode'] = 'mass_mail'
+ base_context['active_ids'] = records.ids
+ if add_web:
+ base_context['active_model'] = records._name
+ base_context['active_id'] = records[0].id
+ if values:
+ base_context.update(**values)
+ return base_context
+
+
+@tagged('mail_composer')
+class TestComposerForm(TestMailComposer):
+
+ @users('employee')
+ def test_mail_composer_comment(self):
+ composer_form = Form(self.env['mail.compose.message'].with_context(self._get_web_context(self.test_record, add_web=True)))
+ self.assertEqual(
+ composer_form.subject, 'Re: %s' % self.test_record.name,
+ 'MailComposer: comment mode should have default subject Re: record_name')
+ # record name not displayed currently in view
+ # self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name')
+ self.assertFalse(composer_form.no_auto_thread)
+ self.assertEqual(composer_form.composition_mode, 'comment')
+ self.assertEqual(composer_form.model, self.test_record._name)
+
+ @users('employee')
+ def test_mail_composer_comment_attachments(self):
+ """Tests that all attachments are added to the composer, static attachments
+ are not duplicated and while reports are re-generated, and that intermediary
+ attachments are dropped."""
+ attachment_data = self._generate_attachments_data(2)
+ template_1 = self.template.copy({
+ 'attachment_ids': [(0, 0, a) for a in attachment_data],
+ 'report_name': 'TestReport for ${object.name}.html', # test cursor forces html
+ 'report_template': self.test_report.id,
+ })
+ template_1_attachments = template_1.attachment_ids
+ self.assertEqual(len(template_1_attachments), 2)
+ template_2 = self.template.copy({
+ 'attachment_ids': False,
+ 'report_template': self.test_report.id,
+ })
+
+ # begins without attachments
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_record, add_web=True, default_attachment_ids=[])
+ ))
+ self.assertEqual(len(composer_form.attachment_ids), 0)
+
+ # change template: 2 static (attachment_ids) and 1 dynamic (report)
+ composer_form.template_id = template_1
+ self.assertEqual(len(composer_form.attachment_ids), 3)
+ report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments]
+ self.assertEqual(len(report_attachments), 1)
+ tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0]
+ self.assertEqual(tpl_attachments, template_1_attachments)
+
+ # change template: 0 static (attachment_ids) and 1 dynamic (report)
+ composer_form.template_id = template_2
+ self.assertEqual(len(composer_form.attachment_ids), 1)
+ report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments]
+ self.assertEqual(len(report_attachments), 1)
+ tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0]
+ self.assertEqual(tpl_attachments, self.env['ir.attachment'])
+
+ # change back to template 1
+ composer_form.template_id = template_1
+ self.assertEqual(len(composer_form.attachment_ids), 3)
+ report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments]
+ self.assertEqual(len(report_attachments), 1)
+ tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0]
+ self.assertEqual(tpl_attachments, template_1_attachments)
+
+ # reset template
+ composer_form.template_id = self.env['mail.template']
+ self.assertEqual(len(composer_form.attachment_ids), 0)
+
+ @users('employee')
+ def test_mail_composer_mass(self):
+ composer_form = Form(self.env['mail.compose.message'].with_context(self._get_web_context(self.test_records, add_web=True)))
+ self.assertFalse(composer_form.subject, 'MailComposer: mass mode should have void default subject if no template')
+ # record name not displayed currently in view
+ # self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name')
+ self.assertFalse(composer_form.no_auto_thread)
+ self.assertEqual(composer_form.composition_mode, 'mass_mail')
+ self.assertEqual(composer_form.model, self.test_records._name)
+
+ @users('employee')
+ def test_mail_composer_mass_wtpl(self):
+ ctx = self._get_web_context(self.test_records, add_web=True, default_template_id=self.template.id)
+ composer_form = Form(self.env['mail.compose.message'].with_context(ctx))
+ self.assertEqual(composer_form.subject, self.template.subject,
+ 'MailComposer: mass mode should have template raw subject if template')
+ self.assertEqual(composer_form.body, self.template.body_html,
+ 'MailComposer: mass mode should have template raw body if template')
+ # record name not displayed currently in view
+ # self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name')
+ self.assertFalse(composer_form.no_auto_thread)
+ self.assertEqual(composer_form.composition_mode, 'mass_mail')
+ self.assertEqual(composer_form.model, self.test_records._name)
+
+
+@tagged('mail_composer')
+class TestComposerInternals(TestMailComposer):
+
+ @users('employee')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_attachments_comment(self):
+ """ Test attachments management in comment mode. """
+ attachment_data = self._generate_attachments_data(3)
+ self.template.write({
+ 'attachment_ids': [(0, 0, a) for a in attachment_data],
+ 'report_name': 'TestReport for ${object.name}.html', # test cursor forces html
+ 'report_template': self.test_report.id,
+ })
+ attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])])
+ self.assertEqual(len(attachs), 3)
+
+ composer = self.env['mail.compose.message'].with_context({
+ 'default_composition_mode': 'comment',
+ 'default_model': self.test_record._name,
+ 'default_res_id': self.test_record.id,
+ 'default_template_id': self.template.id,
+ }).create({
+ 'body': '<p>Test Body</p>',
+ })
+ # currently onchange necessary
+ composer.onchange_template_id_wrapper()
+
+ # values coming from template
+ self.assertEqual(len(composer.attachment_ids), 4)
+ for attach in attachs:
+ self.assertIn(attach, composer.attachment_ids)
+ generated = composer.attachment_ids - attachs
+ self.assertEqual(len(generated), 1, 'MailComposer: should have 1 additional attachment for report')
+ self.assertEqual(generated.name, 'TestReport for %s.html' % self.test_record.name)
+ self.assertEqual(generated.res_model, 'mail.compose.message')
+ self.assertEqual(generated.res_id, 0)
+
+ @users('employee')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_author(self):
+ """ Test author_id / email_from synchronization, in both comment and mass mail
+ modes. """
+ for composition_mode in ['comment', 'mass_mail']:
+ if composition_mode == 'comment':
+ ctx = self._get_web_context(self.test_record, add_web=False)
+ else:
+ ctx = self._get_web_context(self.test_records, add_web=False)
+ composer = self.env['mail.compose.message'].with_context(ctx).create({
+ 'body': '<p>Test Body</p>',
+ })
+
+ # default values are current user
+ self.assertEqual(composer.author_id, self.env.user.partner_id)
+ self.assertEqual(composer.email_from, self.env.user.email_formatted)
+
+ # author values reset email (FIXME: currently not synchronized)
+ composer.write({'author_id': self.partner_1})
+ self.assertEqual(composer.author_id, self.partner_1)
+ self.assertEqual(composer.email_from, self.env.user.email_formatted)
+ # self.assertEqual(composer.email_from, self.partner_1.email_formatted)
+
+ # changing template should update its email_from
+ composer.write({'template_id': self.template.id, 'author_id': self.env.user.partner_id})
+ # currently onchange necessary
+ composer.onchange_template_id_wrapper()
+ self.assertEqual(composer.author_id, self.env.user.partner_id,
+ 'MailComposer: should take value given by user')
+ if composition_mode == 'comment':
+ self.assertEqual(composer.email_from, self.test_record.user_id.email_formatted,
+ 'MailComposer: should take email_from rendered from template')
+ else:
+ self.assertEqual(composer.email_from, self.template.email_from,
+ 'MailComposer: should take email_from raw from template')
+
+ # manual values are kept over template values
+ composer.write({'email_from': self.test_from})
+ self.assertEqual(composer.author_id, self.env.user.partner_id)
+ self.assertEqual(composer.email_from, self.test_from)
+
+ @users('employee')
+ def test_mail_composer_content(self):
+ """ Test content management (subject, body, server) in both comment and
+ mass mailing mode. Template update is also tested. """
+ for composition_mode in ['comment', 'mass_mail']:
+ if composition_mode == 'comment':
+ ctx = self._get_web_context(self.test_record, add_web=False)
+ else:
+ ctx = self._get_web_context(self.test_records, add_web=False)
+
+ # 1. check without template + template update
+ composer = self.env['mail.compose.message'].with_context(ctx).create({
+ 'subject': 'My amazing subject',
+ 'body': '<p>Test Body</p>',
+ })
+
+ # creation values are taken
+ self.assertEqual(composer.subject, 'My amazing subject')
+ self.assertEqual(composer.body, '<p>Test Body</p>')
+ self.assertEqual(composer.mail_server_id.id, False)
+
+ # changing template should update its content
+ composer.write({'template_id': self.template.id})
+ # currently onchange necessary
+ composer.onchange_template_id_wrapper()
+
+ # values come from template
+ if composition_mode == 'comment':
+ self.assertEqual(composer.subject, 'TemplateSubject %s' % self.test_record.name)
+ self.assertEqual(composer.body, '<p>TemplateBody %s</p>' % self.test_record.name)
+ self.assertEqual(composer.mail_server_id, self.template.mail_server_id)
+ else:
+ self.assertEqual(composer.subject, self.template.subject)
+ self.assertEqual(composer.body, self.template.body_html)
+ self.assertEqual(composer.mail_server_id, self.template.mail_server_id)
+
+ # manual values is kept over template
+ composer.write({'subject': 'Back to my amazing subject'})
+ self.assertEqual(composer.subject, 'Back to my amazing subject')
+
+ # reset template should reset values
+ composer.write({'template_id': False})
+ # currently onchange necessary
+ composer.onchange_template_id_wrapper()
+
+ # values are reset
+ if composition_mode == 'comment':
+ self.assertEqual(composer.subject, 'Re: %s' % self.test_record.name)
+ self.assertEqual(composer.body, '')
+ # TDE FIXME: server id is kept, not sure why
+ # self.assertFalse(composer.mail_server_id.id)
+ self.assertEqual(composer.mail_server_id, self.template.mail_server_id)
+ else:
+ # values are reset TDE FIXME: strange for subject
+ self.assertEqual(composer.subject, 'Back to my amazing subject')
+ self.assertEqual(composer.body, '')
+ # TDE FIXME: server id is kept, not sure why
+ # self.assertFalse(composer.mail_server_id.id)
+ self.assertEqual(composer.mail_server_id, self.template.mail_server_id)
+
+ # 2. check with default
+ ctx['default_template_id'] = self.template.id
+ composer = self.env['mail.compose.message'].with_context(ctx).create({
+ 'template_id': self.template.id,
+ })
+ # currently onchange necessary
+ composer.onchange_template_id_wrapper()
+
+ # values come from template
+ if composition_mode == 'comment':
+ self.assertEqual(composer.subject, 'TemplateSubject %s' % self.test_record.name)
+ self.assertEqual(composer.body, '<p>TemplateBody %s</p>' % self.test_record.name)
+ self.assertEqual(composer.mail_server_id, self.template.mail_server_id)
+ else:
+ self.assertEqual(composer.subject, self.template.subject)
+ self.assertEqual(composer.body, self.template.body_html)
+ self.assertEqual(composer.mail_server_id, self.template.mail_server_id)
+
+ # 3. check at create
+ ctx.pop('default_template_id')
+ composer = self.env['mail.compose.message'].with_context(ctx).create({
+ 'template_id': self.template.id,
+ })
+ # currently onchange necessary
+ composer.onchange_template_id_wrapper()
+
+ # values come from template
+ if composition_mode == 'comment':
+ self.assertEqual(composer.subject, 'TemplateSubject %s' % self.test_record.name)
+ self.assertEqual(composer.body, '<p>TemplateBody %s</p>' % self.test_record.name)
+ self.assertEqual(composer.mail_server_id, self.template.mail_server_id)
+ else:
+ self.assertEqual(composer.subject, self.template.subject)
+ self.assertEqual(composer.body, self.template.body_html)
+ self.assertEqual(composer.mail_server_id, self.template.mail_server_id)
+
+ # 4. template + user input
+ ctx['default_template_id'] = self.template.id
+ composer = self.env['mail.compose.message'].with_context(ctx).create({
+ 'subject': 'My amazing subject',
+ 'body': '<p>Test Body</p>',
+ 'mail_server_id': False,
+ })
+
+ # creation values are taken
+ self.assertEqual(composer.subject, 'My amazing subject')
+ self.assertEqual(composer.body, '<p>Test Body</p>')
+ self.assertEqual(composer.mail_server_id.id, False)
+
+ @users('employee')
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_parent(self):
+ """ Test specific management in comment mode when having parent_id set:
+ record_name, subject, parent's partners. """
+ parent = self.test_record.message_post(body='Test', partner_ids=(self.partner_1 + self.partner_2).ids)
+
+ composer = self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_record, add_web=False, default_parent_id=parent.id)
+ ).create({
+ 'body': '<p>Test Body</p>',
+ })
+
+ # creation values taken from parent
+ self.assertEqual(composer.subject, 'Re: %s' % self.test_record.name)
+ self.assertEqual(composer.body, '<p>Test Body</p>')
+ self.assertEqual(composer.partner_ids, self.partner_1 + self.partner_2)
+
+ def test_mail_composer_rights_portal(self):
+ portal_user = self._create_portal_user()
+
+ with patch.object(MailTestTicket, 'check_access_rights', return_value=True):
+ self.env['mail.compose.message'].with_user(portal_user).with_context(
+ self._get_web_context(self.test_record)
+ ).create({
+ 'subject': 'Subject',
+ 'body': '<p>Body text</p>',
+ 'partner_ids': []
+ }).send_mail()
+
+ self.assertEqual(self.test_record.message_ids[0].body, '<p>Body text</p>')
+ self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id)
+
+ self.env['mail.compose.message'].with_user(portal_user).with_context({
+ 'default_composition_mode': 'comment',
+ 'default_parent_id': self.test_record.message_ids.ids[0],
+ }).create({
+ 'subject': 'Subject',
+ 'body': '<p>Body text 2</p>'
+ }).send_mail()
+
+ self.assertEqual(self.test_record.message_ids[0].body, '<p>Body text 2</p>')
+ self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id)
+
+ @users('employee')
+ def test_mail_composer_save_template(self):
+ self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_record, add_web=False)
+ ).create({
+ 'subject': 'Template Subject',
+ 'body': '<p>Template Body</p>',
+ }).save_as_template()
+
+ # Test: email_template subject, body_html, model
+ template = self.env['mail.template'].search([
+ ('model', '=', self.test_record._name),
+ ('subject', '=', 'Template Subject')
+ ], limit=1)
+ self.assertEqual(template.name, "%s: %s" % (self.env['ir.model']._get(self.test_record._name).name, 'Template Subject'))
+ self.assertEqual(template.body_html, '<p>Template Body</p>', 'email_template incorrect body_html')
+
+
+@tagged('mail_composer')
+class TestComposerResultsComment(TestMailComposer):
+ """ Test global output of composer used in comment mode. Test notably
+ notification and emails generated during this process. """
+
+ @users('employee')
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_notifications_delete(self):
+ """ Notifications are correctly deleted once sent """
+ composer = self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_record)
+ ).create({
+ 'body': '<p>Test Body</p>',
+ 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)]
+ })
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ composer.send_mail()
+
+ # notifications
+ message = self.test_record.message_ids[0]
+ self.assertEqual(message.notified_partner_ids, self.partner_employee_2 + self.partner_1 + self.partner_2)
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail (1 for users, 1 for customers)')
+ self.assertEqual(len(self._mails), 3, 'Should have sent an email each recipient')
+ self.assertEqual(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records')
+
+ # ensure ``mail_auto_delete`` context key allow to override this behavior
+ composer = self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_record),
+ mail_auto_delete=False,
+ ).create({
+ 'body': '<p>Test Body</p>',
+ 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)]
+ })
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ composer.send_mail()
+
+ # notifications
+ message = self.test_record.message_ids[0]
+ self.assertEqual(message.notified_partner_ids, self.partner_employee_2 + self.partner_1 + self.partner_2)
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail (1 for users, 1 for customers)')
+ self.assertEqual(len(self._mails), 3, 'Should have sent an email each recipient')
+ self.assertEqual(len(self._new_mails.exists()), 2, 'Should not have deleted mail.mail records')
+
+ @users('employee')
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_recipients(self):
+ """ Test partner_ids given to composer are given to the final message. """
+ composer = self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_record)
+ ).create({
+ 'body': '<p>Test Body</p>',
+ 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)]
+ })
+ composer.send_mail()
+
+ message = self.test_record.message_ids[0]
+ self.assertEqual(message.body, '<p>Test Body</p>')
+ self.assertEqual(message.author_id, self.user_employee.partner_id)
+ self.assertEqual(message.subject, 'Re: %s' % self.test_record.name)
+ self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment'))
+ self.assertEqual(message.partner_ids, self.partner_1 | self.partner_2)
+
+ @users('employee')
+ @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_wtpl_complete(self):
+ """ Test a posting process using a complex template, holding several
+ additional recipients and attachments.
+
+ This tests notifies: 2 new email_to (+ 1 duplicated), 1 email_cc,
+ test_record followers and partner_admin added in partner_to."""
+ attachment_data = self._generate_attachments_data(2)
+ email_to_1 = 'test.to.1@test.example.com'
+ email_to_2 = 'test.to.2@test.example.com'
+ email_to_3 = 'test.to.1@test.example.com' # duplicate: should not sent twice the email
+ email_cc_1 = 'test.cc.1@test.example.com'
+ self.template.write({
+ 'auto_delete': False, # keep sent emails to check content
+ 'attachment_ids': [(0, 0, a) for a in attachment_data],
+ 'email_to': '%s, %s, %s' % (email_to_1, email_to_2, email_to_3),
+ 'email_cc': email_cc_1,
+ 'partner_to': '%s, ${object.customer_id.id if object.customer_id else ""}' % self.partner_admin.id,
+ 'report_name': 'TestReport for ${object.name}', # test cursor forces html
+ 'report_template': self.test_report.id,
+ })
+ attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])])
+ self.assertEqual(len(attachs), 2)
+
+ # ensure initial data
+ self.assertEqual(self.test_record.user_id, self.user_employee_2)
+ self.assertEqual(self.test_record.message_partner_ids, self.partner_employee_2)
+
+ # open a composer and run it in comment mode
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_record, add_web=True,
+ default_template_id=self.template.id)
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
+ composer.send_mail()
+
+ # check new partners have been created based on emails given
+ new_partners = self.env['res.partner'].search([
+ ('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1])
+ ])
+ self.assertEqual(len(new_partners), 3)
+ self.assertEqual(set(new_partners.mapped('email')),
+ set(['test.to.1@test.example.com', 'test.to.2@test.example.com', 'test.cc.1@test.example.com'])
+ )
+
+ # global outgoing: one mail.mail (all customer recipients, then all employee recipients)
+ # and 5 emails, and 1 inbox notification (admin)
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail')
+ self.assertEqual(len(self._mails), 5, 'Should have sent 5 emails, one per recipient')
+
+ # template is sent only to partners (email_to are transformed)
+ message = self.test_record.message_ids[0]
+ self.assertMailMail(self.partner_employee_2, 'sent',
+ mail_message=message,
+ author=self.partner_employee, # author != email_from (template sets only email_from)
+ email_values={
+ 'body_content': 'TemplateBody %s' % self.test_record.name,
+ 'email_from': self.test_record.user_id.email_formatted, # set by template
+ 'subject': 'TemplateSubject %s' % self.test_record.name,
+ 'attachments_info': [
+ {'name': '00.txt', 'raw': b'Att00', 'type': 'text/plain'},
+ {'name': '01.txt', 'raw': b'Att01', 'type': 'text/plain'},
+ {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'},
+ ]
+ },
+ fields_values={},
+ )
+ self.assertMailMail(self.test_record.customer_id + new_partners, 'sent',
+ mail_message=message,
+ author=self.partner_employee, # author != email_from (template sets only email_from)
+ email_values={
+ 'body_content': 'TemplateBody %s' % self.test_record.name,
+ 'email_from': self.test_record.user_id.email_formatted, # set by template
+ 'subject': 'TemplateSubject %s' % self.test_record.name,
+ 'attachments_info': [
+ {'name': '00.txt', 'raw': b'Att00', 'type': 'text/plain'},
+ {'name': '01.txt', 'raw': b'Att01', 'type': 'text/plain'},
+ {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'},
+ ]
+ },
+ fields_values={},
+ )
+
+ # message is posted and notified admin
+ self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment'))
+ self.assertNotified(message, [{'partner': self.partner_admin, 'is_read': False, 'type': 'inbox'}])
+ # attachments are copied on message and linked to document
+ self.assertEqual(
+ set(message.attachment_ids.mapped('name')),
+ set(['00.txt', '01.txt', 'TestReport for %s.html' % self.test_record.name])
+ )
+ self.assertEqual(set(message.attachment_ids.mapped('res_model')), set([self.test_record._name]))
+ self.assertEqual(set(message.attachment_ids.mapped('res_id')), set(self.test_record.ids))
+ self.assertTrue(all(attach not in message.attachment_ids for attach in attachs), 'Should have copied attachments')
+
+
+@tagged('mail_composer')
+class TestComposerResultsMass(TestMailComposer):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestComposerResultsMass, 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)],
+ })
+
+ @users('employee')
+ @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_wtpl(self):
+ self.template.auto_delete = False # keep sent emails to check content
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_records, add_web=True,
+ default_template_id=self.template.id)
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ composer.send_mail()
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record')
+ self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record')
+
+ for record in self.test_records:
+ # message copy is kept
+ message = record.message_ids[0]
+
+ # template is sent directly using customer field, meaning we have recipients
+ self.assertMailMail(record.customer_id, 'sent',
+ mail_message=message,
+ author=self.partner_employee)
+
+ # message content
+ self.assertEqual(message.subject, 'TemplateSubject %s' % record.name)
+ self.assertEqual(message.body, '<p>TemplateBody %s</p>' % record.name)
+ self.assertEqual(message.author_id, self.user_employee.partner_id)
+ # post-related fields are void
+ self.assertEqual(message.subtype_id, self.env['mail.message.subtype'])
+ self.assertEqual(message.partner_ids, self.env['res.partner'])
+
+ @users('employee')
+ @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_wtpl_complete(self):
+ """ Test a composer in mass mode with a quite complete template, containing
+ notably email-based recipients and attachments. """
+ attachment_data = self._generate_attachments_data(2)
+ email_to_1 = 'test.to.1@test.example.com'
+ email_to_2 = 'test.to.2@test.example.com'
+ email_to_3 = 'test.to.1@test.example.com' # duplicate: should not sent twice the email
+ email_cc_1 = 'test.cc.1@test.example.com'
+ self.template.write({
+ 'auto_delete': False, # keep sent emails to check content
+ 'attachment_ids': [(0, 0, a) for a in attachment_data],
+ 'email_to': '%s, %s, %s' % (email_to_1, email_to_2, email_to_3),
+ 'email_cc': email_cc_1,
+ 'partner_to': '%s, ${object.customer_id.id if object.customer_id else ""}' % self.partner_admin.id,
+ 'report_name': 'TestReport for ${object.name}', # test cursor forces html
+ 'report_template': self.test_report.id,
+ })
+ attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])])
+ self.assertEqual(len(attachs), 2)
+
+ # ensure initial data
+ self.assertEqual(self.test_records.user_id, self.env['res.users'])
+ self.assertEqual(self.test_records.message_partner_ids, self.env['res.partner'])
+
+ # launch composer in mass mode
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_records, add_web=True,
+ default_template_id=self.template.id)
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=False):
+ composer.send_mail()
+
+ new_partners = self.env['res.partner'].search([
+ ('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1])
+ ])
+ self.assertEqual(len(new_partners), 3)
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record')
+ self.assertEqual(len(self._mails), 10, 'Should have sent 5 emails per record')
+
+ # hack to use assertEmails: filtering on from/to only is not sufficient to distinguish emails
+ _mails_records = [
+ [mail for mail in self._mails if '%s-%s' % (record.id, record._name) in mail['message_id']]
+ for record in self.test_records
+ ]
+ _mails_record2 = [mail for mail in self._mails if '%s-%s' % (self.test_records[1].id, self.test_records._name) in mail['message_id']]
+
+ for record, _mails in zip(self.test_records, _mails_records):
+ # message copy is kept
+ message = record.message_ids[0]
+
+ # template is sent only to partners (email_to are transformed)
+ self._mails = _mails
+ self.assertMailMail(record.customer_id + new_partners + self.partner_admin,
+ 'sent',
+ mail_message=message,
+ author=self.partner_employee,
+ email_values={
+ 'body_content': 'TemplateBody %s' % record.name,
+ 'subject': 'TemplateSubject %s' % record.name,
+ # 'attachments': [
+ # ('00.txt', b'Att00', 'text/plain'),
+ # ('01.txt', b'Att01', 'text/plain'),
+ # ('report.test_mail.mail_test_ticket_test_template.html', b'My second attachment', 'text/plain')
+ # ]
+ }
+ )
+
+ @users('employee')
+ @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_wtpl_delete(self):
+ self.template.auto_delete = True
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_records, add_web=True,
+ default_template_id=self.template.id)
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ composer.send_mail()
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record')
+ self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record')
+ self.assertEqual(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records')
+
+ for record in self.test_records:
+ # message copy is kept
+ message = record.message_ids[0]
+
+ # template is sent directly using customer field
+ self.assertSentEmail(self.partner_employee, record.customer_id)
+
+ # message content
+ self.assertEqual(message.subject, 'TemplateSubject %s' % record.name)
+ self.assertEqual(message.body, '<p>TemplateBody %s</p>' % record.name)
+ self.assertEqual(message.author_id, self.user_employee.partner_id)
+ # post-related fields are void
+ self.assertEqual(message.subtype_id, self.env['mail.message.subtype'])
+ self.assertEqual(message.partner_ids, self.env['res.partner'])
+
+ @users('employee')
+ @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_wtpl_delete_notif(self):
+ self.template.auto_delete = True
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_records, add_web=True,
+ default_template_id=self.template.id,
+ default_auto_delete_message=True)
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ composer.send_mail()
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record')
+ self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record')
+ self.assertEqual(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records')
+
+ for record in self.test_records:
+ # message copy is unlinked
+ self.assertEqual(record.message_ids, self.env['mail.message'], 'Should have deleted mail.message records')
+
+ # template is sent directly using customer field
+ self.assertSentEmail(self.partner_employee, record.customer_id)
+
+ @users('employee')
+ @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_wtpl_recipients(self):
+ """ Test various combinations of recipients: active_domain, active_id,
+ active_ids, ... to ensure fallback behavior are working. """
+ # 1: active_domain
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_records, add_web=True,
+ default_template_id=self.template.id,
+ active_ids=[],
+ default_use_active_domain=True,
+ default_active_domain=[('id', 'in', self.test_records.ids)])
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ composer.send_mail()
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record')
+ self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record')
+
+ for record in self.test_records:
+ # template is sent directly using customer field
+ self.assertSentEmail(self.partner_employee, record.customer_id)
+
+ # 2: active_domain not taken into account if use_active_domain is False
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_records, add_web=True,
+ default_template_id=self.template.id,
+ default_use_active_domain=False,
+ default_active_domain=[('id', 'in', -1)])
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ composer.send_mail()
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record')
+ self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record')
+
+ # 3: fallback on active_id if not active_ids
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ self._get_web_context(self.test_records, add_web=True,
+ default_template_id=self.template.id,
+ active_ids=[])
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=False):
+ composer.send_mail()
+
+ # global outgoing
+ self.assertEqual(len(self._new_mails), 1, 'Should have created 1 mail.mail per record')
+ self.assertEqual(len(self._mails), 1, 'Should have sent 1 email per record')
+
+ # 3: void is void
+ composer_form = Form(self.env['mail.compose.message'].with_context(
+ default_model='mail.test.ticket',
+ default_template_id=self.template.id
+ ))
+ composer = composer_form.save()
+ with self.mock_mail_gateway(mail_unlink_sent=False), self.assertRaises(ValueError):
+ composer.send_mail()
diff --git a/addons/test_mail/tests/test_mail_followers.py b/addons/test_mail/tests/test_mail_followers.py
new file mode 100644
index 00000000..9246073a
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_followers.py
@@ -0,0 +1,498 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from psycopg2 import IntegrityError
+
+from odoo.addons.test_mail.tests.common import TestMailCommon
+from odoo.tests import tagged
+from odoo.tests import users
+from odoo.tools.misc import mute_logger
+
+
+@tagged('mail_followers')
+class BaseFollowersTest(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(BaseFollowersTest, cls).setUpClass()
+ cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+ cls._create_portal_user()
+ cls._create_channel_listener()
+
+ # allow employee to update partners
+ cls.user_employee.write({'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)]})
+
+ Subtype = cls.env['mail.message.subtype']
+ # global
+ cls.mt_al_def = Subtype.create({'name': 'mt_al_def', 'default': True, 'res_model': False})
+ cls.mt_al_nodef = Subtype.create({'name': 'mt_al_nodef', 'default': False, 'res_model': False})
+ # mail.test.simple
+ cls.mt_mg_def = Subtype.create({'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.test.simple'})
+ cls.mt_mg_nodef = Subtype.create({'name': 'mt_mg_nodef', 'default': False, 'res_model': 'mail.test.simple'})
+ cls.mt_mg_def_int = Subtype.create({'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.test.simple', 'internal': True})
+ # mail.test.container
+ cls.mt_cl_def = Subtype.create({'name': 'mt_cl_def', 'default': True, 'res_model': 'mail.test.container'})
+
+ cls.default_group_subtypes = Subtype.search([('default', '=', True), '|', ('res_model', '=', 'mail.test.simple'), ('res_model', '=', False)])
+ cls.default_group_subtypes_portal = Subtype.search([('internal', '=', False), ('default', '=', True), '|', ('res_model', '=', 'mail.test.simple'), ('res_model', '=', False)])
+
+ def test_field_message_is_follower(self):
+ test_record = self.test_record.with_user(self.user_employee)
+ followed_before = test_record.search([('message_is_follower', '=', True)])
+ self.assertFalse(test_record.message_is_follower)
+ test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id])
+ followed_after = test_record.search([('message_is_follower', '=', True)])
+ self.assertTrue(test_record.message_is_follower)
+ self.assertEqual(followed_before | test_record, followed_after)
+
+ def test_field_followers(self):
+ test_record = self.test_record.with_user(self.user_employee)
+ test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id, self.user_admin.partner_id.id], channel_ids=[self.channel_listen.id])
+ followers = self.env['mail.followers'].search([
+ ('res_model', '=', 'mail.test.simple'),
+ ('res_id', '=', test_record.id)])
+ self.assertEqual(followers, test_record.message_follower_ids)
+ self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id)
+ self.assertEqual(test_record.message_channel_ids, self.channel_listen)
+
+ def test_followers_subtypes_default(self):
+ test_record = self.test_record.with_user(self.user_employee)
+ test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id])
+ self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id)
+ follower = self.env['mail.followers'].search([
+ ('res_model', '=', 'mail.test.simple'),
+ ('res_id', '=', test_record.id),
+ ('partner_id', '=', self.user_employee.partner_id.id)])
+ self.assertEqual(follower, test_record.message_follower_ids)
+ self.assertEqual(follower.subtype_ids, self.default_group_subtypes)
+
+ def test_followers_subtypes_default_internal(self):
+ test_record = self.test_record.with_user(self.user_employee)
+ test_record.message_subscribe(partner_ids=[self.partner_portal.id])
+ self.assertEqual(test_record.message_partner_ids, self.partner_portal)
+ follower = self.env['mail.followers'].search([
+ ('res_model', '=', 'mail.test.simple'),
+ ('res_id', '=', test_record.id),
+ ('partner_id', '=', self.partner_portal.id)])
+ self.assertEqual(follower.subtype_ids, self.default_group_subtypes_portal)
+
+ def test_followers_subtypes_specified(self):
+ test_record = self.test_record.with_user(self.user_employee)
+ test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_mg_nodef.id])
+ self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id)
+ follower = self.env['mail.followers'].search([
+ ('res_model', '=', 'mail.test.simple'),
+ ('res_id', '=', test_record.id),
+ ('partner_id', '=', self.user_employee.partner_id.id)])
+ self.assertEqual(follower, test_record.message_follower_ids)
+ self.assertEqual(follower.subtype_ids, self.mt_mg_nodef)
+
+ def test_followers_multiple_subscription_force(self):
+ test_record = self.test_record.with_user(self.user_employee)
+
+ test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id])
+ self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id)
+ self.assertEqual(test_record.message_channel_ids, self.env['mail.channel'])
+ self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef)
+
+ test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id, self.mt_al_nodef.id])
+ self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id)
+ self.assertEqual(test_record.message_channel_ids, self.env['mail.channel'])
+ self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef)
+
+ def test_followers_multiple_subscription_noforce(self):
+ """ Calling message_subscribe without subtypes on an existing subscription should not do anything (default < existing) """
+ test_record = self.test_record.with_user(self.user_employee)
+
+ test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id, self.mt_al_nodef.id])
+ self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id)
+ self.assertEqual(test_record.message_channel_ids, self.env['mail.channel'])
+ self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef)
+
+ # set new subtypes with force=False, meaning no rewriting of the subscription is done -> result should not change
+ test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id])
+ self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id)
+ self.assertEqual(test_record.message_channel_ids, self.env['mail.channel'])
+ self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef)
+
+ def test_followers_multiple_subscription_update(self):
+ """ Calling message_subscribe with subtypes on an existing subscription should replace them (new > existing) """
+ test_record = self.test_record.with_user(self.user_employee)
+ test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_mg_def.id, self.mt_cl_def.id])
+ self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id)
+ follower = self.env['mail.followers'].search([
+ ('res_model', '=', 'mail.test.simple'),
+ ('res_id', '=', test_record.id),
+ ('partner_id', '=', self.user_employee.partner_id.id)])
+ self.assertEqual(follower, test_record.message_follower_ids)
+ self.assertEqual(follower.subtype_ids, self.mt_mg_def | self.mt_cl_def)
+
+ # remove one subtype `mt_mg_def` and set new subtype `mt_al_def`
+ test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_cl_def.id, self.mt_al_def.id])
+ self.assertEqual(follower.subtype_ids, self.mt_cl_def | self.mt_al_def)
+
+ def test_followers_no_DID(self):
+ """Test that a follower cannot suffer from dissociative identity disorder.
+ It cannot be both a partner and a channel.
+ """
+ with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'):
+ self.env['mail.followers'].create({
+ 'res_model': self.test_record._name,
+ 'res_id': self.test_record.id,
+ 'partner_id': self.user_employee.partner_id.id,
+ 'channel_id': self.channel_listen.id,
+ })
+
+ def test_followers_default_partner_context(self):
+ """Test that a follower partner_id is not taken from context
+ when channel id is also defined.
+ """
+ test_record = self.test_record.with_user(self.user_employee)
+ test_record.with_context(default_partner_id=1).message_subscribe(
+ partner_ids=[self.user_employee.partner_id.id, self.user_admin.partner_id.id],
+ channel_ids=[self.channel_listen.id]
+ )
+
+ @users('employee')
+ def test_followers_inactive(self):
+ """ Test standard API does not subscribe inactive partners """
+ customer = self.env['res.partner'].create({
+ 'name': 'Valid Lelitre',
+ 'email': 'valid.lelitre@agrolait.com',
+ 'country_id': self.env.ref('base.be').id,
+ 'mobile': '0456001122',
+ 'active': False,
+ })
+ document = self.env['mail.test.simple'].browse(self.test_record.id)
+ self.assertEqual(document.message_partner_ids, self.env['res.partner'])
+ document.message_subscribe(partner_ids=(self.partner_portal | customer).ids)
+ self.assertEqual(document.message_partner_ids, self.partner_portal)
+ self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal)
+
+ # works through low-level API
+ document._message_subscribe(partner_ids=(self.partner_portal | customer).ids)
+ self.assertEqual(document.message_partner_ids, self.partner_portal, 'No active test: customer not visible')
+ self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | customer)
+
+ @users('employee')
+ def test_followers_private_address(self):
+ """ Test standard API does not subscribe private addresses """
+ private_address = self.env['res.partner'].sudo().create({
+ 'name': 'Private Address',
+ 'type': 'private',
+ })
+ document = self.env['mail.test.simple'].browse(self.test_record.id)
+ document.message_subscribe(partner_ids=(self.partner_portal | private_address).ids)
+ self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal)
+
+ # works through low-level API
+ document._message_subscribe(partner_ids=(self.partner_portal | private_address).ids)
+ self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | private_address)
+
+
+@tagged('mail_followers')
+class AdvancedFollowersTest(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(AdvancedFollowersTest, cls).setUpClass()
+ cls._create_portal_user()
+
+ cls.test_track = cls.env['mail.test.track'].with_user(cls.user_employee).create({
+ 'name': 'Test',
+ })
+
+ Subtype = cls.env['mail.message.subtype']
+
+ # clean demo data to avoid interferences
+ Subtype.search([('res_model', 'in', ['mail.test.container', 'mail.test.track'])]).unlink()
+
+ # mail.test.track subtypes (aka: task records)
+ cls.sub_track_1 = Subtype.create({
+ 'name': 'Track (with child relation) 1', 'default': False,
+ 'res_model': 'mail.test.track'
+ })
+ cls.sub_track_2 = Subtype.create({
+ 'name': 'Track (with child relation) 2', 'default': False,
+ 'res_model': 'mail.test.track'
+ })
+ cls.sub_track_nodef = Subtype.create({
+ 'name': 'Generic Track subtype', 'default': False, 'internal': False,
+ 'res_model': 'mail.test.track'
+ })
+ cls.sub_track_def = Subtype.create({
+ 'name': 'Default track subtype', 'default': True, 'internal': False,
+ 'res_model': 'mail.test.track'
+ })
+
+ # mail.test.container subtypes (aka: project records)
+ cls.umb_nodef = Subtype.create({
+ 'name': 'Container NoDefault', 'default': False,
+ 'res_model': 'mail.test.container'
+ })
+ cls.umb_def = Subtype.create({
+ 'name': 'Container Default', 'default': True,
+ 'res_model': 'mail.test.container'
+ })
+ cls.umb_def_int = Subtype.create({
+ 'name': 'Container Default', 'default': True, 'internal': True,
+ 'res_model': 'mail.test.container'
+ })
+ # -> subtypes for auto subscription from container to sub records
+ cls.umb_autosub_def = Subtype.create({
+ 'name': 'Container AutoSub (default)', 'default': True, 'res_model': 'mail.test.container',
+ 'parent_id': cls.sub_track_1.id, 'relation_field': 'container_id'
+ })
+ cls.umb_autosub_nodef = Subtype.create({
+ 'name': 'Container AutoSub 2', 'default': False, 'res_model': 'mail.test.container',
+ 'parent_id': cls.sub_track_2.id, 'relation_field': 'container_id'
+ })
+
+ # generic subtypes
+ cls.sub_comment = cls.env.ref('mail.mt_comment')
+ cls.sub_generic_int_nodef = Subtype.create({
+ 'name': 'Generic internal subtype',
+ 'default': False,
+ 'internal': True,
+ })
+ cls.sub_generic_int_def = Subtype.create({
+ 'name': 'Generic internal subtype (default)',
+ 'default': True,
+ 'internal': True,
+ })
+
+ def test_auto_subscribe_create(self):
+ """ Creator of records are automatically added as followers """
+ self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id)
+
+ def test_auto_subscribe_inactive(self):
+ """ Test inactive are not added as followers in automated subscription """
+ self.test_track.user_id = False
+ self.user_admin.active = False
+ self.user_admin.flush()
+ self.partner_admin.active = False
+ self.partner_admin.flush()
+
+ self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='comment')
+ self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id)
+ self.assertEqual(self.test_track.message_follower_ids.partner_id, self.user_employee.partner_id)
+
+ self.test_track.write({'user_id': self.user_admin.id})
+ self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id)
+ self.assertEqual(self.test_track.message_follower_ids.partner_id, self.user_employee.partner_id)
+
+ def test_auto_subscribe_post(self):
+ """ People posting a message are automatically added as followers """
+ self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='comment')
+ self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id)
+
+ def test_auto_subscribe_post_email(self):
+ """ People posting an email are automatically added as followers """
+ self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='email')
+ self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id)
+
+ def test_auto_subscribe_not_on_notification(self):
+ """ People posting an automatic notification are not subscribed """
+ self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='notification')
+ self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id)
+
+ def test_auto_subscribe_responsible(self):
+ """ Responsibles are tracked and added as followers """
+ sub = self.env['mail.test.track'].with_user(self.user_employee).create({
+ 'name': 'Test',
+ 'user_id': self.user_admin.id,
+ })
+ self.assertEqual(sub.message_partner_ids, (self.user_employee.partner_id | self.user_admin.partner_id))
+
+ def test_auto_subscribe_defaults(self):
+ """ Test auto subscription based on an container record. This mimics
+ the behavior of addons like project and task where subscribing to
+ some project's subtypes automatically subscribe the follower to its tasks.
+
+ Functional rules applied here
+
+ * subscribing to an container subtype with parent_id / relation_field set
+ automatically create subscription with matching subtypes
+ * subscribing to a sub-record as creator applies default subtype values
+ * portal user should not have access to internal subtypes
+
+ Inactive partners should not be auto subscribed.
+ """
+ container = self.env['mail.test.container'].with_context(self._test_context).create({
+ 'name': 'Project-Like',
+ })
+
+ # have an inactive partner to check auto subscribe does not subscribe it
+ user_root = self.env.ref('base.user_root')
+ self.assertFalse(user_root.active)
+ self.assertFalse(user_root.partner_id.active)
+
+ container.message_subscribe(partner_ids=(self.partner_portal | user_root.partner_id).ids)
+ container.message_subscribe(partner_ids=self.partner_admin.ids, subtype_ids=(self.sub_comment | self.umb_autosub_nodef | self.sub_generic_int_nodef).ids)
+ self.assertEqual(container.message_partner_ids, self.partner_portal | self.partner_admin)
+ follower_por = container.message_follower_ids.filtered(lambda f: f.partner_id == self.partner_portal)
+ follower_adm = container.message_follower_ids.filtered(lambda f: f.partner_id == self.partner_admin)
+ self.assertEqual(
+ follower_por.subtype_ids,
+ self.sub_comment | self.umb_def | self.umb_autosub_def,
+ 'Subscribe: Default subtypes: comment (default generic) and two model-related defaults')
+ self.assertEqual(
+ follower_adm.subtype_ids,
+ self.sub_comment | self.umb_autosub_nodef | self.sub_generic_int_nodef,
+ 'Subscribe: Asked subtypes when subscribing')
+
+ sub1 = self.env['mail.test.track'].with_user(self.user_employee).create({
+ 'name': 'Task-Like Test',
+ 'container_id': container.id,
+ })
+
+ self.assertEqual(
+ sub1.message_partner_ids, self.partner_portal | self.partner_admin | self.user_employee.partner_id,
+ 'Followers: creator (employee) + auto subscribe from parent (portal)')
+ follower_por = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal)
+ follower_adm = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_admin)
+ follower_emp = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.user_employee.partner_id)
+ self.assertEqual(
+ follower_por.subtype_ids, self.sub_comment | self.sub_track_1,
+ 'AutoSubscribe: comment (generic checked), Track (with child relation) 1 as Umbrella AutoSub (default) was checked'
+ )
+ self.assertEqual(
+ follower_adm.subtype_ids, self.sub_comment | self.sub_track_2 | self.sub_generic_int_nodef,
+ 'AutoSubscribe: comment (generic checked), Track (with child relation) 2) as Umbrella AutoSub 2 was checked, Generic internal subtype (generic checked)'
+ )
+ self.assertEqual(
+ follower_emp.subtype_ids, self.sub_comment | self.sub_track_def | self.sub_generic_int_def,
+ 'AutoSubscribe: only default one as no subscription on parent'
+ )
+
+ # check portal generic subscribe
+ sub1.message_unsubscribe(partner_ids=self.partner_portal.ids)
+ sub1.message_subscribe(partner_ids=self.partner_portal.ids)
+ follower_por = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal)
+
+ self.assertEqual(
+ follower_por.subtype_ids, self.sub_comment | self.sub_track_def,
+ 'AutoSubscribe: only default one as no subscription on parent (no internal as portal)'
+ )
+
+ # check auto subscribe as creator + auto subscribe as parent follower takes both subtypes
+ container.message_subscribe(
+ partner_ids=self.user_employee.partner_id.ids,
+ subtype_ids=(self.sub_comment | self.sub_generic_int_nodef | self.umb_autosub_nodef).ids)
+ sub2 = self.env['mail.test.track'].with_user(self.user_employee).create({
+ 'name': 'Task-Like Test',
+ 'container_id': container.id,
+ })
+ follower_emp = sub2.message_follower_ids.filtered(lambda fol: fol.partner_id == self.user_employee.partner_id)
+ defaults = self.sub_comment | self.sub_track_def | self.sub_generic_int_def
+ parents = self.sub_generic_int_nodef | self.sub_track_2
+ self.assertEqual(
+ follower_emp.subtype_ids, defaults + parents,
+ 'AutoSubscribe: at create auto subscribe as creator + from parent take both subtypes'
+ )
+
+
+class AdvancedResponsibleNotifiedTest(TestMailCommon):
+ def setUp(self):
+ super(AdvancedResponsibleNotifiedTest, self).setUp()
+
+ # patch registry to simulate a ready environment so that _message_auto_subscribe_notify
+ # will be executed with the associated notification
+ old = self.env.registry.ready
+ self.env.registry.ready = True
+ self.addCleanup(setattr, self.env.registry, 'ready', old)
+
+ def test_auto_subscribe_notify_email(self):
+ """ Responsible is notified when assigned """
+ partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
+ notified_user = self.env['res.users'].create({
+ 'login': 'demo1',
+ 'partner_id': partner.id,
+ 'notification_type': 'email',
+ })
+
+ # TODO master: add a 'state' selection field on 'mail.test.track' with a 'done' value to have a complete test
+ # check that 'default_state' context does not collide with mail.mail default values
+ sub = self.env['mail.test.track'].with_user(self.user_employee).with_context({
+ 'default_state': 'done',
+ 'mail_notify_force_send': False
+ }).create({
+ 'name': 'Test',
+ 'user_id': notified_user.id,
+ })
+
+ self.assertEqual(sub.message_partner_ids, (self.user_employee.partner_id | notified_user.partner_id))
+ # fetch created "You have been assigned to 'Test'" mail.message
+ mail_message = self.env['mail.message'].search([
+ ('model', '=', 'mail.test.track'),
+ ('res_id', '=', sub.id),
+ ('partner_ids', 'in', partner.id),
+ ])
+ self.assertEqual(1, len(mail_message))
+
+ # verify that a mail.mail is attached to it with the correct state ('outgoing')
+ mail_notification = mail_message.notification_ids
+ self.assertEqual(1, len(mail_notification))
+ self.assertTrue(bool(mail_notification.mail_id))
+ self.assertEqual('outgoing', mail_notification.mail_id.state)
+
+
+@tagged('post_install', '-at_install')
+class DuplicateNotificationTest(TestMailCommon):
+ def test_no_duplicate_notification(self):
+ """
+ Check that we only create one mail.notification per partner
+
+ Post install because we need the registery to be ready to send notification
+ """
+ #Simulate case of 2 users that got their partner merged
+ common_partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
+ user_1 = self.env['res.users'].create({'login': 'demo1', 'partner_id': common_partner.id, 'notification_type': 'email'})
+ user_2 = self.env['res.users'].create({'login': 'demo2', 'partner_id': common_partner.id, 'notification_type': 'inbox'})
+
+ #Trigger auto subscribe notification
+ test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": user_2.id})
+ mail_message = self.env['mail.message'].search([
+ ('res_id', '=', test.id),
+ ('model', '=', 'mail.test.track'),
+ ('message_type', '=', 'user_notification')
+ ])
+ notif = self.env['mail.notification'].search([
+ ('mail_message_id', '=', mail_message.id),
+ ('res_partner_id', '=', common_partner.id)
+ ])
+ self.assertEqual(len(notif), 1)
+ self.assertEqual(notif.notification_type, 'email')
+
+ subtype = self.env.ref('mail.mt_comment')
+ res = self.env['mail.followers']._get_recipient_data(test, 'comment', subtype.id, pids=common_partner.ids)
+ partner_notif = [r for r in res if r[0] == common_partner.id]
+ self.assertEqual(len(partner_notif), 1)
+ self.assertEqual(partner_notif[0][5], 'email')
+
+@tagged('post_install', '-at_install')
+class UnlinkedNotificationTest(TestMailCommon):
+ def test_unlinked_notification(self):
+ """
+ Check that we unlink the created user_notification after unlinked the related document
+
+ Post install because we need the registery to be ready to send notification
+ """
+ common_partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
+ user_1 = self.env['res.users'].create({'login': 'demo1', 'partner_id': common_partner.id, 'notification_type': 'inbox'})
+
+ test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": user_1.id})
+ test_id = test.id
+ mail_message = self.env['mail.message'].search([
+ ('res_id', '=', test_id),
+ ('model', '=', 'mail.test.track'),
+ ('message_type', '=', 'user_notification')
+ ])
+ self.assertEqual(len(mail_message), 1)
+ test.unlink()
+ mail_message = self.env['mail.message'].search([
+ ('res_id', '=', test_id),
+ ('model', '=', 'mail.test.track'),
+ ('message_type', '=', 'user_notification')
+ ])
+ self.assertEqual(len(mail_message), 0)
diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py
new file mode 100644
index 00000000..ea0ef7c6
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_gateway.py
@@ -0,0 +1,1273 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import socket
+
+from unittest.mock import DEFAULT
+from unittest.mock import patch
+
+from odoo import exceptions
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.addons.test_mail.data import test_mail_data
+from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE
+from odoo.addons.test_mail.models.test_mail_models import MailTestGateway
+from odoo.addons.test_mail.tests.common import TestMailCommon
+from odoo.tests import tagged
+from odoo.tests.common import users
+from odoo.tools import email_split_and_format, formataddr, mute_logger
+
+
+@tagged('mail_gateway')
+class TestEmailParsing(TestMailCommon):
+
+ def test_message_parse_body(self):
+ # test pure plaintext
+ plaintext = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>')
+ res = self.env['mail.thread'].message_parse(self.from_string(plaintext))
+ self.assertIn('Please call me as soon as possible this afternoon!', res['body'])
+
+ # test multipart / text and html -> html has priority
+ multipart = self.format(MAIL_TEMPLATE, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>')
+ res = self.env['mail.thread'].message_parse(self.from_string(multipart))
+ self.assertIn('<p>Please call me as soon as possible this afternoon!</p>', res['body'])
+
+ # test multipart / mixed
+ res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_MULTIPART_MIXED))
+ self.assertNotIn(
+ 'Should create a multipart/mixed: from gmail, *bold*, with attachment', res['body'],
+ 'message_parse: text version should not be in body after parsing multipart/mixed')
+ self.assertIn(
+ '<div dir="ltr">Should create a multipart/mixed: from gmail, <b>bold</b>, with attachment.<br clear="all"><div><br></div>', res['body'],
+ 'message_parse: html version should be in body after parsing multipart/mixed')
+
+ res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_MULTIPART_MIXED_TWO))
+ self.assertNotIn('First and second part', res['body'],
+ 'message_parse: text version should not be in body after parsing multipart/mixed')
+ self.assertIn('First part', res['body'],
+ 'message_parse: first part of the html version should be in body after parsing multipart/mixed')
+ self.assertIn('Second part', res['body'],
+ 'message_parse: second part of the html version should be in body after parsing multipart/mixed')
+
+ res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_SINGLE_BINARY))
+ self.assertEqual(res['body'], '')
+ self.assertEqual(res['attachments'][0][0], 'thetruth.pdf')
+
+ res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_MULTIPART_WEIRD_FILENAME))
+ self.assertEqual(res['attachments'][0][0], '62_@;,][)=.(ÇÀÉ.txt')
+
+ def test_message_parse_eml(self):
+ # Test that the parsing of mail with embedded emails as eml(msg) which generates empty attachments, can be processed.
+ mail = self.format(test_mail_data.MAIL_EML_ATTACHMENT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>', to='generic@test.com')
+ self.env['mail.thread'].message_parse(self.from_string(mail))
+
+ def test_message_parse_eml_bounce_headers(self):
+ # Test Text/RFC822-Headers MIME content-type
+ msg_id = '<861878175823148.1577183525.736005783081055-openerp-19177-account.invoice@mycompany.example.com>'
+ mail = self.format(
+ test_mail_data.MAIL_EML_ATTACHMENT_BOUNCE_HEADERS,
+ email_from='MAILER-DAEMON@example.com (Mail Delivery System)',
+ to='test_bounce+82240-account.invoice-19177@mycompany.example.com',
+ # msg_id goes to the attachment's Message-Id header
+ msg_id=msg_id,
+ )
+ res = self.env['mail.thread'].message_parse(self.from_string(mail))
+
+ self.assertEqual(res['bounced_msg_id'], [msg_id], "Message-Id is not extracted from Text/RFC822-Headers attachment")
+
+ def test_message_parse_plaintext(self):
+ """ Incoming email in plaintext should be stored as html """
+ mail = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>', to='generic@test.com')
+ res = self.env['mail.thread'].message_parse(self.from_string(mail))
+ self.assertIn('<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>', res['body'])
+
+ def test_message_parse_xhtml(self):
+ # Test that the parsing of XHTML mails does not fail
+ self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_XHTML))
+
+
+@tagged('mail_gateway')
+class TestMailAlias(TestMailCommon):
+
+ @users('employee')
+ def test_alias_creation(self):
+ record = self.env['mail.test.container'].create({
+ 'name': 'Test Record',
+ 'alias_name': 'alias.test',
+ 'alias_contact': 'followers',
+ })
+ self.assertEqual(record.alias_id.alias_model_id, self.env['ir.model']._get('mail.test.container'))
+ self.assertEqual(record.alias_id.alias_force_thread_id, record.id)
+ self.assertEqual(record.alias_id.alias_parent_model_id, self.env['ir.model']._get('mail.test.container'))
+ self.assertEqual(record.alias_id.alias_parent_thread_id, record.id)
+ self.assertEqual(record.alias_id.alias_name, 'alias.test')
+ self.assertEqual(record.alias_id.alias_contact, 'followers')
+
+ record.write({
+ 'alias_name': 'better.alias.test',
+ 'alias_defaults': "{'default_name': 'defaults'}"
+ })
+ self.assertEqual(record.alias_id.alias_name, 'better.alias.test')
+ self.assertEqual(record.alias_id.alias_defaults, "{'default_name': 'defaults'}")
+
+ with self.assertRaises(exceptions.AccessError):
+ record.write({
+ 'alias_force_thread_id': 0,
+ })
+
+ with self.assertRaises(exceptions.AccessError):
+ record.write({
+ 'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id,
+ })
+
+ with self.assertRaises(exceptions.ValidationError):
+ record.write({'alias_defaults': "{'custom_field': brokendict"})
+
+ def test_alias_setup(self):
+ alias = self.env['mail.alias'].create({
+ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
+ 'alias_name': 'b4r+_#_R3wl$$',
+ })
+ self.assertEqual(alias.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens')
+
+ with self.assertRaises(exceptions.ValidationError):
+ alias.write({'alias_defaults': "{'custom_field': brokendict"})
+
+ def test_alias_name_unique(self):
+ alias_model_id = self.env['ir.model']._get('mail.test.gateway').id
+ catchall_alias = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.alias')
+ bounce_alias = self.env['ir.config_parameter'].sudo().get_param('mail.bounce.alias')
+
+ # test you cannot create aliases matching bounce / catchall
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ self.env['mail.alias'].create({'alias_model_id': alias_model_id, 'alias_name': catchall_alias})
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ self.env['mail.alias'].create({'alias_model_id': alias_model_id, 'alias_name': bounce_alias})
+
+ new_mail_alias = self.env['mail.alias'].create({
+ 'alias_model_id': alias_model_id,
+ 'alias_name': 'unused.test.alias'
+ })
+
+ # test that re-using catchall and bounce alias raises UserError
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ new_mail_alias.write({
+ 'alias_name': catchall_alias
+ })
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ new_mail_alias.write({
+ 'alias_name': bounce_alias
+ })
+
+ new_mail_alias.write({'alias_name': 'another.unused.test.alias'})
+
+ # test that duplicating an alias should have blank name
+ copy_new_mail_alias = new_mail_alias.copy()
+ self.assertFalse(copy_new_mail_alias.alias_name)
+
+ # cannot set catchall / bounce to used alias
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ self.env['ir.config_parameter'].sudo().set_param('mail.catchall.alias', new_mail_alias.alias_name)
+ with self.assertRaises(exceptions.UserError), self.cr.savepoint():
+ self.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', new_mail_alias.alias_name)
+
+ def test_alias_mixin_copy(self):
+ user_demo = self.env.ref('base.user_demo')
+ self.assertFalse(user_demo.has_group('base.group_system'), 'Demo user is not supposed to have Administrator access')
+ self._test_alias_mixin_copy(user_demo, 'alias.test1', False)
+ self._test_alias_mixin_copy(user_demo, 'alias.test2', '<p>What Is Dead May Never Die</p>')
+
+ def _test_alias_mixin_copy(self, user, alias_name, alias_bounced_content):
+ record = self.env['mail.test.container'].with_user(user).with_context(lang='en_US').create({
+ 'name': 'Test Record',
+ 'alias_name': alias_name,
+ 'alias_contact': 'followers',
+ 'alias_bounced_content': alias_bounced_content,
+ })
+ self.assertEqual(record.alias_bounced_content, alias_bounced_content)
+ record_copy = record.copy()
+ self.assertEqual(record_copy.alias_bounced_content, alias_bounced_content)
+
+
+@tagged('mail_gateway')
+class TestMailgateway(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailgateway, cls).setUpClass()
+ cls.test_model = cls.env['ir.model']._get('mail.test.gateway')
+ cls.email_from = '"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>'
+
+ cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({
+ 'name': 'Test',
+ 'email_from': 'ignasse@example.com',
+ }).with_context({})
+
+ cls.partner_1 = cls.env['res.partner'].with_context(cls._test_context).create({
+ 'name': 'Valid Lelitre',
+ 'email': 'valid.lelitre@agrolait.com',
+ })
+ # groups@.. will cause the creation of new mail.test.gateway
+ cls.alias = cls.env['mail.alias'].create({
+ 'alias_name': 'groups',
+ 'alias_user_id': False,
+ 'alias_model_id': cls.test_model.id,
+ 'alias_contact': 'everyone'})
+
+ # Set a first message on public group to test update and hierarchy
+ cls.fake_email = cls.env['mail.message'].create({
+ 'model': 'mail.test.gateway',
+ 'res_id': cls.test_record.id,
+ 'subject': 'Public Discussion',
+ 'message_type': 'email',
+ 'subtype_id': cls.env.ref('mail.mt_comment').id,
+ 'author_id': cls.partner_1.id,
+ 'message_id': '<123456-openerp-%s-mail.test.gateway@%s>' % (cls.test_record.id, socket.gethostname()),
+ })
+
+ cls._init_mail_gateway()
+
+ # --------------------------------------------------
+ # Base low-level tests
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_alias_basic(self):
+ """ Test details of created message going through mailgateway """
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Specific')
+
+ # Test: one group created by mailgateway administrator as user_id is not set
+ self.assertEqual(len(record), 1, 'message_process: a new mail.test should have been created')
+ res = record.get_metadata()[0].get('create_uid') or [None]
+ self.assertEqual(res[0], self.env.uid)
+
+ # Test: one message that is the incoming email
+ self.assertEqual(len(record.message_ids), 1)
+ msg = record.message_ids[0]
+ self.assertEqual(msg.subject, 'Specific')
+ self.assertIn('Please call me as soon as possible this afternoon!', msg.body)
+ self.assertEqual(msg.message_type, 'email')
+ self.assertEqual(msg.subtype_id, self.env.ref('mail.mt_comment'))
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_cid(self):
+ record = self.format_and_process(test_mail_data.MAIL_MULTIPART_IMAGE, self.email_from, 'groups@test.com')
+ message = record.message_ids[0]
+ for attachment in message.attachment_ids:
+ self.assertIn('/web/image/%s' % attachment.id, message.body)
+ self.assertEqual(
+ set(message.attachment_ids.mapped('name')),
+ set(['rosaçée.gif', 'verte!µ.gif', 'orangée.gif']))
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_followers(self):
+ """ Incoming email: recognized author not archived and not odoobot: added as follower """
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com')
+
+ self.assertEqual(record.message_ids[0].author_id, self.partner_1,
+ 'message_process: recognized email -> author_id')
+ self.assertEqual(record.message_ids[0].email_from, self.partner_1.email_formatted)
+ self.assertEqual(record.message_follower_ids.partner_id, self.partner_1,
+ 'message_process: recognized email -> added as follower')
+ self.assertEqual(record.message_partner_ids, self.partner_1,
+ 'message_process: recognized email -> added as follower')
+
+ # just an email -> no follower
+ with self.mock_mail_gateway():
+ record2 = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'groups@test.com',
+ subject='Another Email')
+
+ self.assertEqual(record2.message_ids[0].author_id, self.env['res.partner'])
+ self.assertEqual(record2.message_ids[0].email_from, self.email_from)
+ self.assertEqual(record2.message_follower_ids.partner_id, self.env['res.partner'],
+ 'message_process: unrecognized email -> no follower')
+ self.assertEqual(record2.message_partner_ids, self.env['res.partner'],
+ 'message_process: unrecognized email -> no follower')
+
+ # archived partner -> no follower
+ self.partner_1.active = False
+ self.partner_1.flush()
+ with self.mock_mail_gateway():
+ record3 = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com',
+ subject='Yet Another Email')
+
+ self.assertEqual(record3.message_ids[0].author_id, self.env['res.partner'])
+ self.assertEqual(record3.message_ids[0].email_from, self.partner_1.email_formatted)
+ self.assertEqual(record3.message_follower_ids.partner_id, self.env['res.partner'],
+ 'message_process: unrecognized email -> no follower')
+ self.assertEqual(record3.message_partner_ids, self.env['res.partner'],
+ 'message_process: unrecognized email -> no follower')
+
+
+ # partner_root -> never again
+ odoobot = self.env.ref('base.partner_root')
+ odoobot.active = True
+ odoobot.email = 'odoobot@example.com'
+ with self.mock_mail_gateway():
+ record4 = self.format_and_process(
+ MAIL_TEMPLATE, odoobot.email_formatted, 'groups@test.com',
+ subject='Odoobot Automatic Answer')
+
+ self.assertEqual(record4.message_ids[0].author_id, odoobot)
+ self.assertEqual(record4.message_ids[0].email_from, odoobot.email_formatted)
+ self.assertEqual(record4.message_follower_ids.partner_id, self.env['res.partner'],
+ 'message_process: unrecognized email -> no follower')
+ self.assertEqual(record4.message_partner_ids, self.env['res.partner'],
+ 'message_process: unrecognized email -> no follower')
+
+ # --------------------------------------------------
+ # Author recognition
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_email_email_from(self):
+ """ Incoming email: not recognized author: email_from, no author_id, no followers """
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com')
+ self.assertFalse(record.message_ids[0].author_id, 'message_process: unrecognized email -> no author_id')
+ self.assertEqual(record.message_ids[0].email_from, self.email_from)
+ self.assertEqual(len(record.message_partner_ids), 0,
+ 'message_process: newly create group should not have any follower')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_email_author(self):
+ """ Incoming email: recognized author: email_from, author_id, added as follower """
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com', subject='Test1')
+
+ self.assertEqual(record.message_ids[0].author_id, self.partner_1,
+ 'message_process: recognized email -> author_id')
+ self.assertEqual(record.message_ids[0].email_from, self.partner_1.email_formatted)
+ self.assertNotSentEmail() # No notification / bounce should be sent
+
+ # Email recognized if partner has a formatted email
+ self.partner_1.write({'email': '"Valid Lelitre" <%s>' % self.partner_1.email})
+ record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email, 'groups@test.com', subject='Test2')
+
+ self.assertEqual(record.message_ids[0].author_id, self.partner_1,
+ 'message_process: recognized email -> author_id')
+ self.assertEqual(record.message_ids[0].email_from, self.partner_1.email)
+ self.assertNotSentEmail() # No notification / bounce should be sent
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_email_partner_find(self):
+ """ Finding the partner based on email, based on partner / user / follower """
+ self.alias.write({'alias_force_thread_id': self.test_record.id})
+ from_1 = self.env['res.partner'].create({'name': 'Brice Denisse', 'email': 'from.test@example.com'})
+
+ self.format_and_process(MAIL_TEMPLATE, from_1.email_formatted, 'groups@test.com')
+ self.assertEqual(self.test_record.message_ids[0].author_id, from_1)
+ self.test_record.message_unsubscribe([from_1.id])
+
+ from_2 = mail_new_test_user(self.env, login='B', groups='base.group_user', name='User Denisse', email='from.test@example.com')
+
+ self.format_and_process(MAIL_TEMPLATE, from_1.email_formatted, 'groups@test.com')
+ self.assertEqual(self.test_record.message_ids[0].author_id, from_2.partner_id)
+ self.test_record.message_unsubscribe([from_2.partner_id.id])
+
+ from_3 = self.env['res.partner'].create({'name': 'FOllower Denisse', 'email': 'from.test@example.com'})
+ self.test_record.message_subscribe([from_3.id])
+
+ self.format_and_process(MAIL_TEMPLATE, from_1.email_formatted, 'groups@test.com')
+ self.assertEqual(self.test_record.message_ids[0].author_id, from_3)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_email_author_exclude_alias(self):
+ """ Do not set alias as author to avoid including aliases in discussions """
+ from_1 = self.env['res.partner'].create({'name': 'Brice Denisse', 'email': 'from.test@test.com'})
+ self.env['mail.alias'].create({
+ 'alias_name': 'from.test',
+ 'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id
+ })
+
+ record = self.format_and_process(MAIL_TEMPLATE, from_1.email_formatted, 'groups@test.com')
+ self.assertFalse(record.message_ids[0].author_id)
+ self.assertEqual(record.message_ids[0].email_from, from_1.email_formatted)
+
+ # --------------------------------------------------
+ # Alias configuration
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_message_process_alias_config_bounced_content(self):
+ """ Custom bounced message for the alias => Received this custom message """
+ self.alias.write({
+ 'alias_contact': 'partners',
+ 'alias_bounced_content': '<p>What Is Dead May Never Die</p>'
+ })
+
+ # Test: custom bounced content
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce')
+ self.assertFalse(record, 'message_process: should have bounced')
+ self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], body_content='<p>What Is Dead May Never Die</p>')
+
+ self.alias.write({
+ 'alias_contact': 'partners',
+ 'alias_bounced_content': '<p></br></p>'
+ })
+
+ # Test: with "empty" bounced content (simulate view, putting always '<p></br></p>' in html field)
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce')
+ self.assertFalse(record, 'message_process: should have bounced')
+ # Check if default (hardcoded) value is in the mail content
+ self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], body_content='The following email sent to')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ def test_message_process_alias_config_bounced_to(self):
+ """ Check bounce message contains the bouncing alias, not a generic "to" """
+ self.alias.write({'alias_contact': 'partners'})
+ bounce_message_with_alias = "The following email sent to %s cannot be accepted because this is a private email address." % self.alias.display_name.lower()
+
+ # Bounce is To
+ with self.mock_mail_gateway():
+ self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'groups@example.com',
+ cc='other@gmail.com', subject='Should Bounce')
+ self.assertIn(bounce_message_with_alias, self._mails[0].get('body'))
+
+ # Bounce is CC
+ with self.mock_mail_gateway():
+ self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'other@gmail.com',
+ cc='groups@example.com', subject='Should Bounce')
+ self.assertIn(bounce_message_with_alias, self._mails[0].get('body'))
+
+ # Bounce is part of To
+ with self.mock_mail_gateway():
+ self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'other@gmail.com, groups@example.com',
+ subject='Should Bounce')
+ self.assertIn(bounce_message_with_alias, self._mails[0].get('body'))
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_alias_defaults(self):
+ """ Test alias defaults and inner values """
+ self.alias.write({
+ 'alias_user_id': self.user_employee.id,
+ 'alias_defaults': "{'custom_field': 'defaults_custom'}"
+ })
+
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Specific')
+ self.assertEqual(len(record), 1)
+ res = record.get_metadata()[0].get('create_uid') or [None]
+ self.assertEqual(res[0], self.user_employee.id)
+ self.assertEqual(record.name, 'Specific')
+ self.assertEqual(record.custom_field, 'defaults_custom')
+
+ self.alias.write({'alias_defaults': '""'})
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Specific2')
+ self.assertEqual(len(record), 1)
+ res = record.get_metadata()[0].get('create_uid') or [None]
+ self.assertEqual(res[0], self.user_employee.id)
+ self.assertEqual(record.name, 'Specific2')
+ self.assertFalse(record.custom_field)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_alias_user_id(self):
+ """ Test alias ownership """
+ self.alias.write({'alias_user_id': self.user_employee.id})
+
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com')
+ self.assertEqual(len(record), 1)
+ res = record.get_metadata()[0].get('create_uid') or [None]
+ self.assertEqual(res[0], self.user_employee.id)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_alias_everyone(self):
+ """ Incoming email: everyone: new record + message_new """
+ self.alias.write({'alias_contact': 'everyone'})
+
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Specific')
+ self.assertEqual(len(record), 1)
+ self.assertEqual(len(record.message_ids), 1)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_message_process_alias_partners_bounce(self):
+ """ Incoming email from an unknown partner on a Partners only alias -> bounce + test bounce email """
+ self.alias.write({'alias_contact': 'partners'})
+
+ # Test: no group created, email bounced
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce')
+ self.assertFalse(record)
+ self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], subject='Re: Should Bounce')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_message_process_alias_followers_bounce(self):
+ """ Incoming email from unknown partner / not follower partner on a Followers only alias -> bounce """
+ self.alias.write({
+ 'alias_contact': 'followers',
+ 'alias_parent_model_id': self.env['ir.model']._get('mail.test.gateway').id,
+ 'alias_parent_thread_id': self.test_record.id,
+ })
+
+ # Test: unknown on followers alias -> bounce
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce')
+ self.assertFalse(record, 'message_process: should have bounced')
+ self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], subject='Re: Should Bounce')
+
+ # Test: partner on followers alias -> bounce
+ self._init_mail_mock()
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com', subject='Should Bounce')
+ self.assertFalse(record, 'message_process: should have bounced')
+ self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], subject='Re: Should Bounce')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_alias_partner(self):
+ """ Incoming email from a known partner on a Partners alias -> ok (+ test on alias.user_id) """
+ self.alias.write({'alias_contact': 'partners'})
+ record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com')
+
+ # Test: one group created by alias user
+ self.assertEqual(len(record), 1)
+ self.assertEqual(len(record.message_ids), 1)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_alias_followers(self):
+ """ Incoming email from a parent document follower on a Followers only alias -> ok """
+ self.alias.write({
+ 'alias_contact': 'followers',
+ 'alias_parent_model_id': self.env['ir.model']._get('mail.test.gateway').id,
+ 'alias_parent_thread_id': self.test_record.id,
+ })
+ self.test_record.message_subscribe(partner_ids=[self.partner_1.id])
+ record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com')
+
+ # Test: one group created by Raoul (or Sylvie maybe, if we implement it)
+ self.assertEqual(len(record), 1)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_message_process_alias_update(self):
+ """ Incoming email update discussion + notification email """
+ self.alias.write({'alias_force_thread_id': self.test_record.id})
+
+ self.test_record.message_subscribe(partner_ids=[self.partner_1.id])
+ with self.mock_mail_gateway():
+ record = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'groups@test.com>',
+ msg_id='<1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>', subject='Re: cats')
+
+ # Test: no new group + new message
+ self.assertFalse(record, 'message_process: alias update should not create new records')
+ self.assertEqual(len(self.test_record.message_ids), 2)
+ # Test: sent emails: 1 (Sylvie copy of the incoming email)
+ self.assertSentEmail(self.email_from, [self.partner_1], subject='Re: cats')
+
+ # --------------------------------------------------
+ # Creator recognition
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_create_uid_crash(self):
+ def _employee_crash(*args, **kwargs):
+ """ If employee is test employee, consider he has no access on document """
+ recordset = args[0]
+ if recordset.env.uid == self.user_employee.id and not recordset.env.su:
+ if kwargs.get('raise_exception', True):
+ raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
+ return False
+ return DEFAULT
+
+ with patch.object(MailTestGateway, 'check_access_rights', autospec=True, side_effect=_employee_crash):
+ record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_formatted, 'groups@test.com', subject='NoEmployeeAllowed')
+ self.assertEqual(record.create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].subject, 'NoEmployeeAllowed')
+ self.assertEqual(record.message_ids[0].create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_create_uid_email(self):
+ record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_formatted, 'groups@test.com', subject='Email Found')
+ self.assertEqual(record.create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].subject, 'Email Found')
+ self.assertEqual(record.message_ids[0].create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id)
+
+ record = self.format_and_process(MAIL_TEMPLATE, 'Another name <%s>' % self.user_employee.email, 'groups@test.com', subject='Email OtherName')
+ self.assertEqual(record.create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].subject, 'Email OtherName')
+ self.assertEqual(record.message_ids[0].create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id)
+
+ record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_normalized, 'groups@test.com', subject='Email SimpleEmail')
+ self.assertEqual(record.create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].subject, 'Email SimpleEmail')
+ self.assertEqual(record.message_ids[0].create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_create_uid_email_follower(self):
+ self.alias.write({
+ 'alias_parent_model_id': self.test_model.id,
+ 'alias_parent_thread_id': self.test_record.id,
+ })
+ follower_user = mail_new_test_user(self.env, login='better', groups='base.group_user', name='Ernest Follower', email=self.user_employee.email)
+ self.test_record.message_subscribe(follower_user.partner_id.ids)
+
+ record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_formatted, 'groups@test.com', subject='FollowerWinner')
+ self.assertEqual(record.create_uid, follower_user)
+ self.assertEqual(record.message_ids[0].subject, 'FollowerWinner')
+ self.assertEqual(record.message_ids[0].create_uid, follower_user)
+ self.assertEqual(record.message_ids[0].author_id, follower_user.partner_id)
+
+ # name order win
+ self.test_record.message_unsubscribe(follower_user.partner_id.ids)
+ self.test_record.flush()
+ record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_formatted, 'groups@test.com', subject='FirstFoundWinner')
+ self.assertEqual(record.create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].subject, 'FirstFoundWinner')
+ self.assertEqual(record.message_ids[0].create_uid, self.user_employee)
+ self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id)
+
+ # --------------------------------------------------
+ # Alias routing management
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_alias_no_domain(self):
+ """ Incoming email: write to alias even if no domain set: considered as valid alias """
+ self.env['ir.config_parameter'].set_param('mail.catchall.domain', '')
+
+ new_record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@another.domain.com', subject='Test Subject')
+ # Test: one group created
+ self.assertEqual(len(new_record), 1, 'message_process: a new mail.test.simple should have been created')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_alias_forward_bypass_reply_first(self):
+ """ Incoming email: write to two "new thread" alias, one as a reply, one being another model -> consider as a forward """
+ self.assertEqual(len(self.test_record.message_ids), 1)
+
+ # test@.. will cause the creation of new mail.test
+ new_alias_2 = self.env['mail.alias'].create({
+ 'alias_name': 'test',
+ 'alias_user_id': False,
+ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
+ 'alias_contact': 'everyone',
+ })
+ new_rec = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s, %s@%s' % (new_alias_2.alias_name, self.alias_domain, self.alias.alias_name, self.alias_domain),
+ subject='Test Subject',
+ extra='In-Reply-To:\r\n\t%s\n' % self.fake_email.message_id,
+ target_model=new_alias_2.alias_model_id.model
+ )
+ # Forward created a new record in mail.test
+ self.assertEqual(len(new_rec), 1, 'message_process: a new mail.test should have been created')
+ self.assertEqual(new_rec._name, new_alias_2.alias_model_id.model)
+ # No new post on test_record, no new record in mail.test.simple either
+ self.assertEqual(len(self.test_record.message_ids), 1, 'message_process: should not post on replied record as forward should bypass it')
+ new_simple = self.env['mail.test.simple'].search([('name', '=', 'Test Subject')])
+ self.assertEqual(len(new_simple), 0, 'message_process: a new mail.test should not have been created')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_alias_forward_bypass_reply_second(self):
+ """ Incoming email: write to two "new thread" alias, one as a reply, one being another model -> consider as a forward """
+ self.assertEqual(len(self.test_record.message_ids), 1)
+
+ # test@.. will cause the creation of new mail.test
+ new_alias_2 = self.env['mail.alias'].create({
+ 'alias_name': 'test',
+ 'alias_user_id': False,
+ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
+ 'alias_contact': 'everyone',
+ })
+ new_rec = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s, %s@%s' % (self.alias.alias_name, self.alias_domain, new_alias_2.alias_name, self.alias_domain),
+ subject='Test Subject',
+ extra='In-Reply-To:\r\n\t%s\n' % self.fake_email.message_id,
+ target_model=new_alias_2.alias_model_id.model
+ )
+ # Forward created a new record in mail.test
+ self.assertEqual(len(new_rec), 1, 'message_process: a new mail.test should have been created')
+ self.assertEqual(new_rec._name, new_alias_2.alias_model_id.model)
+ # No new post on test_record, no new record in mail.test.simple either
+ self.assertEqual(len(self.test_record.message_ids), 1, 'message_process: should not post on replied record as forward should bypass it')
+ new_simple = self.env['mail.test.simple'].search([('name', '=', 'Test Subject')])
+ self.assertEqual(len(new_simple), 0, 'message_process: a new mail.test should not have been created')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_alias_forward_bypass_update_alias(self):
+ """ Incoming email: write to one "update", one "new thread" alias, one as a reply, one being another model -> consider as a forward """
+ self.assertEqual(len(self.test_record.message_ids), 1)
+ self.alias.write({
+ 'alias_force_thread_id': self.test_record.id,
+ })
+
+ # test@.. will cause the creation of new mail.test
+ new_alias_2 = self.env['mail.alias'].create({
+ 'alias_name': 'test',
+ 'alias_user_id': False,
+ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
+ 'alias_contact': 'everyone',
+ })
+ new_rec = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s, %s@%s' % (new_alias_2.alias_name, self.alias_domain, self.alias.alias_name, self.alias_domain),
+ subject='Test Subject',
+ extra='In-Reply-To:\r\n\t%s\n' % self.fake_email.message_id,
+ target_model=new_alias_2.alias_model_id.model
+ )
+ # Forward created a new record in mail.test
+ self.assertEqual(len(new_rec), 1, 'message_process: a new mail.test should have been created')
+ self.assertEqual(new_rec._name, new_alias_2.alias_model_id.model)
+ # No new post on test_record, no new record in mail.test.simple either
+ self.assertEqual(len(self.test_record.message_ids), 1, 'message_process: should not post on replied record as forward should bypass it')
+ # No new record on first alias model
+ new_simple = self.env['mail.test.gateway'].search([('name', '=', 'Test Subject')])
+ self.assertEqual(len(new_simple), 0, 'message_process: a new mail.test should not have been created')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_alias_multiple_new(self):
+ """ Incoming email: write to two aliases creating records: both should be activated """
+ # test@.. will cause the creation of new mail.test
+ new_alias_2 = self.env['mail.alias'].create({
+ 'alias_name': 'test',
+ 'alias_user_id': False,
+ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
+ 'alias_contact': 'everyone',
+ })
+ new_rec = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s, %s@%s' % (self.alias.alias_name, self.alias_domain, new_alias_2.alias_name, self.alias_domain),
+ subject='Test Subject',
+ target_model=new_alias_2.alias_model_id.model
+ )
+ # New record in both mail.test (new_alias_2) and mail.test.simple (self.alias)
+ self.assertEqual(len(new_rec), 1, 'message_process: a new mail.test should have been created')
+ self.assertEqual(new_rec._name, new_alias_2.alias_model_id.model)
+ new_simple = self.env['mail.test.gateway'].search([('name', '=', 'Test Subject')])
+ self.assertEqual(len(new_simple), 1, 'message_process: a new mail.test should have been created')
+
+ # --------------------------------------------------
+ # Email Management
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_bounce(self):
+ """Incoming email: bounce using bounce alias: no record creation """
+ with self.mock_mail_gateway():
+ new_recs = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s+%s-%s-%s@%s' % (
+ self.alias_bounce, self.fake_email.id,
+ self.fake_email.model, self.fake_email.res_id,
+ self.alias_domain
+ ),
+ subject='Should bounce',
+ )
+ self.assertFalse(new_recs)
+ self.assertNotSentEmail()
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_bounce_if_static_but_still_has_plus_addressing(self):
+ """Incoming email: bounce using bounce alias without plus addressing: keep old behavior."""
+ self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True)
+ with self.mock_mail_gateway():
+ new_recs = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s+%s-%s-%s@%s' % (
+ self.alias_bounce, self.fake_email.id,
+ self.fake_email.model, self.fake_email.res_id,
+ self.alias_domain
+ ),
+ subject='Should bounce',
+ )
+ self.assertFalse(new_recs)
+ self.assertEqual(len(self._mails), 0, 'message_process: incoming bounce produces no mails')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_bounce_if_static_without_plus_addressing(self):
+ """Incoming email: bounce using bounce alias without plus addressing: bounce it."""
+ self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True)
+ with self.mock_mail_gateway():
+ new_recs = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s' % (self.alias_bounce, self.alias_domain),
+ subject='Should bounce',
+ )
+ self.assertFalse(new_recs)
+ self.assertEqual(len(self._mails), 0, 'message_process: incoming bounce produces no mails')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_no_bounce_if_not_static_without_plus_addressing(self):
+ """Incoming email: bounce using bounce alias without plus addressing: raise as
+ considering as a direct write to bounce alias -> invalid """
+ self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', False)
+ with self.assertRaises(ValueError):
+ self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s' % (self.alias_bounce, self.alias_domain),
+ subject="Should fail because it is not a bounce and there's no alias",
+ )
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_bounce_other_recipients(self):
+ """Incoming email: bounce processing: bounce should be computed even if not first recipient """
+ with self.mock_mail_gateway():
+ new_recs = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s, %s+%s-%s-%s@%s' % (
+ self.alias.alias_name, self.alias_domain,
+ self.alias_bounce, self.fake_email.id,
+ self.fake_email.model, self.fake_email.res_id,
+ self.alias_domain
+ ),
+ subject='Should bounce',
+ )
+ self.assertFalse(new_recs)
+ self.assertNotSentEmail()
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ def test_message_route_write_to_catchall(self):
+ """ Writing directly to catchall should bounce """
+ # Test: no group created, email bounced
+ with self.mock_mail_gateway():
+ record = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '"My Super Catchall" <%s@%s>' % (self.alias_catchall, self.alias_domain),
+ subject='Should Bounce')
+ self.assertFalse(record)
+ self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], subject='Re: Should Bounce')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_write_to_catchall_other_recipients_first(self):
+ """ Writing directly to catchall and a valid alias should take alias """
+ # Test: no group created, email bounced
+ with self.mock_mail_gateway():
+ record = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s, %s@%s' % (self.alias_catchall, self.alias_domain, self.alias.alias_name, self.alias_domain),
+ subject='Catchall Not Blocking'
+ )
+ # Test: one group created
+ self.assertEqual(len(record), 1, 'message_process: a new mail.test should have been created')
+ # No bounce email
+ self.assertNotSentEmail()
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_route_write_to_catchall_other_recipients_second(self):
+ """ Writing directly to catchall and a valid alias should take alias """
+ # Test: no group created, email bounced
+ with self.mock_mail_gateway():
+ record = self.format_and_process(
+ MAIL_TEMPLATE, self.partner_1.email_formatted,
+ '%s@%s, %s@%s' % (self.alias.alias_name, self.alias_domain, self.alias_catchall, self.alias_domain),
+ subject='Catchall Not Blocking'
+ )
+ # Test: one group created
+ self.assertEqual(len(record), 1, 'message_process: a new mail.test should have been created')
+ # No bounce email
+ self.assertNotSentEmail()
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_bounce_alias(self):
+ """ Writing to bounce alias is considered as a bounce even if not multipart/report bounce structure """
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ bounced_mail_id = 4442
+ bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com')
+ record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender')
+ self.assertFalse(record)
+ # No information found in bounce email -> not possible to do anything except avoiding email
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_bounce_from_mailer_demon(self):
+ """ MAILER_DAEMON emails are considered as bounce """
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ record = self.format_and_process(MAIL_TEMPLATE, 'MAILER-DAEMON@example.com', 'groups@test.com', subject='Undelivered Mail Returned to Sender')
+ self.assertFalse(record)
+ # No information found in bounce email -> not possible to do anything except avoiding email
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_bounce_multipart_alias(self):
+ """ Multipart/report bounce correctly make related partner bounce """
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ bounced_mail_id = 4442
+ bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com')
+ record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender')
+ self.assertFalse(record)
+ # Missing in reply to message_id -> cannot find original record
+ self.assertEqual(self.partner_1.message_bounce, 1)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_bounce_multipart_alias_reply(self):
+ """ Multipart/report bounce correctly make related partner and record found in bounce email bounce """
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ bounced_mail_id = 4442
+ bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com')
+ extra = self.fake_email.message_id
+ record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender', extra=extra)
+ self.assertFalse(record)
+ self.assertEqual(self.partner_1.message_bounce, 1)
+ self.assertEqual(self.test_record.message_bounce, 1)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_bounce_multipart_alias_whatever_from(self):
+ """ Multipart/report bounce correctly make related record found in bounce email bounce """
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ bounced_mail_id = 4442
+ bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com')
+ extra = self.fake_email.message_id
+ record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'Whatever <what@ever.com>', bounce_email_to, subject='Undelivered Mail Returned to Sender', extra=extra)
+ self.assertFalse(record)
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 1)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_bounce_multipart_whatever_to_and_from(self):
+ """ Multipart/report bounce correctly make related record found in bounce email bounce """
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ extra = self.fake_email.message_id
+ record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'Whatever <what@ever.com>', 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra)
+ self.assertFalse(record)
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(self.test_record.message_bounce, 1)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_bounce_records_channel(self):
+ """ Test blacklist allow to multi-bounce and auto update of mail.channel """
+ other_record = self.env['mail.test.gateway'].create({
+ 'email_from': 'Another name <%s>' % self.partner_1.email
+ })
+ yet_other_record = self.env['mail.test.gateway'].create({
+ 'email_from': 'Yet Another name <%s>' % self.partner_1.email.upper()
+ })
+ test_channel = self.env['mail.channel'].create({
+ 'name': 'Test',
+ 'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.partner_1.id})],
+ })
+ self.fake_email.write({
+ 'model': 'mail.channel',
+ 'res_id': test_channel.id,
+ })
+ self.assertIn(self.partner_1, test_channel.channel_partner_ids)
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.assertEqual(other_record.message_bounce, 0)
+ self.assertEqual(yet_other_record.message_bounce, 0)
+
+ extra = self.fake_email.message_id
+ for i in range(10):
+ record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'A third name <%s>' % self.partner_1.email, 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra)
+ self.assertFalse(record)
+ self.assertEqual(self.partner_1.message_bounce, 10)
+ self.assertEqual(self.test_record.message_bounce, 0)
+ self.assertEqual(other_record.message_bounce, 10)
+ self.assertEqual(yet_other_record.message_bounce, 10)
+ self.assertNotIn(self.partner_1, test_channel.channel_partner_ids)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_bounce_records_partner(self):
+ """ Test blacklist + bounce on ``res.partner`` model """
+ self.assertEqual(self.partner_1.message_bounce, 0)
+ self.fake_email.write({
+ 'model': 'res.partner',
+ 'res_id': self.partner_1.id,
+ })
+
+ extra = self.fake_email.message_id
+ record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra)
+ self.assertFalse(record)
+ self.assertEqual(self.partner_1.message_bounce, 1)
+ self.assertEqual(self.test_record.message_bounce, 0)
+
+ # --------------------------------------------------
+ # Thread formation
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_in_reply_to(self):
+ """ Incoming email using in-rely-to should go into the right destination even with a wrong destination """
+ init_msg_count = len(self.test_record.message_ids)
+ self.format_and_process(
+ MAIL_TEMPLATE, 'valid.other@gmail.com', 'erroneous@test.com>',
+ subject='Re: news', extra='In-Reply-To:\r\n\t%s\n' % self.fake_email.message_id)
+
+ self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1)
+ self.assertEqual(self.fake_email.child_ids, self.test_record.message_ids[0])
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_references(self):
+ """ Incoming email using references should go into the right destination even with a wrong destination """
+ init_msg_count = len(self.test_record.message_ids)
+ self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'erroneous@test.com',
+ extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id)
+
+ self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1)
+ self.assertEqual(self.fake_email.child_ids, self.test_record.message_ids[0])
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_references_external(self):
+ """ Incoming email being a reply to an external email processed by odoo should update thread accordingly """
+ new_message_id = '<ThisIsTooMuchFake.MonsterEmail.789@agrolait.com>'
+ self.fake_email.write({
+ 'message_id': new_message_id
+ })
+ init_msg_count = len(self.test_record.message_ids)
+ self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'erroneous@test.com',
+ extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id)
+
+ self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1)
+ self.assertEqual(self.fake_email.child_ids, self.test_record.message_ids[0])
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_references_external_buggy_message_id(self):
+ """
+ Incoming email being a reply to an external email processed by
+ odoo should update thread accordingly. Special case when the
+ external mail service wrongly folds the message_id on several
+ lines.
+ """
+ new_message_id = '<ThisIsTooMuchFake.MonsterEmail.789@agrolait.com>'
+ buggy_message_id = new_message_id.replace('MonsterEmail', 'Monster\r\n Email')
+ self.fake_email.write({
+ 'message_id': new_message_id
+ })
+ init_msg_count = len(self.test_record.message_ids)
+ self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'erroneous@test.com',
+ extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % buggy_message_id)
+
+ self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1)
+ self.assertEqual(self.fake_email.child_ids, self.test_record.message_ids[0])
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_process_references_forward(self):
+ """ Incoming email using references but with alias forward should not go into references destination """
+ self.env['mail.alias'].create({
+ 'alias_name': 'test.alias',
+ 'alias_user_id': False,
+ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
+ 'alias_contact': 'everyone',
+ })
+ init_msg_count = len(self.test_record.message_ids)
+ res_test = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'test.alias@test.com',
+ subject='My Dear Forward', extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id,
+ target_model='mail.test.container')
+
+ self.assertEqual(len(self.test_record.message_ids), init_msg_count)
+ self.assertEqual(len(self.fake_email.child_ids), 0)
+ self.assertEqual(res_test.name, 'My Dear Forward')
+ self.assertEqual(len(res_test.message_ids), 1)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_references_forward_same_model(self):
+ """ Incoming email using references but with alias forward on same model should be considered as a reply """
+ self.env['mail.alias'].create({
+ 'alias_name': 'test.alias',
+ 'alias_user_id': False,
+ 'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id,
+ 'alias_contact': 'everyone',
+ })
+ init_msg_count = len(self.test_record.message_ids)
+ res_test = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'test.alias@test.com',
+ subject='My Dear Forward', extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id,
+ target_model='mail.test.container')
+
+ self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1)
+ self.assertEqual(len(self.fake_email.child_ids), 1)
+ self.assertFalse(res_test)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_references_forward_cc(self):
+ """ Incoming email using references but with alias forward in CC should be considered as a repy (To > Cc) """
+ self.env['mail.alias'].create({
+ 'alias_name': 'test.alias',
+ 'alias_user_id': False,
+ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
+ 'alias_contact': 'everyone',
+ })
+ init_msg_count = len(self.test_record.message_ids)
+ res_test = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'catchall.test@test.com', cc='test.alias@test.com',
+ subject='My Dear Forward', extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id,
+ target_model='mail.test.container')
+
+ self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1)
+ self.assertEqual(len(self.fake_email.child_ids), 1)
+ self.assertFalse(res_test)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models', 'odoo.addons.mail.models.mail_mail')
+ def test_message_process_reply_to_new_thread(self):
+ """ Test replies not being considered as replies but use destination information instead (aka, mass post + specific reply to using aliases) """
+ first_record = self.env['mail.test.simple'].with_user(self.user_employee).create({'name': 'Replies to Record'})
+ record_msg = first_record.message_post(
+ subject='Discussion',
+ no_auto_thread=False,
+ subtype_xmlid='mail.mt_comment',
+ )
+ self.assertEqual(record_msg.reply_to, formataddr(('%s %s' % (self.user_employee.company_id.name, first_record.name), '%s@%s' % ('catchall.test', 'test.com'))))
+ mail_msg = first_record.message_post(
+ subject='Replies to Record',
+ reply_to='groups@test.com',
+ no_auto_thread=True,
+ subtype_xmlid='mail.mt_comment',
+ )
+ self.assertEqual(mail_msg.reply_to, 'groups@test.com')
+
+ # reply to mail but should be considered as a new mail for alias
+ msgID = '<this.is.duplicate.test@iron.sky>'
+ res_test = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, record_msg.reply_to, cc='',
+ subject='Re: Replies to Record', extra='In-Reply-To: %s' % record_msg.message_id,
+ msg_id=msgID, target_model='mail.test.simple')
+ incoming_msg = self.env['mail.message'].search([('message_id', '=', msgID)])
+ self.assertFalse(res_test)
+ self.assertEqual(incoming_msg.model, 'mail.test.simple')
+ self.assertEqual(incoming_msg.parent_id, first_record.message_ids[-1])
+ self.assertTrue(incoming_msg.res_id == first_record.id)
+
+ # reply to mail but should be considered as a new mail for alias
+ msgID = '<this.is.for.testing@iron.sky>'
+ res_test = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, mail_msg.reply_to, cc='',
+ subject='Re: Replies to Record', extra='In-Reply-To: %s' % mail_msg.message_id,
+ msg_id=msgID, target_model='mail.test.gateway')
+ incoming_msg = self.env['mail.message'].search([('message_id', '=', msgID)])
+ self.assertEqual(len(res_test), 1)
+ self.assertEqual(res_test.name, 'Re: Replies to Record')
+ self.assertEqual(incoming_msg.model, 'mail.test.gateway')
+ self.assertFalse(incoming_msg.parent_id)
+ self.assertTrue(incoming_msg.res_id == res_test.id)
+
+ # --------------------------------------------------
+ # Thread formation: mail gateway corner cases
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_extra_model_res_id(self):
+ """ Incoming email with ref holding model / res_id but that does not match any message in the thread: must raise since OpenERP saas-3 """
+ self.assertRaises(ValueError,
+ self.format_and_process, MAIL_TEMPLATE,
+ self.partner_1.email_formatted, 'noone@test.com', subject='spam',
+ extra='In-Reply-To: <12321321-openerp-%d-mail.test.gateway@%s>' % (self.test_record.id, socket.gethostname()))
+
+ # when 6.1 messages are present, compat mode is available
+ # Odoo 10 update: compat mode has been removed and should not work anymore
+ self.fake_email.write({'message_id': False})
+ # Do: compat mode accepts partial-matching emails
+ self.assertRaises(
+ ValueError,
+ self.format_and_process, MAIL_TEMPLATE,
+ self.partner_1.email_formatted, 'noone@test.com>', subject='spam',
+ extra='In-Reply-To: <12321321-openerp-%d-mail.test.gateway@%s>' % (self.test_record.id, socket.gethostname()))
+
+ # Test created messages
+ self.assertEqual(len(self.test_record.message_ids), 1)
+ self.assertEqual(len(self.test_record.message_ids[0].child_ids), 0)
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_duplicate(self):
+ """ Duplicate emails (same message_id) are not processed """
+ self.alias.write({'alias_force_thread_id': self.test_record.id,})
+
+ # Post a base message
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Re: super cats', msg_id='<123?456.diff1@agrolait.com>')
+ self.assertFalse(record)
+ self.assertEqual(len(self.test_record.message_ids), 2)
+
+ # Do: due to some issue, same email goes back into the mailgateway
+ record = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Re: news',
+ msg_id='<123?456.diff1@agrolait.com>', extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>\n')
+ self.assertFalse(record)
+ self.assertEqual(len(self.test_record.message_ids), 2)
+
+ # Test: message_id is still unique
+ no_of_msg = self.env['mail.message'].search_count([('message_id', 'ilike', '<123?456.diff1@agrolait.com>')])
+ self.assertEqual(no_of_msg, 1,
+ 'message_process: message with already existing message_id should not have been duplicated')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_crash_wrong_model(self):
+ """ Incoming email with model that does not accepts incoming emails must raise """
+ self.assertRaises(ValueError,
+ self.format_and_process,
+ MAIL_TEMPLATE, self.email_from, 'noone@test.com',
+ subject='spam', extra='', model='res.country')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_crash_no_data(self):
+ """ Incoming email without model and without alias must raise """
+ self.assertRaises(ValueError,
+ self.format_and_process,
+ MAIL_TEMPLATE, self.email_from, 'noone@test.com',
+ subject='spam', extra='')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
+ def test_message_process_fallback(self):
+ """ Incoming email with model that accepting incoming emails as fallback """
+ record = self.format_and_process(
+ MAIL_TEMPLATE, self.email_from, 'noone@test.com',
+ subject='Spammy', extra='', model='mail.test.gateway')
+ self.assertEqual(len(record), 1)
+ self.assertEqual(record.name, 'Spammy')
+ self.assertEqual(record._name, 'mail.test.gateway')
+
+
+class TestMailThreadCC(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailThreadCC, cls).setUpClass()
+
+ cls.email_from = 'Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>'
+ cls.alias = cls.env['mail.alias'].create({
+ 'alias_name': 'cc_record',
+ 'alias_user_id': False,
+ 'alias_model_id': cls.env['ir.model']._get('mail.test.cc').id,
+ 'alias_contact': 'everyone'})
+
+ cls._init_mail_gateway()
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_cc_new(self):
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'cc_record@test.com',
+ cc='cc1@example.com, cc2@example.com', target_model='mail.test.cc')
+ cc = email_split_and_format(record.email_cc)
+ self.assertEqual(sorted(cc), ['cc1@example.com', 'cc2@example.com'])
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_cc_update_with_old(self):
+ record = self.env['mail.test.cc'].create({'email_cc': 'cc1 <cc1@example.com>, cc2@example.com'})
+ self.alias.write({'alias_force_thread_id': record.id})
+
+ self.format_and_process(MAIL_TEMPLATE, self.email_from, 'cc_record@test.com',
+ cc='cc2 <cc2@example.com>, cc3@example.com', target_model='mail.test.cc')
+ cc = email_split_and_format(record.email_cc)
+ self.assertEqual(sorted(cc), ['"cc1" <cc1@example.com>', 'cc2@example.com', 'cc3@example.com'], 'new cc should have been added on record (unique)')
+
+ @mute_logger('odoo.addons.mail.models.mail_thread')
+ def test_message_cc_update_no_old(self):
+ record = self.env['mail.test.cc'].create({})
+ self.alias.write({'alias_force_thread_id': record.id})
+
+ self.format_and_process(MAIL_TEMPLATE, self.email_from, 'cc_record@test.com',
+ cc='cc2 <cc2@example.com>, cc3@example.com', target_model='mail.test.cc')
+ cc = email_split_and_format(record.email_cc)
+ self.assertEqual(sorted(cc), ['"cc2" <cc2@example.com>', 'cc3@example.com'], 'new cc should have been added on record (unique)')
diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py
new file mode 100644
index 00000000..3ebee263
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_mail.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import psycopg2
+
+from odoo import api
+from odoo.addons.test_mail.tests.common import TestMailCommon
+from odoo.tests import common, tagged
+from odoo.tools import mute_logger
+
+
+@tagged('mail_mail')
+class TestMailMail(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailMail, cls).setUpClass()
+ cls._init_mail_gateway()
+
+ cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({
+ 'name': 'Test',
+ 'email_from': 'ignasse@example.com',
+ }).with_context({})
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_message_notify_from_mail_mail(self):
+ # Due ot post-commit hooks, store send emails in every step
+ mail = self.env['mail.mail'].sudo().create({
+ 'body_html': '<p>Test</p>',
+ 'email_to': 'test@example.com',
+ 'partner_ids': [(4, self.user_employee.partner_id.id)]
+ })
+ with self.mock_mail_gateway():
+ mail.send()
+ self.assertSentEmail(mail.env.user.partner_id, ['test@example.com'])
+ self.assertEqual(len(self._mails), 1)
+
+ def test_mail_mail_return_path(self):
+ # mail without thread-enabled record
+ base_values = {
+ 'body_html': '<p>Test</p>',
+ 'email_to': 'test@example.com',
+ }
+
+ mail = self.env['mail.mail'].create(base_values)
+ with self.mock_mail_gateway():
+ mail.send()
+ self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s+%d@%s' % (self.alias_bounce, mail.id, self.alias_domain))
+
+ # mail on thread-enabled record
+ mail = self.env['mail.mail'].create(dict(base_values, **{
+ 'model': self.test_record._name,
+ 'res_id': self.test_record.id,
+ }))
+ with self.mock_mail_gateway():
+ mail.send()
+ self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s+%d-%s-%s@%s' % (self.alias_bounce, mail.id, self.test_record._name, self.test_record.id, self.alias_domain))
+
+ # force static addressing on bounce alias
+ self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True)
+
+ # mail without thread-enabled record
+ mail = self.env['mail.mail'].create(base_values)
+ with self.mock_mail_gateway():
+ mail.send()
+ self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain))
+
+ # mail on thread-enabled record
+ mail = self.env['mail.mail'].create(dict(base_values, **{
+ 'model': self.test_record._name,
+ 'res_id': self.test_record.id,
+ }))
+ with self.mock_mail_gateway():
+ mail.send()
+ self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain))
+
+
+class TestMailMailRace(common.TransactionCase):
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_bounce_during_send(self):
+ self.partner = self.env['res.partner'].create({
+ 'name': 'Ernest Partner',
+ })
+ # we need to simulate a mail sent by the cron task, first create mail, message and notification by hand
+ mail = self.env['mail.mail'].sudo().create({
+ 'body_html': '<p>Test</p>',
+ 'notification': True,
+ 'state': 'outgoing',
+ 'recipient_ids': [(4, self.partner.id)]
+ })
+ message = self.env['mail.message'].create({
+ 'subject': 'S',
+ 'body': 'B',
+ 'subtype_id': self.ref('mail.mt_comment'),
+ 'notification_ids': [(0, 0, {
+ 'res_partner_id': self.partner.id,
+ 'mail_id': mail.id,
+ 'notification_type': 'email',
+ 'is_read': True,
+ 'notification_status': 'ready',
+ })],
+ })
+ notif = self.env['mail.notification'].search([('res_partner_id', '=', self.partner.id)])
+ # we need to commit transaction or cr will keep the lock on notif
+ self.cr.commit()
+
+ # patch send_email in order to create a concurent update and check the notif is already locked by _send()
+ this = self # coding in javascript ruinned my life
+ bounce_deferred = []
+ @api.model
+ def send_email(self, message, *args, **kwargs):
+ with this.registry.cursor() as cr, mute_logger('odoo.sql_db'):
+ try:
+ # try ro aquire lock (no wait) on notification (should fail)
+ cr.execute("SELECT notification_status FROM mail_message_res_partner_needaction_rel WHERE id = %s FOR UPDATE NOWAIT", [notif.id])
+ except psycopg2.OperationalError:
+ # record already locked by send, all good
+ bounce_deferred.append(True)
+ else:
+ # this should trigger psycopg2.extensions.TransactionRollbackError in send().
+ # Only here to simulate the initial use case
+ # If the record is lock, this line would create a deadlock since we are in the same thread
+ # In practice, the update will wait the end of the send() transaction and set the notif as bounce, as expeced
+ cr.execute("UPDATE mail_message_res_partner_needaction_rel SET notification_status='bounce' WHERE id = %s", [notif.id])
+ return message['Message-Id']
+ self.env['ir.mail_server']._patch_method('send_email', send_email)
+
+ mail.send()
+
+ self.assertTrue(bounce_deferred, "The bounce should have been deferred")
+ self.assertEqual(notif.notification_status, 'sent')
+
+ # some cleaning since we commited the cr
+ self.env['ir.mail_server']._revert_method('send_email')
+
+ notif.unlink()
+ message.unlink()
+ mail.unlink()
+ self.partner.unlink()
+ self.env.cr.commit()
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
new file mode 100644
index 00000000..8d384f72
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -0,0 +1,591 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+from unittest.mock import patch
+
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.addons.test_mail.tests.common import TestMailCommon
+from odoo.addons.test_mail.models.test_mail_models import MailTestSimple
+from odoo.exceptions import AccessError
+from odoo.tools import mute_logger, formataddr
+from odoo.tests import tagged
+
+
+class TestMessageValues(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMessageValues, cls).setUpClass()
+
+ cls._init_mail_gateway()
+ cls.alias_record = cls.env['mail.test.container'].with_context(cls._test_context).create({
+ 'name': 'Pigs',
+ 'alias_name': 'pigs',
+ 'alias_contact': 'followers',
+ })
+ cls.Message = cls.env['mail.message'].with_user(cls.user_employee)
+
+ @mute_logger('odoo.models.unlink')
+ def test_mail_message_format(self):
+ record1 = self.env['mail.test.simple'].create({'name': 'Test1'})
+ message = self.env['mail.message'].create([{
+ 'model': 'mail.test.simple',
+ 'res_id': record1.id,
+ }])
+ res = message.message_format()
+ self.assertEqual(res[0].get('record_name'), 'Test1')
+
+ record1.write({"name": "Test2"})
+ res = message.message_format()
+ self.assertEqual(res[0].get('record_name'), 'Test2')
+
+ @mute_logger('odoo.models.unlink')
+ def test_mail_message_format_access(self):
+ """
+ User that doesn't have access to a record should still be able to fetch
+ the record_name inside message_format.
+ """
+ company_2 = self.env['res.company'].create({'name': 'Second Test Company'})
+ record1 = self.env['mail.test.multi.company'].create({
+ 'name': 'Test1',
+ 'company_id': company_2.id,
+ })
+ message = record1.message_post(body='', partner_ids=[self.user_employee.partner_id.id])
+ # We need to flush and invalidate the ORM cache since the record_name
+ # is already cached from the creation. Otherwise it will leak inside
+ # message_format.
+ message.flush()
+ message.invalidate_cache()
+ res = message.with_user(self.user_employee).message_format()
+ self.assertEqual(res[0].get('record_name'), 'Test1')
+
+ @mute_logger('odoo.models.unlink')
+ def test_mail_message_values_no_document_values(self):
+ msg = self.Message.create({
+ 'reply_to': 'test.reply@example.com',
+ 'email_from': 'test.from@example.com',
+ })
+ self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
+ self.assertEqual(msg.reply_to, 'test.reply@example.com')
+ self.assertEqual(msg.email_from, 'test.from@example.com')
+
+ @mute_logger('odoo.models.unlink')
+ def test_mail_message_values_no_document(self):
+ msg = self.Message.create({})
+ self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
+ reply_to_name = self.env.user.company_id.name
+ reply_to_email = '%s@%s' % (self.alias_catchall, self.alias_domain)
+ self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
+ self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+
+ # no alias domain -> author
+ self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
+
+ msg = self.Message.create({})
+ self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
+ self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email)))
+ self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+
+ # no alias catchall, no alias -> author
+ self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain)
+ self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink()
+
+ msg = self.Message.create({})
+ self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
+ self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email)))
+ self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+
+ @mute_logger('odoo.models.unlink')
+ def test_mail_message_values_document_alias(self):
+ msg = self.Message.create({
+ 'model': 'mail.test.container',
+ 'res_id': self.alias_record.id
+ })
+ self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
+ reply_to_name = '%s %s' % (self.env.user.company_id.name, self.alias_record.name)
+ reply_to_email = '%s@%s' % (self.alias_record.alias_name, self.alias_domain)
+ self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
+ self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+
+ # no alias domain -> author
+ self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
+
+ msg = self.Message.create({
+ 'model': 'mail.test.container',
+ 'res_id': self.alias_record.id
+ })
+ self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
+ self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email)))
+ self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+
+ # no catchall -> don't care, alias
+ self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain)
+ self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink()
+
+ msg = self.Message.create({
+ 'model': 'mail.test.container',
+ 'res_id': self.alias_record.id
+ })
+ self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
+ reply_to_name = '%s %s' % (self.env.company.name, self.alias_record.name)
+ reply_to_email = '%s@%s' % (self.alias_record.alias_name, self.alias_domain)
+ self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
+ self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+
+ @mute_logger('odoo.models.unlink')
+ def test_mail_message_values_document_no_alias(self):
+ test_record = self.env['mail.test.simple'].create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+
+ msg = self.Message.create({
+ 'model': 'mail.test.simple',
+ 'res_id': test_record.id
+ })
+ self.assertIn('-openerp-%d-mail.test.simple' % test_record.id, msg.message_id.split('@')[0])
+ reply_to_name = '%s %s' % (self.env.user.company_id.name, test_record.name)
+ reply_to_email = '%s@%s' % (self.alias_catchall, self.alias_domain)
+ self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
+ self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+
+ @mute_logger('odoo.models.unlink')
+ def test_mail_message_values_document_manual_alias(self):
+ test_record = self.env['mail.test.simple'].create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+ alias = self.env['mail.alias'].create({
+ 'alias_name': 'MegaLias',
+ 'alias_user_id': False,
+ 'alias_model_id': self.env['ir.model']._get('mail.test.simple').id,
+ 'alias_parent_model_id': self.env['ir.model']._get('mail.test.simple').id,
+ 'alias_parent_thread_id': test_record.id,
+ })
+
+ msg = self.Message.create({
+ 'model': 'mail.test.simple',
+ 'res_id': test_record.id
+ })
+
+ self.assertIn('-openerp-%d-mail.test.simple' % test_record.id, msg.message_id.split('@')[0])
+ reply_to_name = '%s %s' % (self.env.user.company_id.name, test_record.name)
+ reply_to_email = '%s@%s' % (alias.alias_name, self.alias_domain)
+ self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
+ self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+
+ def test_mail_message_values_no_auto_thread(self):
+ msg = self.Message.create({
+ 'model': 'mail.test.container',
+ 'res_id': self.alias_record.id,
+ 'no_auto_thread': True,
+ })
+ self.assertIn('reply_to', msg.message_id.split('@')[0])
+ self.assertNotIn('mail.test.container', msg.message_id.split('@')[0])
+ self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id.split('@')[0])
+
+ def test_mail_message_base64_image(self):
+ msg = self.env['mail.message'].with_user(self.user_employee).create({
+ 'body': 'taratata <img src="data:image/png;base64,iV/+OkI=" width="2"> <img src="data:image/png;base64,iV/+OkI=" width="2">',
+ })
+ self.assertEqual(len(msg.attachment_ids), 1)
+ body = '<p>taratata <img src="/web/image/%s?access_token=%s" alt="image0" width="2"> <img src="/web/image/%s?access_token=%s" alt="image0" width="2"></p>'
+ body = body % (msg.attachment_ids[0].id, msg.attachment_ids[0].access_token, msg.attachment_ids[0].id, msg.attachment_ids[0].access_token)
+ self.assertEqual(msg.body, body)
+
+
+class TestMessageAccess(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMessageAccess, cls).setUpClass()
+
+ cls.user_public = mail_new_test_user(cls.env, login='bert', groups='base.group_public', name='Bert Tartignole')
+ cls.user_portal = mail_new_test_user(cls.env, login='chell', groups='base.group_portal', name='Chell Gladys')
+
+ Channel = cls.env['mail.channel'].with_context(cls._test_context)
+ # Pigs: base group for tests
+ cls.group_pigs = Channel.create({
+ 'name': 'Pigs',
+ 'public': 'groups',
+ 'group_public_id': cls.env.ref('base.group_user').id})
+ # Jobs: public group
+ cls.group_public = Channel.create({
+ 'name': 'Jobs',
+ 'description': 'NotFalse',
+ 'public': 'public'})
+ # Private: private gtroup
+ cls.group_private = Channel.create({
+ 'name': 'Private',
+ 'public': 'private'})
+ cls.message = cls.env['mail.message'].create({
+ 'body': 'My Body',
+ 'model': 'mail.channel',
+ 'res_id': cls.group_private.id,
+ })
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_message_access_search(self):
+ # Data: various author_ids, partner_ids, documents
+ msg1 = self.env['mail.message'].create({
+ 'subject': '_ZTest', 'body': 'A', 'subtype_id': self.ref('mail.mt_comment')})
+ msg2 = self.env['mail.message'].create({
+ 'subject': '_ZTest', 'body': 'A+B', 'subtype_id': self.ref('mail.mt_comment'),
+ 'partner_ids': [(6, 0, [self.user_public.partner_id.id])]})
+ msg3 = self.env['mail.message'].create({
+ 'subject': '_ZTest', 'body': 'A Pigs', 'subtype_id': False,
+ 'model': 'mail.channel', 'res_id': self.group_pigs.id})
+ msg4 = self.env['mail.message'].create({
+ 'subject': '_ZTest', 'body': 'A+P Pigs', 'subtype_id': self.ref('mail.mt_comment'),
+ 'model': 'mail.channel', 'res_id': self.group_pigs.id,
+ 'partner_ids': [(6, 0, [self.user_public.partner_id.id])]})
+ msg5 = self.env['mail.message'].create({
+ 'subject': '_ZTest', 'body': 'A+E Pigs', 'subtype_id': self.ref('mail.mt_comment'),
+ 'model': 'mail.channel', 'res_id': self.group_pigs.id,
+ 'partner_ids': [(6, 0, [self.user_employee.partner_id.id])]})
+ msg6 = self.env['mail.message'].create({
+ 'subject': '_ZTest', 'body': 'A Birds', 'subtype_id': self.ref('mail.mt_comment'),
+ 'model': 'mail.channel', 'res_id': self.group_private.id})
+ msg7 = self.env['mail.message'].with_user(self.user_employee).create({
+ 'subject': '_ZTest', 'body': 'B', 'subtype_id': self.ref('mail.mt_comment')})
+ msg8 = self.env['mail.message'].with_user(self.user_employee).create({
+ 'subject': '_ZTest', 'body': 'B+E', 'subtype_id': self.ref('mail.mt_comment'),
+ 'partner_ids': [(6, 0, [self.user_employee.partner_id.id])]})
+
+ # Test: Public: 2 messages (recipient)
+ messages = self.env['mail.message'].with_user(self.user_public).search([('subject', 'like', '_ZTest')])
+ self.assertEqual(messages, msg2 | msg4)
+
+ # Test: Employee: 3 messages on Pigs Raoul can read (employee can read group with default values)
+ messages = self.env['mail.message'].with_user(self.user_employee).search([('subject', 'like', '_ZTest'), ('body', 'ilike', 'A')])
+ self.assertEqual(messages, msg3 | msg4 | msg5)
+
+ # Test: Raoul: 3 messages on Pigs Raoul can read (employee can read group with default values), 0 on Birds (private group) + 2 messages as author
+ messages = self.env['mail.message'].with_user(self.user_employee).search([('subject', 'like', '_ZTest')])
+ self.assertEqual(messages, msg3 | msg4 | msg5 | msg7 | msg8)
+
+ # Test: Admin: all messages
+ messages = self.env['mail.message'].search([('subject', 'like', '_ZTest')])
+ self.assertEqual(messages, msg1 | msg2 | msg3 | msg4 | msg5 | msg6 | msg7 | msg8)
+
+ # Test: Portal: 0 (no access to groups, not recipient)
+ messages = self.env['mail.message'].with_user(self.user_portal).search([('subject', 'like', '_ZTest')])
+ self.assertFalse(messages)
+
+ # Test: Portal: 2 messages (public group with a subtype)
+ self.group_pigs.write({'public': 'public'})
+ messages = self.env['mail.message'].with_user(self.user_portal).search([('subject', 'like', '_ZTest')])
+ self.assertEqual(messages, msg4 | msg5)
+
+ # --------------------------------------------------
+ # READ
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
+ def test_mail_message_access_read_crash(self):
+ with self.assertRaises(AccessError):
+ self.message.with_user(self.user_employee).read()
+
+ @mute_logger('odoo.models')
+ def test_mail_message_access_read_crash_portal(self):
+ with self.assertRaises(AccessError):
+ self.message.with_user(self.user_portal).read(['body', 'message_type', 'subtype_id'])
+
+ def test_mail_message_access_read_ok_portal(self):
+ self.message.write({'subtype_id': self.ref('mail.mt_comment'), 'res_id': self.group_public.id})
+ self.message.with_user(self.user_portal).read(['body', 'message_type', 'subtype_id'])
+
+ def test_mail_message_access_read_notification(self):
+ attachment = self.env['ir.attachment'].create({
+ 'datas': base64.b64encode(b'My attachment'),
+ 'name': 'doc.txt'})
+ # attach the attachment to the message
+ self.message.write({'attachment_ids': [(4, attachment.id)]})
+ self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
+ self.message.with_user(self.user_employee).read()
+ # Test: Bert has access to attachment, ok because he can read message
+ attachment.with_user(self.user_employee).read(['name', 'datas'])
+
+ def test_mail_message_access_read_author(self):
+ self.message.write({'author_id': self.user_employee.partner_id.id})
+ self.message.with_user(self.user_employee).read()
+
+ def test_mail_message_access_read_doc(self):
+ self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id})
+ # Test: Bert reads the message, ok because linked to a doc he is allowed to read
+ self.message.with_user(self.user_employee).read()
+
+ def test_mail_message_access_read_crash_moderation(self):
+ # with self.assertRaises(AccessError):
+ self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
+ # Test: Bert reads the message, ok because linked to a doc he is allowed to read
+ self.message.with_user(self.user_employee).read()
+
+ # --------------------------------------------------
+ # CREATE
+ # --------------------------------------------------
+
+ @mute_logger('odoo.addons.base.models.ir_model')
+ def test_mail_message_access_create_crash_public(self):
+ # Do: Bert creates a message on Pigs -> ko, no creation rights
+ with self.assertRaises(AccessError):
+ self.env['mail.message'].with_user(self.user_public).create({'model': 'mail.channel', 'res_id': self.group_pigs.id, 'body': 'Test'})
+
+ # Do: Bert create a message on Jobs -> ko, no creation rights
+ with self.assertRaises(AccessError):
+ self.env['mail.message'].with_user(self.user_public).create({'model': 'mail.channel', 'res_id': self.group_public.id, 'body': 'Test'})
+
+ @mute_logger('odoo.models')
+ def test_mail_message_access_create_crash(self):
+ # Do: Bert create a private message -> ko, no creation rights
+ with self.assertRaises(AccessError):
+ self.env['mail.message'].with_user(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test'})
+
+ @mute_logger('odoo.models')
+ def test_mail_message_access_create_doc(self):
+ Message = self.env['mail.message'].with_user(self.user_employee)
+ # Do: Raoul creates a message on Jobs -> ok, write access to the related document
+ Message.create({'model': 'mail.channel', 'res_id': self.group_public.id, 'body': 'Test'})
+ # Do: Raoul creates a message on Priv -> ko, no write access to the related document
+ with self.assertRaises(AccessError):
+ Message.create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test'})
+
+ def test_mail_message_access_create_private(self):
+ self.env['mail.message'].with_user(self.user_employee).create({'body': 'Test'})
+
+ def test_mail_message_access_create_reply(self):
+ # TDE FIXME: should it really work ? not sure - catchall makes crash (aka, post will crash also)
+ self.env['ir.config_parameter'].set_param('mail.catchall.domain', False)
+ self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
+ self.env['mail.message'].with_user(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test', 'parent_id': self.message.id})
+
+ def test_mail_message_access_create_wo_parent_access(self):
+ """ Purpose is to test posting a message on a record whose first message / parent
+ is not accessible by current user. """
+ test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+ partner_1 = self.env['res.partner'].create({
+ 'name': 'Jitendra Prajapati (jpr-odoo)',
+ 'email': 'jpr@odoo.com',
+ })
+ test_record.message_subscribe((partner_1 | self.user_admin.partner_id).ids)
+
+ message = test_record.message_post(
+ body='<p>This is First Message</p>', subject='Subject',
+ message_type='comment', subtype_xmlid='mail.mt_note')
+ # portal user have no rights to read the message
+ with self.assertRaises(AccessError):
+ message.with_user(self.user_portal).read(['subject, body'])
+
+ with patch.object(MailTestSimple, 'check_access_rights', return_value=True):
+ with self.assertRaises(AccessError):
+ message.with_user(self.user_portal).read(['subject, body'])
+
+ # parent message is accessible to references notification mail values
+ # for _notify method and portal user have no rights to send the message for this model
+ new_msg = test_record.with_user(self.user_portal).message_post(
+ body='<p>This is Second Message</p>',
+ subject='Subject',
+ parent_id=message.id,
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment',
+ mail_auto_delete=False)
+
+ new_mail = self.env['mail.mail'].sudo().search([
+ ('mail_message_id', '=', new_msg.id),
+ ('references', '=', message.message_id),
+ ])
+
+ self.assertTrue(new_mail)
+ self.assertEqual(new_msg.parent_id, message)
+
+ # --------------------------------------------------
+ # WRITE
+ # --------------------------------------------------
+
+ def test_mail_message_access_write_moderation(self):
+ """ Only moderators can modify pending messages """
+ self.group_public.write({
+ 'email_send': True,
+ 'moderation': True,
+ 'channel_partner_ids': [(4, self.partner_employee.id)],
+ 'moderator_ids': [(4, self.user_employee.id)],
+ })
+ self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
+ self.message.with_user(self.user_employee).write({'moderation_status': 'accepted'})
+
+ def test_mail_message_access_write_crash_moderation(self):
+ self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
+ with self.assertRaises(AccessError):
+ self.message.with_user(self.user_employee).write({'moderation_status': 'accepted'})
+
+ @mute_logger('openerp.addons.mail.models.mail_mail')
+ def test_mark_all_as_read(self):
+ self.user_employee.notification_type = 'inbox'
+ emp_partner = self.user_employee.partner_id.with_user(self.user_employee)
+
+ group_private = self.env['mail.channel'].with_context({
+ 'mail_create_nolog': True,
+ 'mail_create_nosubscribe': True,
+ 'mail_channel_noautofollow': True,
+ }).create({
+ 'name': 'Private',
+ 'description': 'Private James R.',
+ 'public': 'private',
+ 'alias_name': 'private',
+ 'alias_contact': 'followers'}
+ ).with_context({'mail_create_nosubscribe': False})
+
+ # mark all as read clear needactions
+ msg1 = group_private.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[emp_partner.id])
+ self._reset_bus()
+ emp_partner.env['mail.message'].mark_all_as_read(domain=[])
+ self.assertBusNotifications([(self.cr.dbname, 'res.partner', emp_partner.id)], [{ 'type': 'mark_as_read', 'message_ids': [msg1.id], 'needaction_inbox_counter': 0 }])
+ na_count = emp_partner.get_needaction_count()
+ self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
+
+ # mark all as read also clear inaccessible needactions
+ msg2 = group_private.message_post(body='Zest', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[emp_partner.id])
+ needaction_accessible = len(emp_partner.env['mail.message'].search([['needaction', '=', True]]))
+ self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner")
+
+ msg2.sudo().partner_ids = self.env['res.partner']
+ emp_partner.env['mail.message'].search([['needaction', '=', True]])
+ needaction_length = len(emp_partner.env['mail.message'].search([['needaction', '=', True]]))
+ self.assertEqual(needaction_length, 1, "message should still be readable when notified")
+
+ na_count = emp_partner.get_needaction_count()
+ self.assertEqual(na_count, 1, "message not accessible is currently still counted")
+
+ self._reset_bus()
+ emp_partner.env['mail.message'].mark_all_as_read(domain=[])
+ self.assertBusNotifications([(self.cr.dbname, 'res.partner', emp_partner.id)], [{ 'type': 'mark_as_read', 'message_ids': [msg2.id], 'needaction_inbox_counter': 0 }])
+ na_count = emp_partner.get_needaction_count()
+ self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
+
+ @mute_logger('openerp.addons.mail.models.mail_mail')
+ def test_mark_all_as_read_share(self):
+ self.user_portal.notification_type = 'inbox'
+ portal_partner = self.user_portal.partner_id.with_user(self.user_portal)
+
+ # mark all as read clear needactions
+ self.group_pigs.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[portal_partner.id])
+ portal_partner.env['mail.message'].mark_all_as_read(domain=[])
+ na_count = portal_partner.get_needaction_count()
+ self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
+
+ # mark all as read also clear inaccessible needactions
+ new_msg = self.group_pigs.message_post(body='Zest', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[portal_partner.id])
+ needaction_accessible = len(portal_partner.env['mail.message'].search([['needaction', '=', True]]))
+ self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner")
+
+ new_msg.sudo().partner_ids = self.env['res.partner']
+ needaction_length = len(portal_partner.env['mail.message'].search([['needaction', '=', True]]))
+ self.assertEqual(needaction_length, 1, "message should still be readable when notified")
+
+ na_count = portal_partner.get_needaction_count()
+ self.assertEqual(na_count, 1, "message not accessible is currently still counted")
+
+ portal_partner.env['mail.message'].mark_all_as_read(domain=[])
+ na_count = portal_partner.get_needaction_count()
+ self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
+
+
+@tagged('moderation')
+class TestMessageModeration(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMessageModeration, cls).setUpClass()
+
+ cls.channel_1 = cls.env['mail.channel'].create({
+ 'name': 'Moderation_1',
+ 'email_send': True,
+ 'moderation': True
+ })
+ cls.user_employee.write({'moderation_channel_ids': [(6, 0, [cls.channel_1.id])]})
+ cls.user_portal = cls._create_portal_user()
+
+ # A pending moderation message needs to have field channel_ids empty. Moderators
+ # need to be able to notify a pending moderation message (in a channel they moderate).
+ cls.msg_c1_admin1 = cls._add_messages(cls.channel_1, 'Body11', author=cls.partner_admin, moderation_status='pending_moderation')
+ cls.msg_c1_admin2 = cls._add_messages(cls.channel_1, 'Body12', author=cls.partner_admin, moderation_status='pending_moderation')
+ cls.msg_c1_portal = cls._add_messages(cls.channel_1, 'Body21', author=cls.partner_portal, moderation_status='pending_moderation')
+
+ @mute_logger('odoo.models.unlink')
+ def test_moderate_accept(self):
+ self._reset_bus()
+ self.assertFalse(self.msg_c1_admin1.channel_ids | self.msg_c1_admin2.channel_ids | self.msg_c1_portal.channel_ids)
+
+ self.msg_c1_admin1.with_user(self.user_employee)._moderate('accept')
+ self.assertEqual(self.msg_c1_admin1.channel_ids, self.channel_1)
+ self.assertEqual(self.msg_c1_admin1.moderation_status, 'accepted')
+ self.assertEqual(self.msg_c1_admin2.moderation_status, 'pending_moderation')
+ self.assertBusNotifications([(self.cr.dbname, 'mail.channel', self.channel_1.id)])
+
+ @mute_logger('odoo.models.unlink')
+ def test_moderate_allow(self):
+ self._reset_bus()
+
+ self.msg_c1_admin1.with_user(self.user_employee)._moderate('allow')
+ self.assertEqual(self.msg_c1_admin1.channel_ids, self.channel_1)
+ self.assertEqual(self.msg_c1_admin2.channel_ids, self.channel_1)
+ self.assertEqual(self.msg_c1_admin1.moderation_status, 'accepted')
+ self.assertEqual(self.msg_c1_admin2.moderation_status, 'accepted')
+ self.assertBusNotifications([
+ (self.cr.dbname, 'mail.channel', self.channel_1.id),
+ (self.cr.dbname, 'mail.channel', self.channel_1.id)])
+
+ @mute_logger('odoo.models.unlink')
+ def test_moderate_reject(self):
+ with self.mock_mail_gateway():
+ (self.msg_c1_admin1 | self.msg_c1_portal).with_user(self.user_employee)._moderate_send_reject_email('Title', 'Message to author')
+ self.assertEqual(len(self._new_mails), 2)
+ for mail in self._new_mails:
+ self.assertEqual(mail.author_id, self.partner_employee)
+ self.assertEqual(mail.subject, 'Title')
+ self.assertEqual(mail.state, 'outgoing')
+ self.assertEqual(
+ set(self._new_mails.mapped('email_to')),
+ set([self.msg_c1_admin1.email_from, self.msg_c1_portal.email_from])
+ )
+ self.assertEqual(
+ set(self._new_mails.mapped('body_html')),
+ set(['<div>Message to author</div>\n%s\n' % self.msg_c1_admin1.body, '<div>Message to author</div>\n%s\n' % self.msg_c1_portal.body])
+ ) # TDE note: \n are added by append content to html, because why not
+
+ @mute_logger('odoo.models.unlink')
+ def test_moderate_discard(self):
+ self._reset_bus()
+ id1, id2, id3 = self.msg_c1_admin1.id, self.msg_c1_admin2.id, self.msg_c1_portal.id # save ids because unlink will discard them
+ (self.msg_c1_admin1 | self.msg_c1_admin2 | self.msg_c1_portal).with_user(self.user_employee)._moderate_discard()
+
+ self.assertBusNotifications(
+ [(self.cr.dbname, 'res.partner', self.partner_admin.id),
+ (self.cr.dbname, 'res.partner', self.partner_employee.id),
+ (self.cr.dbname, 'res.partner', self.partner_portal.id)],
+ [{'type': 'deletion', 'message_ids': [id1, id2]}, # author of 2 messages
+ {'type': 'deletion', 'message_ids': [id1, id2, id3]}, # moderator
+ {'type': 'deletion', 'message_ids': [id3]}] # author of 1 message
+ )
+
+ @mute_logger('odoo.models.unlink')
+ def test_notify_moderators(self):
+ # create pending messages in another channel to have two notification to push
+ channel_2 = self.env['mail.channel'].create({
+ 'name': 'Moderation_1',
+ 'email_send': True,
+ 'moderation': True
+ })
+ self.user_admin.write({'moderation_channel_ids': [(6, 0, [channel_2.id])]})
+ self.msg_c2_portal = self._add_messages(channel_2, 'Body31', author=self.partner_portal, moderation_status='pending_moderation')
+
+ # one notification for each moderator: employee (channel1), admin (channel2)
+ with self.assertPostNotifications([{
+ 'content': 'Hello %s' % self.partner_employee.name,
+ 'message_type': 'user_notification', 'subtype': 'mail.mt_note',
+ 'notif': [{
+ 'partner': self.partner_employee,
+ 'type': 'inbox'}]
+ }, {
+ 'content': 'Hello %s' % self.partner_admin.name,
+ 'message_type': 'user_notification', 'subtype': 'mail.mt_note',
+ 'notif': [{
+ 'partner': self.partner_admin,
+ 'type': 'inbox'}]
+ }]):
+ self.env['mail.message']._notify_moderators()
diff --git a/addons/test_mail/tests/test_mail_template.py b/addons/test_mail/tests/test_mail_template.py
new file mode 100644
index 00000000..5bf72f3d
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_template.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+
+from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
+from odoo.tools import mute_logger
+
+
+class TestMailTemplate(TestMailCommon, TestRecipients):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailTemplate, cls).setUpClass()
+ cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+
+ cls.user_employee.write({
+ 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)],
+ })
+
+ cls._attachments = [{
+ 'name': 'first.txt',
+ 'datas': base64.b64encode(b'My first attachment'),
+ 'res_model': 'res.partner',
+ 'res_id': cls.user_admin.partner_id.id
+ }, {
+ 'name': 'second.txt',
+ 'datas': base64.b64encode(b'My second attachment'),
+ 'res_model': 'res.partner',
+ 'res_id': cls.user_admin.partner_id.id
+ }]
+
+ cls.email_1 = 'test1@example.com'
+ cls.email_2 = 'test2@example.com'
+ cls.email_3 = cls.partner_1.email
+ cls._create_template('mail.test.simple', {
+ 'attachment_ids': [(0, 0, cls._attachments[0]), (0, 0, cls._attachments[1])],
+ 'partner_to': '%s,%s' % (cls.partner_2.id, cls.user_admin.partner_id.id),
+ 'email_to': '%s, %s' % (cls.email_1, cls.email_2),
+ 'email_cc': '%s' % cls.email_3,
+ })
+
+ # admin should receive emails
+ cls.user_admin.write({'notification_type': 'email'})
+ # Force the attachments of the template to be in the natural order.
+ cls.email_template.invalidate_cache(['attachment_ids'], ids=cls.email_template.ids)
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_template_send_email(self):
+ mail_id = self.email_template.send_mail(self.test_record.id)
+ mail = self.env['mail.mail'].sudo().browse(mail_id)
+ self.assertEqual(mail.subject, 'About %s' % self.test_record.name)
+ self.assertEqual(mail.email_to, self.email_template.email_to)
+ self.assertEqual(mail.email_cc, self.email_template.email_cc)
+ self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_template_translation(self):
+ self.env['res.lang']._activate_lang('es_ES')
+ self.env.ref('base.module_base')._update_translations(['es_ES'])
+
+ partner = self.env['res.partner'].create({'name': "test", 'lang': 'es_ES'})
+ email_template = self.env['mail.template'].create({
+ 'name': 'TestTemplate',
+ 'subject': 'English Subject',
+ 'body_html': '<p>English Body</p>',
+ 'model_id': self.env['ir.model']._get(partner._name).id,
+ 'lang': '${object.lang}'
+ })
+ # Make sure Spanish translations have not been altered
+ description_translations = self.env['ir.translation'].search([('module', '=', 'base'), ('src', '=', partner._description), ('lang', '=', 'es_ES')])
+ description_translations.update({'value': 'Spanish description'})
+
+ self.env['ir.translation'].create({
+ 'type': 'model',
+ 'name': 'mail.template,subject',
+ 'module': 'mail',
+ 'lang': 'es_ES',
+ 'res_id': email_template.id,
+ 'value': 'Spanish Subject',
+ 'state': 'translated',
+ })
+ self.env['ir.translation'].create({
+ 'type': 'model',
+ 'name': 'mail.template,body_html',
+ 'module': 'mail',
+ 'lang': 'es_ES',
+ 'res_id': email_template.id,
+ 'value': '<p>Spanish Body</p>',
+ 'state': 'translated',
+ })
+ view = self.env['ir.ui.view'].create({
+ 'name': 'test_layout',
+ 'key': 'test_layout',
+ 'type': 'qweb',
+ 'arch_db': '<body><t t-raw="message.body"/> English Layout <t t-esc="model_description"/></body>'
+ })
+ self.env['ir.model.data'].create({
+ 'name': 'test_layout',
+ 'module': 'test_mail',
+ 'model': 'ir.ui.view',
+ 'res_id': view.id
+ })
+ self.env['ir.translation'].create({
+ 'type': 'model_terms',
+ 'name': 'ir.ui.view,arch_db',
+ 'module': 'test_mail',
+ 'lang': 'es_ES',
+ 'res_id': view.id,
+ 'src': 'English Layout',
+ 'value': 'Spanish Layout',
+ 'state': 'translated',
+ })
+
+ mail_id = email_template.send_mail(partner.id, notif_layout='test_mail.test_layout')
+ mail = self.env['mail.mail'].sudo().browse(mail_id)
+ self.assertEqual(mail.subject, 'Spanish Subject')
+ self.assertEqual(mail.body_html, '<body><p>Spanish Body</p> Spanish Layout Spanish description</body>')
+
+ def test_template_add_context_action(self):
+ self.email_template.create_action()
+
+ # check template act_window has been updated
+ self.assertTrue(bool(self.email_template.ref_ir_act_window))
+
+ # check those records
+ action = self.email_template.ref_ir_act_window
+ self.assertEqual(action.name, 'Send Mail (%s)' % self.email_template.name)
+ self.assertEqual(action.binding_model_id.model, 'mail.test.simple')
+
+ # def test_template_scheduled_date(self):
+ # from unittest.mock import patch
+
+ # self.email_template_in_2_days = self.email_template.copy()
+
+ # with patch('odoo.addons.mail.tests.test_mail_template.datetime', wraps=datetime) as mock_datetime:
+ # mock_datetime.now.return_value = datetime(2017, 11, 15, 11, 30, 28)
+ # mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw)
+
+ # self.email_template_in_2_days.write({
+ # 'scheduled_date': "${(datetime.datetime.now() + relativedelta(days=2)).strftime('%s')}" % DEFAULT_SERVER_DATETIME_FORMAT,
+ # })
+
+ # mail_now_id = self.email_template.send_mail(self.test_record.id)
+ # mail_in_2_days_id = self.email_template_in_2_days.send_mail(self.test_record.id)
+
+ # mail_now = self.env['mail.mail'].browse(mail_now_id)
+ # mail_in_2_days = self.env['mail.mail'].browse(mail_in_2_days_id)
+
+ # # mail preparation
+ # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_now | mail_in_2_days)
+ # self.assertEqual(bool(mail_now.scheduled_date), False)
+ # self.assertEqual(mail_now.state, 'outgoing')
+ # self.assertEqual(mail_in_2_days.state, 'outgoing')
+ # scheduled_date = datetime.strptime(mail_in_2_days.scheduled_date, DEFAULT_SERVER_DATETIME_FORMAT)
+ # date_in_2_days = datetime.now() + timedelta(days = 2)
+ # self.assertEqual(scheduled_date, date_in_2_days)
+ # # self.assertEqual(scheduled_date.month, date_in_2_days.month)
+ # # self.assertEqual(scheduled_date.year, date_in_2_days.year)
+
+ # # Launch the scheduler on the first mail, it should be reported in self.mails
+ # # and the mail_mail is now deleted
+ # self.env['mail.mail'].process_email_queue()
+ # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_in_2_days)
+
+ # # Launch the scheduler on the first mail, it's still in 'outgoing' state
+ # self.env['mail.mail'].process_email_queue(ids=[mail_in_2_days.id])
+ # self.assertEqual(mail_in_2_days.state, 'outgoing')
+ # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_in_2_days)
diff --git a/addons/test_mail/tests/test_mail_thread_internals.py b/addons/test_mail/tests/test_mail_thread_internals.py
new file mode 100644
index 00000000..afdf2522
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_thread_internals.py
@@ -0,0 +1,253 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from werkzeug.urls import url_parse, url_decode
+
+from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
+from odoo.tests.common import tagged, HttpCase
+from odoo.tools import mute_logger
+
+
+class TestChatterTweaks(TestMailCommon, TestRecipients):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestChatterTweaks, cls).setUpClass()
+ cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+
+ def test_post_no_subscribe_author(self):
+ original = self.test_record.message_follower_ids
+ self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True}).message_post(
+ body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment')
+ self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id'))
+ self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id'))
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_no_subscribe_recipients(self):
+ original = self.test_record.message_follower_ids
+ self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True}).message_post(
+ body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id])
+ self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id'))
+ self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id'))
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_subscribe_recipients(self):
+ original = self.test_record.message_follower_ids
+ self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True, 'mail_post_autofollow': True}).message_post(
+ body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id])
+ self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id') | self.partner_1 | self.partner_2)
+ self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id'))
+
+ def test_chatter_mail_create_nolog(self):
+ """ Test disable of automatic chatter message at create """
+ rec = self.env['mail.test.simple'].with_user(self.user_employee).with_context({'mail_create_nolog': True}).create({'name': 'Test'})
+ self.flush_tracking()
+ self.assertEqual(rec.message_ids, self.env['mail.message'])
+
+ rec = self.env['mail.test.simple'].with_user(self.user_employee).with_context({'mail_create_nolog': False}).create({'name': 'Test'})
+ self.flush_tracking()
+ self.assertEqual(len(rec.message_ids), 1)
+
+ def test_chatter_mail_notrack(self):
+ """ Test disable of automatic value tracking at create and write """
+ rec = self.env['mail.test.track'].with_user(self.user_employee).create({'name': 'Test', 'user_id': self.user_employee.id})
+ self.flush_tracking()
+ self.assertEqual(len(rec.message_ids), 1,
+ "A creation message without tracking values should have been posted")
+ self.assertEqual(len(rec.message_ids.sudo().tracking_value_ids), 0,
+ "A creation message without tracking values should have been posted")
+
+ rec.with_context({'mail_notrack': True}).write({'user_id': self.user_admin.id})
+ self.flush_tracking()
+ self.assertEqual(len(rec.message_ids), 1,
+ "No new message should have been posted with mail_notrack key")
+
+ rec.with_context({'mail_notrack': False}).write({'user_id': self.user_employee.id})
+ self.flush_tracking()
+ self.assertEqual(len(rec.message_ids), 2,
+ "A tracking message should have been posted")
+ self.assertEqual(len(rec.message_ids.sudo().mapped('tracking_value_ids')), 1,
+ "New tracking message should have tracking values")
+
+ def test_chatter_tracking_disable(self):
+ """ Test disable of all chatter features at create and write """
+ rec = self.env['mail.test.track'].with_user(self.user_employee).with_context({'tracking_disable': True}).create({'name': 'Test', 'user_id': self.user_employee.id})
+ self.flush_tracking()
+ self.assertEqual(rec.sudo().message_ids, self.env['mail.message'])
+ self.assertEqual(rec.sudo().mapped('message_ids.tracking_value_ids'), self.env['mail.tracking.value'])
+
+ rec.write({'user_id': self.user_admin.id})
+ self.flush_tracking()
+ self.assertEqual(rec.sudo().mapped('message_ids.tracking_value_ids'), self.env['mail.tracking.value'])
+
+ rec.with_context({'tracking_disable': False}).write({'user_id': self.user_employee.id})
+ self.flush_tracking()
+ self.assertEqual(len(rec.sudo().mapped('message_ids.tracking_value_ids')), 1)
+
+ rec = self.env['mail.test.track'].with_user(self.user_employee).with_context({'tracking_disable': False}).create({'name': 'Test', 'user_id': self.user_employee.id})
+ self.flush_tracking()
+ self.assertEqual(len(rec.sudo().message_ids), 1,
+ "Creation message without tracking values should have been posted")
+ self.assertEqual(len(rec.sudo().mapped('message_ids.tracking_value_ids')), 0,
+ "Creation message without tracking values should have been posted")
+
+ def test_cache_invalidation(self):
+ """ Test that creating a mail-thread record does not invalidate the whole cache. """
+ # make a new record in cache
+ record = self.env['res.partner'].new({'name': 'Brave New Partner'})
+ self.assertTrue(record.name)
+
+ # creating a mail-thread record should not invalidate the whole cache
+ self.env['res.partner'].create({'name': 'Actual Partner'})
+ self.assertTrue(record.name)
+
+
+class TestDiscuss(TestMailCommon, TestRecipients):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestDiscuss, cls).setUpClass()
+ cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+
+ def test_set_message_done_user(self):
+ with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], message_info={'content': 'Test'}):
+ message = self.test_record.message_post(
+ body='Test', message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.user_employee.partner_id.id])
+ message.with_user(self.user_employee).set_message_done()
+ self.assertMailNotifications(message, [{'notif': [{'partner': self.partner_employee, 'type': 'inbox', 'is_read': True}]}])
+ # TDE TODO: it seems bus notifications could be checked
+
+ def test_set_star(self):
+ msg = self.test_record.with_user(self.user_admin).message_post(body='My Body', subject='1')
+ msg_emp = self.env['mail.message'].with_user(self.user_employee).browse(msg.id)
+
+ # Admin set as starred
+ msg.toggle_message_starred()
+ self.assertTrue(msg.starred)
+
+ # Employee set as starred
+ msg_emp.toggle_message_starred()
+ self.assertTrue(msg_emp.starred)
+
+ # Do: Admin unstars msg
+ msg.toggle_message_starred()
+ self.assertFalse(msg.starred)
+ self.assertTrue(msg_emp.starred)
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_cc_recipient_suggestion(self):
+ record = self.env['mail.test.cc'].create({'email_cc': 'cc1@example.com, cc2@example.com, cc3 <cc3@example.com>'})
+ suggestions = record._message_get_suggested_recipients()[record.id]
+ self.assertEqual(sorted(suggestions), [
+ (False, '"cc3" <cc3@example.com>', 'CC Email'),
+ (False, 'cc1@example.com', 'CC Email'),
+ (False, 'cc2@example.com', 'CC Email'),
+ ], 'cc should be in suggestions')
+
+ def test_inbox_message_fetch_needaction(self):
+ user1 = self.env['res.users'].create({'login': 'user1', 'name': 'User 1'})
+ user1.notification_type = 'inbox'
+ user2 = self.env['res.users'].create({'login': 'user2', 'name': 'User 2'})
+ user2.notification_type = 'inbox'
+ message1 = self.test_record.with_user(self.user_admin).message_post(body='Message 1', partner_ids=[user1.partner_id.id, user2.partner_id.id])
+ message2 = self.test_record.with_user(self.user_admin).message_post(body='Message 2', partner_ids=[user1.partner_id.id, user2.partner_id.id])
+
+ # both notified users should have the 2 messages in Inbox initially
+ messages = self.env['mail.message'].with_user(user1).message_fetch(domain=[['needaction', '=', True]])
+ self.assertEqual(len(messages), 2)
+ messages = self.env['mail.message'].with_user(user2).message_fetch(domain=[['needaction', '=', True]])
+ self.assertEqual(len(messages), 2)
+
+ # first user is marking one message as done: the other message is still Inbox, while the other user still has the 2 messages in Inbox
+ message1.with_user(user1).set_message_done()
+ messages = self.env['mail.message'].with_user(user1).message_fetch(domain=[['needaction', '=', True]])
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0].get('id'), message2.id)
+ messages = self.env['mail.message'].with_user(user2).message_fetch(domain=[['needaction', '=', True]])
+ self.assertEqual(len(messages), 2)
+
+ def test_notification_has_error_filter(self):
+ """Ensure message_has_error filter is only returning threads for which
+ the current user is author of a failed message."""
+ message = self.test_record.with_user(self.user_admin).message_post(
+ body='Test', message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.user_employee.partner_id.id]
+ )
+ self.assertFalse(message.has_error)
+ with self.mock_mail_gateway(sim_error='connect_smtp_notfound'):
+ self.user_admin.notification_type = 'email'
+ message2 = self.test_record.with_user(self.user_employee).message_post(
+ body='Test', message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.user_admin.partner_id.id]
+ )
+ self.assertTrue(message2.has_error)
+ # employee is author of message which has a failure
+ threads_employee = self.test_record.with_user(self.user_employee).search([('message_has_error', '=', True)])
+ self.assertEqual(len(threads_employee), 1)
+ # admin is also author of a message, but it doesn't have a failure
+ # and the failure from employee's message should not be taken into account for admin
+ threads_admin = self.test_record.with_user(self.user_admin).search([('message_has_error', '=', True)])
+ self.assertEqual(len(threads_admin), 0)
+
+
+@tagged('-at_install', 'post_install')
+class TestMultiCompany(HttpCase):
+
+ def test_redirect_to_records(self):
+
+ self.company_A = self.env['res.company'].create({
+ 'name': 'Company A',
+ 'user_ids': [(4, self.ref('base.user_admin'))],
+ })
+
+ self.company_B = self.env['res.company'].create({
+ 'name': 'Company B',
+ })
+
+ self.multi_company_record = self.env['mail.test.multi.company'].create({
+ 'name': 'Multi Company Record',
+ 'company_id': self.company_A.id,
+ })
+
+ # Test Case 0
+ # Not logged, redirect to web/login
+ response = self.url_open('/mail/view?model=%s&res_id=%s' % (
+ self.multi_company_record._name,
+ self.multi_company_record.id), timeout=15)
+
+ path = url_parse(response.url).path
+ self.assertEqual(path, '/web/login')
+
+ self.authenticate('admin', 'admin')
+
+ # Test Case 1
+ # Logged into company 1, try accessing record in company A
+ # _redirect_to_record should add company A in allowed_company_ids
+ response = self.url_open('/mail/view?model=%s&res_id=%s' % (
+ self.multi_company_record._name,
+ self.multi_company_record.id), timeout=15)
+
+ self.assertEqual(response.status_code, 200)
+
+ fragment = url_parse(response.url).fragment
+ cids = url_decode(fragment)['cids']
+
+ self.assertEqual(cids, '1,%s' % (self.company_A.id))
+
+ # Test Case 2
+ # Logged into company 1, try accessing record in company B
+ # _redirect_to_record should redirect to messaging as the user
+ # doesn't have any access for this company
+ self.multi_company_record.company_id = self.company_B
+
+ response = self.url_open('/mail/view?model=%s&res_id=%s' % (
+ self.multi_company_record._name,
+ self.multi_company_record.id), timeout=15)
+
+ self.assertEqual(response.status_code, 200)
+
+ fragment = url_parse(response.url).fragment
+ action = url_decode(fragment)['action']
+
+ self.assertEqual(action, 'mail.action_discuss')
diff --git a/addons/test_mail/tests/test_mail_tools.py b/addons/test_mail/tests/test_mail_tools.py
new file mode 100644
index 00000000..b36a1335
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_tools.py
@@ -0,0 +1,83 @@
+# -*- 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.tests import tagged, users
+
+
+@tagged('mail_tools')
+class TestMailTools(TestMailCommon, TestRecipients):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailTools, cls).setUpClass()
+
+ cls._test_email = 'alfredoastaire@test.example.com'
+ cls.test_partner = cls.env['res.partner'].create({
+ 'country_id': cls.env.ref('base.be').id,
+ 'email': cls._test_email,
+ 'mobile': '0456001122',
+ 'name': 'Alfred Astaire',
+ 'phone': '0456334455',
+ })
+
+ @users('employee')
+ def test_find_partner_from_emails(self):
+ Partner = self.env['res.partner']
+ test_partner = Partner.browse(self.test_partner.ids)
+ self.assertEqual(test_partner.email, self._test_email)
+
+ # test direct match
+ found = Partner._mail_find_partner_from_emails([self._test_email])
+ self.assertEqual(found, [test_partner])
+
+ # test encapsulated email
+ found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email])
+ self.assertEqual(found, [test_partner])
+
+ # test with wildcard "_"
+ found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com'])
+ self.assertEqual(found, [self.env['res.partner']])
+
+ # sub-check: this search does not consider _ as a wildcard
+ found = Partner._mail_search_on_partner(['alfred_astaire@test.example.com'])
+ self.assertEqual(found, self.env['res.partner'])
+
+ # test partners with encapsulated emails
+ # ------------------------------------------------------------
+ test_partner.sudo().write({'email': '"Alfred Mighty Power Astaire" <%s>' % self._test_email})
+
+ # test direct match
+ found = Partner._mail_find_partner_from_emails([self._test_email])
+ self.assertEqual(found, [test_partner])
+
+ # test encapsulated email
+ found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email])
+ self.assertEqual(found, [test_partner])
+
+ # test with wildcard "_"
+ found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com'])
+ self.assertEqual(found, [self.env['res.partner']])
+
+ # sub-check: this search does not consider _ as a wildcard
+ found = Partner._mail_search_on_partner(['alfred_astaire@test.example.com'])
+ self.assertEqual(found, self.env['res.partner'])
+
+ # test partners with look-alike emails
+ # ------------------------------------------------------------
+ for email_lookalike in [
+ 'alfred.astaire@test.example.com',
+ 'alfredoastaire@example.com',
+ 'aalfredoastaire@test.example.com',
+ 'alfredoastaire@test.example.comm']:
+ test_partner.sudo().write({'email': '"Alfred Astaire" <%s>' % email_lookalike})
+
+ # test direct match
+ found = Partner._mail_find_partner_from_emails([self._test_email])
+ self.assertEqual(found, [self.env['res.partner']])
+ # test encapsulated email
+ found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email])
+ self.assertEqual(found, [self.env['res.partner']])
+ # test with wildcard "_"
+ found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com'])
+ self.assertEqual(found, [self.env['res.partner']])
diff --git a/addons/test_mail/tests/test_message_management.py b/addons/test_mail/tests/test_message_management.py
new file mode 100644
index 00000000..289b3681
--- /dev/null
+++ b/addons/test_mail/tests/test_message_management.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+# 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.test_mail.tests.common import TestMailCommon
+from odoo.tests import tagged
+from odoo.tools import mute_logger
+
+
+@tagged('mail_wizards')
+class TestMailResend(TestMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailResend, cls).setUpClass()
+ cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+
+ #Two users
+ cls.user1 = mail_new_test_user(cls.env, login='e1', groups='base.group_public', name='Employee 1', notification_type='email', email='e1') # invalid email
+ cls.user2 = mail_new_test_user(cls.env, login='e2', groups='base.group_portal', name='Employee 2', notification_type='email', email='e2@example.com')
+ #Two partner
+ cls.partner1 = cls.env['res.partner'].with_context(cls._test_context).create({
+ 'name': 'Partner 1',
+ 'email': 'p1' # invalid email
+ })
+ cls.partner2 = cls.env['res.partner'].with_context(cls._test_context).create({
+ 'name': 'Partner 2',
+ 'email': 'p2@example.com'
+ })
+ cls.partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.user2.partner_id, cls.partner1, cls.partner2)
+ cls.invalid_email_partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.partner1)
+
+ # @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_resend_workflow(self):
+ with self.assertSinglePostNotifications(
+ [{'partner': partner, 'type': 'email', 'status': 'exception'} for partner in self.partners],
+ message_info={'message_type': 'notification'},
+ sim_error='connect_failure'):
+ message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification')
+
+ wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
+ self.assertEqual(wizard.notification_ids.mapped('res_partner_id'), self.partners, "wizard should manage notifications for each failed partner")
+
+ # three more failure sent on bus, one for each mail in failure and one for resend
+ self._reset_bus()
+ with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 3):
+ wizard.resend_mail_action()
+ done_msgs, done_notifs = self.assertMailNotifications(message, [
+ {'content': '', 'message_type': 'notification',
+ 'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}]
+ )
+ self.assertEqual(wizard.notification_ids, done_notifs)
+ self.assertEqual(done_msgs, message)
+
+ self.user1.write({"email": 'u1@example.com'})
+
+ # two more failure update sent on bus, one for failed mail and one for resend
+ self._reset_bus()
+ with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 2):
+ self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action()
+ done_msgs, done_notifs = self.assertMailNotifications(message, [
+ {'content': '', 'message_type': 'notification',
+ 'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner == self.partner1 else 'sent', 'check_send': partner == self.partner1} for partner in self.partners]}]
+ )
+ self.assertEqual(wizard.notification_ids, done_notifs)
+ self.assertEqual(done_msgs, message)
+
+ self.partner1.write({"email": 'p1@example.com'})
+
+ # A success update should be sent on bus once the email has no more failure
+ self._reset_bus()
+ with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)]):
+ self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action()
+ self.assertMailNotifications(message, [
+ {'content': '', 'message_type': 'notification',
+ 'notif': [{'partner': partner, 'type': 'email', 'status': 'sent', 'check_send': partner == self.partner1} for partner in self.partners]}]
+ )
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_remove_mail_become_canceled(self):
+ # two failure sent on bus, one for each mail
+ self._reset_bus()
+ with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 2):
+ message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification')
+
+ self.assertMailNotifications(message, [
+ {'content': '', 'message_type': 'notification',
+ 'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}]
+ )
+
+ wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
+ partners = wizard.partner_ids.mapped("partner_id")
+ self.assertEqual(self.invalid_email_partners, partners)
+ wizard.partner_ids.filtered(lambda p: p.partner_id == self.partner1).write({"resend": False})
+ wizard.resend_mail_action()
+
+ self.assertMailNotifications(message, [
+ {'content': '', 'message_type': 'notification',
+ 'notif': [{'partner': partner, 'type': 'email',
+ 'status': (partner == self.user1.partner_id and 'exception') or (partner == self.partner1 and 'canceled') or 'sent'} for partner in self.partners]}]
+ )
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_cancel_all(self):
+ self._reset_bus()
+ with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 2):
+ message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification')
+
+ wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
+ # one update for cancell
+ self._reset_bus()
+ with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 1):
+ wizard.cancel_mail_action()
+
+ self.assertMailNotifications(message, [
+ {'content': '', 'message_type': 'notification',
+ 'notif': [{'partner': partner, 'type': 'email',
+ 'check_send': partner in self.user1.partner_id | self.partner1,
+ 'status': 'canceled' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}]
+ )
diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py
new file mode 100644
index 00000000..edf052d7
--- /dev/null
+++ b/addons/test_mail/tests/test_message_post.py
@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+
+from unittest.mock import patch
+
+from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE_PLAINTEXT
+from odoo.addons.test_mail.models.test_mail_models import MailTestSimple
+from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
+from odoo.api import call_kw
+from odoo.exceptions import AccessError
+from odoo.tests import tagged
+from odoo.tools import mute_logger, formataddr
+from odoo.tests.common import users
+
+
+@tagged('mail_post')
+class TestMessagePost(TestMailCommon, TestRecipients):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMessagePost, cls).setUpClass()
+ cls._create_portal_user()
+ cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+ cls._reset_mail_context(cls.test_record)
+ cls.user_admin.write({'notification_type': 'email'})
+
+ # This method should be run inside a post_install class to ensure that all
+ # message_post overrides are tested.
+ def test_message_post_return(self):
+ test_channel = self.env['mail.channel'].create({
+ 'name': 'Test',
+ })
+ # Use call_kw as shortcut to simulate a RPC call.
+ messageId = call_kw(self.env['mail.channel'], 'message_post', [test_channel.id], {'body': 'test'})
+ self.assertTrue(isinstance(messageId, int))
+
+ @users('employee')
+ def test_notify_prepare_template_context_company_value(self):
+ """ Verify that the template context company value is right
+ after switching the env company or if a company_id is set
+ on mail record.
+ """
+ current_user = self.env.user
+ main_company = current_user.company_id
+ other_company = self.env['res.company'].with_user(self.user_admin).create({'name': 'Company B'})
+ current_user.sudo().write({'company_ids': [(4, other_company.id)]})
+ test_record = self.env['mail.test.multi.company'].with_user(self.user_admin).create({
+ 'name': 'Multi Company Record',
+ 'company_id': False,
+ })
+
+ # self.env.company.id = Main Company AND test_record.company_id = False
+ self.assertEqual(self.env.company.id, main_company.id)
+ self.assertEqual(test_record.company_id.id, False)
+ template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
+ self.assertEqual(template_values.get('company').id, self.env.company.id)
+
+ # self.env.company.id = Other Company AND test_record.company_id = False
+ current_user.company_id = other_company
+ test_record = self.env['mail.test.multi.company'].browse(test_record.id)
+ self.assertEqual(self.env.company.id, other_company.id)
+ self.assertEqual(test_record.company_id.id, False)
+ template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
+ self.assertEqual(template_values.get('company').id, self.env.company.id)
+
+ # self.env.company.id = Other Company AND test_record.company_id = Main Company
+ test_record.company_id = main_company
+ test_record = self.env['mail.test.multi.company'].browse(test_record.id)
+ self.assertEqual(self.env.company.id, other_company.id)
+ self.assertEqual(test_record.company_id.id, main_company.id)
+ template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
+ self.assertEqual(template_values.get('company').id, main_company.id)
+
+ def test_notify_recipients_internals(self):
+ pdata = self._generate_notify_recipients(self.partner_1 | self.partner_employee)
+ msg_vals = {
+ 'body': 'Message body',
+ 'model': self.test_record._name,
+ 'res_id': self.test_record.id,
+ 'subject': 'Message subject',
+ }
+ link_vals = {
+ 'token': 'token_val',
+ 'access_token': 'access_token_val',
+ 'auth_signup_token': 'auth_signup_token_val',
+ 'auth_login': 'auth_login_val',
+ }
+ notify_msg_vals = dict(msg_vals, **link_vals)
+ classify_res = self.env[self.test_record._name]._notify_classify_recipients(pdata, 'My Custom Model Name', msg_vals=notify_msg_vals)
+ # find back information for each recipients
+ partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids)
+ emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids)
+
+ # partner: no access button
+ self.assertFalse(partner_info['has_button_access'])
+
+ # employee: access button and link
+ self.assertTrue(emp_info['has_button_access'])
+ for param, value in link_vals.items():
+ self.assertIn('%s=%s' % (param, value), emp_info['button_access']['url'])
+ self.assertIn('model=%s' % self.test_record._name, emp_info['button_access']['url'])
+ self.assertIn('res_id=%s' % self.test_record.id, emp_info['button_access']['url'])
+ self.assertNotIn('body', emp_info['button_access']['url'])
+ self.assertNotIn('subject', emp_info['button_access']['url'])
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_needaction(self):
+ (self.user_employee | self.user_admin).write({'notification_type': 'inbox'})
+ with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], {'content': 'Body'}):
+ self.test_record.message_post(
+ body='Body', message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.user_employee.partner_id.id])
+
+ self.test_record.message_subscribe([self.partner_1.id])
+ with self.assertSinglePostNotifications([
+ {'partner': self.partner_employee, 'type': 'inbox'},
+ {'partner': self.partner_1, 'type': 'email'}], {'content': 'NewBody'}):
+ self.test_record.message_post(
+ body='NewBody', message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.user_employee.partner_id.id])
+
+ with self.assertSinglePostNotifications([
+ {'partner': self.partner_1, 'type': 'email'},
+ {'partner': self.partner_portal, 'type': 'email'}], {'content': 'ToPortal'}):
+ self.test_record.message_post(
+ body='ToPortal', message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.partner_portal.id])
+
+ def test_post_inactive_follower(self):
+ # In some case odoobot is follower of a record.
+ # Even if it shouldn't be the case, we want to be sure that odoobot is not notified
+ (self.user_employee | self.user_admin).write({'notification_type': 'inbox'})
+ self.test_record._message_subscribe(self.user_employee.partner_id.ids)
+ with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], {'content': 'Test'}):
+ self.test_record.message_post(
+ body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
+
+ self.user_employee.active = False
+ # at this point, partner is still active and would receive an email notification
+ self.user_employee.partner_id._write({'active': False})
+ with self.assertPostNotifications([{'content': 'Test', 'notif': []}]):
+ self.test_record.message_post(
+ body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_notifications(self):
+ _body, _subject = '<p>Test Body</p>', 'Test Subject'
+
+ # subscribe second employee to the group to test notifications
+ self.test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id])
+
+ with self.assertSinglePostNotifications(
+ [{'partner': self.partner_1, 'type': 'email'},
+ {'partner': self.partner_2, 'type': 'email'},
+ {'partner': self.partner_admin, 'type': 'email'}],
+ {'content': _body},
+ mail_unlink_sent=True):
+ msg = self.test_record.with_user(self.user_employee).message_post(
+ body=_body, subject=_subject,
+ message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.partner_1.id, self.partner_2.id]
+ )
+
+ # message content
+ self.assertEqual(msg.subject, _subject)
+ self.assertEqual(msg.body, _body)
+ self.assertEqual(msg.partner_ids, self.partner_1 | self.partner_2)
+ self.assertEqual(msg.notified_partner_ids, self.user_admin.partner_id | self.partner_1 | self.partner_2)
+ self.assertEqual(msg.channel_ids, self.env['mail.channel'])
+
+ # notifications emails should have been deleted
+ self.assertFalse(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)]),
+ 'message_post: mail.mail notifications should have been auto-deleted')
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_notifications_keep_emails(self):
+ self.test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id])
+
+ msg = self.test_record.with_user(self.user_employee).message_post(
+ body='Test', subject='Test',
+ message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.partner_1.id, self.partner_2.id],
+ mail_auto_delete=False
+ )
+
+ # notifications emails should not have been deleted: one for customers, one for user
+ self.assertEqual(len(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)])), 2)
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_notifications_emails_tweak(self):
+ pass
+ # we should check _notification_groups behavior, for emails and buttons
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_attachments(self):
+ _attachments = [
+ ('List1', b'My first attachment'),
+ ('List2', b'My second attachment')
+ ]
+ _attach_1 = self.env['ir.attachment'].with_user(self.user_employee).create({
+ 'name': 'Attach1',
+ 'datas': 'bWlncmF0aW9uIHRlc3Q=',
+ 'res_model': 'mail.compose.message', 'res_id': 0})
+ _attach_2 = self.env['ir.attachment'].with_user(self.user_employee).create({
+ 'name': 'Attach2',
+ 'datas': 'bWlncmF0aW9uIHRlc3Q=',
+ 'res_model': 'mail.compose.message', 'res_id': 0})
+
+ with self.mock_mail_gateway():
+ msg = self.test_record.with_user(self.user_employee).message_post(
+ body='Test', subject='Test',
+ message_type='comment', subtype_xmlid='mail.mt_comment',
+ attachment_ids=[_attach_1.id, _attach_2.id],
+ partner_ids=[self.partner_1.id],
+ attachments=_attachments,
+ )
+
+ # message attachments
+ self.assertEqual(len(msg.attachment_ids), 4)
+ self.assertEqual(set(msg.attachment_ids.mapped('res_model')), set([self.test_record._name]))
+ self.assertEqual(set(msg.attachment_ids.mapped('res_id')), set([self.test_record.id]))
+ self.assertEqual(set([base64.b64decode(x) for x in msg.attachment_ids.mapped('datas')]),
+ set([b'migration test', _attachments[0][1], _attachments[1][1]]))
+ self.assertTrue(set([_attach_1.id, _attach_2.id]).issubset(msg.attachment_ids.ids),
+ 'message_post: mail.message attachments duplicated')
+
+ # notification email attachments
+ self.assertSentEmail(self.user_employee.partner_id, [self.partner_1])
+ # self.assertEqual(len(self._mails), 1)
+ self.assertEqual(len(self._mails[0]['attachments']), 4)
+ self.assertIn(('List1', b'My first attachment', 'application/octet-stream'), self._mails[0]['attachments'])
+ self.assertIn(('List2', b'My second attachment', 'application/octet-stream'), self._mails[0]['attachments'])
+ self.assertIn(('Attach1', b'migration test', 'application/octet-stream'), self._mails[0]['attachments'])
+ self.assertIn(('Attach2', b'migration test', 'application/octet-stream'), self._mails[0]['attachments'])
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_answer(self):
+ with self.mock_mail_gateway():
+ parent_msg = self.test_record.with_user(self.user_employee).message_post(
+ body='<p>Test</p>', subject='Test Subject',
+ message_type='comment', subtype_xmlid='mail.mt_comment')
+
+ self.assertEqual(parent_msg.partner_ids, self.env['res.partner'])
+ self.assertNotSentEmail()
+
+ with self.assertPostNotifications([{'content': '<p>Test Answer</p>', 'notif': [{'partner': self.partner_1, 'type': 'email'}]}]):
+ msg = self.test_record.with_user(self.user_employee).message_post(
+ body='<p>Test Answer</p>',
+ message_type='comment', subtype_xmlid='mail.mt_comment',
+ partner_ids=[self.partner_1.id],
+ parent_id=parent_msg.id)
+
+ self.assertEqual(msg.parent_id.id, parent_msg.id)
+ self.assertEqual(msg.partner_ids, self.partner_1)
+ self.assertEqual(parent_msg.partner_ids, self.env['res.partner'])
+
+ # check notification emails: references
+ self.assertSentEmail(self.user_employee.partner_id, [self.partner_1], ref_content='openerp-%d-mail.test.simple' % self.test_record.id)
+ # self.assertTrue(all('openerp-%d-mail.test.simple' % self.test_record.id in m['references'] for m in self._mails))
+
+ new_msg = self.test_record.with_user(self.user_employee).message_post(
+ body='<p>Test Answer Bis</p>',
+ message_type='comment', subtype_xmlid='mail.mt_comment',
+ parent_id=msg.id)
+
+ self.assertEqual(new_msg.parent_id.id, parent_msg.id, 'message_post: flatten error')
+ self.assertEqual(new_msg.partner_ids, self.env['res.partner'])
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_portal_ok(self):
+ self.test_record.message_subscribe((self.partner_1 | self.user_employee.partner_id).ids)
+
+ with self.assertPostNotifications([{'content': '<p>Test</p>', 'notif': [
+ {'partner': self.partner_employee, 'type': 'inbox'},
+ {'partner': self.partner_1, 'type': 'email'}]}
+ ]), patch.object(MailTestSimple, 'check_access_rights', return_value=True):
+ new_msg = self.test_record.with_user(self.user_portal).message_post(
+ body='<p>Test</p>', subject='Subject',
+ message_type='comment', subtype_xmlid='mail.mt_comment')
+
+ self.assertEqual(new_msg.sudo().notified_partner_ids, (self.partner_1 | self.user_employee.partner_id))
+
+ def test_post_portal_crash(self):
+ with self.assertRaises(AccessError):
+ self.test_record.with_user(self.user_portal).message_post(
+ body='<p>Test</p>', subject='Subject',
+ message_type='comment', subtype_xmlid='mail.mt_comment')
+
+ @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
+ def test_post_internal(self):
+ self.test_record.message_subscribe([self.user_admin.partner_id.id])
+ msg = self.test_record.with_user(self.user_employee).message_post(
+ body='My Body', subject='My Subject',
+ message_type='comment', subtype_xmlid='mail.mt_note')
+ self.assertEqual(msg.partner_ids, self.env['res.partner'])
+ self.assertEqual(msg.notified_partner_ids, self.env['res.partner'])
+
+ self.format_and_process(
+ MAIL_TEMPLATE_PLAINTEXT, self.user_admin.email, 'not_my_businesss@example.com',
+ msg_id='<1198923581.41972151344608186800.JavaMail.diff1@agrolait.com>',
+ extra='In-Reply-To:\r\n\t%s\n' % msg.message_id,
+ target_model='mail.test.simple')
+ reply = self.test_record.message_ids - msg
+ self.assertTrue(reply)
+ self.assertEqual(reply.subtype_id, self.env.ref('mail.mt_note'))
+ self.assertEqual(reply.notified_partner_ids, self.user_employee.partner_id)
+ self.assertEqual(reply.parent_id, msg)
+
+ def test_post_log(self):
+ new_note = self.test_record.with_user(self.user_employee)._message_log(
+ body='<p>Labrador</p>',
+ )
+
+ self.assertEqual(new_note.subtype_id, self.env.ref('mail.mt_note'))
+ self.assertEqual(new_note.body, '<p>Labrador</p>')
+ self.assertEqual(new_note.author_id, self.user_employee.partner_id)
+ self.assertEqual(new_note.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
+ self.assertEqual(new_note.notified_partner_ids, self.env['res.partner'])
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_notify(self):
+ self.user_employee.write({'notification_type': 'inbox'})
+
+ with self.mock_mail_gateway():
+ new_notification = self.test_record.message_notify(
+ subject='This should be a subject',
+ body='<p>You have received a notification</p>',
+ partner_ids=[self.partner_1.id, self.partner_admin.id, self.user_employee.partner_id.id],
+ )
+
+ self.assertEqual(new_notification.subtype_id, self.env.ref('mail.mt_note'))
+ self.assertEqual(new_notification.message_type, 'user_notification')
+ self.assertEqual(new_notification.body, '<p>You have received a notification</p>')
+ self.assertEqual(new_notification.author_id, self.env.user.partner_id)
+ self.assertEqual(new_notification.email_from, formataddr((self.env.user.name, self.env.user.email)))
+ self.assertEqual(new_notification.notified_partner_ids, self.partner_1 | self.user_employee.partner_id | self.partner_admin)
+ self.assertNotIn(new_notification, self.test_record.message_ids)
+
+ admin_mails = [x for x in self._mails if self.partner_admin.name in x.get('email_to')[0]]
+ self.assertEqual(len(admin_mails), 1, 'There should be exactly one email sent to admin')
+ admin_mail = admin_mails[0].get('body')
+ admin_access_link = admin_mail[admin_mail.index('model='):admin_mail.index('/>') - 1] if 'model=' in admin_mail else None
+
+ self.assertIsNotNone(admin_access_link, 'The email sent to admin should contain an access link')
+ self.assertIn('model=%s' % self.test_record._name, admin_access_link, 'The access link should contain a valid model argument')
+ self.assertIn('res_id=%d' % self.test_record.id, admin_access_link, 'The access link should contain a valid res_id argument')
+
+ partner_mails = [x for x in self._mails if self.partner_1.name in x.get('email_to')[0]]
+ self.assertEqual(len(partner_mails), 1, 'There should be exactly one email sent to partner')
+ partner_mail = partner_mails[0].get('body')
+ self.assertNotIn('/mail/view?model=', partner_mail, 'The email sent to admin should not contain an access link')
+ # todo xdo add test message_notify on thread with followers and stuff
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_post_post_w_template(self):
+ test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
+ self.user_employee.write({
+ 'groups_id': [(4, self.env.ref('base.group_partner_manager').id)],
+ })
+ _attachments = [{
+ 'name': 'first.txt',
+ 'datas': base64.b64encode(b'My first attachment'),
+ 'res_model': 'res.partner',
+ 'res_id': self.user_admin.partner_id.id
+ }, {
+ 'name': 'second.txt',
+ 'datas': base64.b64encode(b'My second attachment'),
+ 'res_model': 'res.partner',
+ 'res_id': self.user_admin.partner_id.id
+ }]
+ email_1 = 'test1@example.com'
+ email_2 = 'test2@example.com'
+ email_3 = self.partner_1.email
+ self._create_template('mail.test.simple', {
+ 'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])],
+ 'partner_to': '%s,%s' % (self.partner_2.id, self.user_admin.partner_id.id),
+ 'email_to': '%s, %s' % (email_1, email_2),
+ 'email_cc': '%s' % email_3,
+ })
+ # admin should receive emails
+ self.user_admin.write({'notification_type': 'email'})
+ # Force the attachments of the template to be in the natural order.
+ self.email_template.invalidate_cache(['attachment_ids'], ids=self.email_template.ids)
+
+ with self.mock_mail_gateway():
+ test_record.with_user(self.user_employee).message_post_with_template(self.email_template.id, composition_mode='comment')
+
+ new_partners = self.env['res.partner'].search([('email', 'in', [email_1, email_2])])
+ for r in [self.partner_1, self.partner_2, new_partners[0], new_partners[1], self.partner_admin]:
+ self.assertSentEmail(
+ self.user_employee.partner_id,
+ [r],
+ subject='About %s' % test_record.name,
+ body_content=test_record.name,
+ attachments=[('first.txt', b'My first attachment', 'text/plain'), ('second.txt', b'My second attachment', 'text/plain')])
diff --git a/addons/test_mail/tests/test_message_track.py b/addons/test_mail/tests/test_message_track.py
new file mode 100644
index 00000000..df024c1e
--- /dev/null
+++ b/addons/test_mail/tests/test_message_track.py
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from unittest.mock import patch
+
+from odoo.addons.test_mail.tests.common import TestMailCommon
+from odoo.tests.common import tagged
+from odoo.tests import Form
+
+
+@tagged('mail_track')
+class TestTracking(TestMailCommon):
+
+ def setUp(self):
+ super(TestTracking, self).setUp()
+
+ record = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context).create({
+ 'name': 'Test',
+ })
+ self.flush_tracking()
+ self.record = record.with_context(mail_notrack=False)
+
+ def test_message_track_no_tracking(self):
+ """ Update a set of non tracked fields -> no message, no tracking """
+ self.record.write({
+ 'name': 'Tracking or not',
+ 'count': 32,
+ })
+ self.flush_tracking()
+ self.assertEqual(self.record.message_ids, self.env['mail.message'])
+
+ def test_message_track_no_subtype(self):
+ """ Update some tracked fields not linked to some subtype -> message with onchange """
+ customer = self.env['res.partner'].create({'name': 'Customer', 'email': 'cust@example.com'})
+ with self.mock_mail_gateway():
+ self.record.write({
+ 'name': 'Test2',
+ 'customer_id': customer.id,
+ })
+ self.flush_tracking()
+
+ # one new message containing tracking; without subtype linked to tracking, a note is generated
+ self.assertEqual(len(self.record.message_ids), 1)
+ self.assertEqual(self.record.message_ids.subtype_id, self.env.ref('mail.mt_note'))
+
+ # no specific recipients except those following notes, no email
+ self.assertEqual(self.record.message_ids.partner_ids, self.env['res.partner'])
+ self.assertEqual(self.record.message_ids.notified_partner_ids, self.env['res.partner'])
+ self.assertNotSentEmail()
+
+ # verify tracked value
+ self.assertTracking(
+ self.record.message_ids,
+ [('customer_id', 'many2one', False, customer) # onchange tracked field
+ ])
+
+ def test_message_track_subtype(self):
+ """ Update some tracked fields linked to some subtype -> message with onchange """
+ self.record.message_subscribe(
+ partner_ids=[self.user_admin.partner_id.id],
+ subtype_ids=[self.env.ref('test_mail.st_mail_test_ticket_container_upd').id]
+ )
+
+ container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'})
+ self.record.write({
+ 'name': 'Test2',
+ 'email_from': 'noone@example.com',
+ 'container_id': container.id,
+ })
+ self.flush_tracking()
+ # one new message containing tracking; subtype linked to tracking
+ self.assertEqual(len(self.record.message_ids), 1)
+ self.assertEqual(self.record.message_ids.subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd'))
+
+ # no specific recipients except those following container
+ self.assertEqual(self.record.message_ids.partner_ids, self.env['res.partner'])
+ self.assertEqual(self.record.message_ids.notified_partner_ids, self.user_admin.partner_id)
+
+ # verify tracked value
+ self.assertTracking(
+ self.record.message_ids,
+ [('container_id', 'many2one', False, container) # onchange tracked field
+ ])
+
+ def test_message_track_template(self):
+ """ Update some tracked fields linked to some template -> message with onchange """
+ self.record.write({'mail_template': self.env.ref('test_mail.mail_test_ticket_tracking_tpl').id})
+ self.assertEqual(self.record.message_ids, self.env['mail.message'])
+
+ with self.mock_mail_gateway():
+ self.record.write({
+ 'name': 'Test2',
+ 'customer_id': self.user_admin.partner_id.id,
+ })
+ self.flush_tracking()
+
+ self.assertEqual(len(self.record.message_ids), 2, 'should have 2 new messages: one for tracking, one for template')
+
+ # one new message containing the template linked to tracking
+ self.assertEqual(self.record.message_ids[0].subject, 'Test Template')
+ self.assertEqual(self.record.message_ids[0].body, '<p>Hello Test2</p>')
+
+ # one email send due to template
+ self.assertSentEmail(self.record.env.user.partner_id, [self.partner_admin], body='<p>Hello Test2</p>')
+
+ # one new message containing tracking; without subtype linked to tracking
+ self.assertEqual(self.record.message_ids[1].subtype_id, self.env.ref('mail.mt_note'))
+ self.assertTracking(
+ self.record.message_ids[1],
+ [('customer_id', 'many2one', False, self.user_admin.partner_id) # onchange tracked field
+ ])
+
+ def test_message_track_template_at_create(self):
+ """ Create a record with tracking template on create, template should be sent."""
+
+ Model = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context)
+ Model = Model.with_context(mail_notrack=False)
+ with self.mock_mail_gateway():
+ record = Model.create({
+ 'name': 'Test',
+ 'customer_id': self.user_admin.partner_id.id,
+ 'mail_template': self.env.ref('test_mail.mail_test_ticket_tracking_tpl').id,
+ })
+ self.flush_tracking()
+
+ self.assertEqual(len(record.message_ids), 1, 'should have 1 new messages for template')
+ # one new message containing the template linked to tracking
+ self.assertEqual(record.message_ids[0].subject, 'Test Template')
+ self.assertEqual(record.message_ids[0].body, '<p>Hello Test</p>')
+ # one email send due to template
+ self.assertSentEmail(self.record.env.user.partner_id, [self.partner_admin], body='<p>Hello Test</p>')
+
+ def test_create_partner_from_tracking_multicompany(self):
+ company1 = self.env['res.company'].create({'name': 'company1'})
+ self.env.user.write({'company_ids': [(4, company1.id, False)]})
+ self.assertNotEqual(self.env.company, company1)
+
+ email_new_partner = "diamonds@rust.com"
+ Partner = self.env['res.partner']
+ self.assertFalse(Partner.search([('email', '=', email_new_partner)]))
+
+ template = self.env['mail.template'].create({
+ 'model_id': self.env['ir.model']._get('mail.test.track').id,
+ 'name': 'AutoTemplate',
+ 'subject': 'autoresponse',
+ 'email_from': self.env.user.email_formatted,
+ 'email_to': "${object.email_from}",
+ 'body_html': "<div>A nice body</div>",
+ })
+
+ def patched_message_track_post_template(*args, **kwargs):
+ if args[0]._name == "mail.test.track":
+ args[0].message_post_with_template(template.id)
+ return True
+
+ with patch('odoo.addons.mail.models.mail_thread.MailThread._message_track_post_template', patched_message_track_post_template):
+ self.env['mail.test.track'].create({
+ 'email_from': email_new_partner,
+ 'company_id': company1.id,
+ 'user_id': self.env.user.id, # trigger track template
+ })
+ self.flush_tracking()
+
+ new_partner = Partner.search([('email', '=', email_new_partner)])
+ self.assertTrue(new_partner)
+ self.assertEqual(new_partner.company_id, company1)
+
+ def test_track_template(self):
+ # Test: Check that default_* keys are not taken into account in _message_track_post_template
+ magic_code = 'Up-Up-Down-Down-Left-Right-Left-Right-Square-Triangle'
+
+ mt_name_changed = self.env['mail.message.subtype'].create({
+ 'name': 'MAGIC CODE WOOP WOOP',
+ 'description': 'SPECIAL CONTENT UNLOCKED'
+ })
+ self.env['ir.model.data'].create({
+ 'name': 'mt_name_changed',
+ 'model': 'mail.message.subtype',
+ 'module': 'mail',
+ 'res_id': mt_name_changed.id
+ })
+ mail_template = self.env['mail.template'].create({
+ 'name': 'SPECIAL CONTENT UNLOCKED',
+ 'subject': 'SPECIAL CONTENT UNLOCKED',
+ 'model_id': self.env.ref('test_mail.model_mail_test_container').id,
+ 'auto_delete': True,
+ 'body_html': '''<div>WOOP WOOP</div>''',
+ })
+
+ def _track_subtype(self, init_values):
+ if 'name' in init_values and init_values['name'] == magic_code:
+ return 'mail.mt_name_changed'
+ return False
+ self.registry('mail.test.container')._patch_method('_track_subtype', _track_subtype)
+
+ def _track_template(self, changes):
+ res = {}
+ if 'name' in changes:
+ res['name'] = (mail_template, {'composition_mode': 'mass_mail'})
+ return res
+ self.registry('mail.test.container')._patch_method('_track_template', _track_template)
+
+ cls = type(self.env['mail.test.container'])
+ self.assertFalse(hasattr(getattr(cls, 'name'), 'track_visibility'))
+ getattr(cls, 'name').track_visibility = 'always'
+
+ @self.addCleanup
+ def cleanup():
+ del getattr(cls, 'name').track_visibility
+
+ test_mail_record = self.env['mail.test.container'].create({
+ 'name': 'Zizizatestmailname',
+ 'description': 'Zizizatestmaildescription',
+ })
+ test_mail_record.with_context(default_parent_id=2147483647).write({'name': magic_code})
+
+ def test_message_track_multiple(self):
+ """ check that multiple updates generate a single tracking message """
+ container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'})
+ self.record.name = 'Zboub'
+ self.record.customer_id = self.user_admin.partner_id
+ self.record.user_id = self.user_admin
+ self.record.container_id = container
+ self.flush_tracking()
+
+ # should have a single message with all tracked fields
+ self.assertEqual(len(self.record.message_ids), 1, 'should have 1 tracking message')
+ self.assertTracking(self.record.message_ids[0], [
+ ('customer_id', 'many2one', False, self.user_admin.partner_id),
+ ('user_id', 'many2one', False, self.user_admin),
+ ('container_id', 'many2one', False, container),
+ ])
+
+ def test_tracked_compute(self):
+ # no tracking at creation
+ record = self.env['mail.test.track.compute'].create({})
+ self.flush_tracking()
+ self.assertEqual(len(record.message_ids), 1)
+ self.assertEqual(len(record.message_ids[0].tracking_value_ids), 0)
+
+ # assign partner_id: one tracking message for the modified field and all
+ # the stored and non-stored computed fields on the record
+ partner = self.env['res.partner'].create({
+ 'name': 'Foo',
+ 'email': 'foo@example.com',
+ 'phone': '1234567890',
+ })
+ record.partner_id = partner
+ self.flush_tracking()
+ self.assertEqual(len(record.message_ids), 2)
+ self.assertEqual(len(record.message_ids[0].tracking_value_ids), 4)
+ self.assertTracking(record.message_ids[0], [
+ ('partner_id', 'many2one', False, partner),
+ ('partner_name', 'char', False, 'Foo'),
+ ('partner_email', 'char', False, 'foo@example.com'),
+ ('partner_phone', 'char', False, '1234567890'),
+ ])
+
+ # modify partner: one tracking message for the only recomputed field
+ partner.write({'name': 'Fool'})
+ self.flush_tracking()
+ self.assertEqual(len(record.message_ids), 3)
+ self.assertEqual(len(record.message_ids[0].tracking_value_ids), 1)
+ self.assertTracking(record.message_ids[0], [
+ ('partner_name', 'char', 'Foo', 'Fool'),
+ ])
+
+ # modify partner: one tracking message for both stored computed fields;
+ # the non-stored computed fields have no tracking
+ partner.write({
+ 'name': 'Bar',
+ 'email': 'bar@example.com',
+ 'phone': '0987654321',
+ })
+ # force recomputation of 'partner_phone' to make sure it does not
+ # generate tracking values
+ self.assertEqual(record.partner_phone, '0987654321')
+ self.flush_tracking()
+ self.assertEqual(len(record.message_ids), 4)
+ self.assertEqual(len(record.message_ids[0].tracking_value_ids), 2)
+ self.assertTracking(record.message_ids[0], [
+ ('partner_name', 'char', 'Fool', 'Bar'),
+ ('partner_email', 'char', 'foo@example.com', 'bar@example.com'),
+ ])
+
+
+@tagged('mail_track')
+class TestTrackingInternals(TestMailCommon):
+
+ def setUp(self):
+ super(TestTrackingInternals, self).setUp()
+
+ record = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context).create({
+ 'name': 'Test',
+ })
+ self.flush_tracking()
+ self.record = record.with_context(mail_notrack=False)
+
+ def test_track_groups(self):
+ field = self.record._fields['email_from']
+ self.addCleanup(setattr, field, 'groups', field.groups)
+ field.groups = 'base.group_erp_manager'
+
+ self.record.sudo().write({'email_from': 'X'})
+ self.flush_tracking()
+
+ msg_emp = self.record.message_ids.message_format()
+ msg_sudo = self.record.sudo().message_ids.message_format()
+ self.assertFalse(msg_emp[0].get('tracking_value_ids'), "should not have protected tracking values")
+ self.assertTrue(msg_sudo[0].get('tracking_value_ids'), "should have protected tracking values")
+
+ msg_emp = self.record._notify_prepare_template_context(self.record.message_ids, {})
+ msg_sudo = self.record.sudo()._notify_prepare_template_context(self.record.message_ids, {})
+ self.assertFalse(msg_emp.get('tracking_values'), "should not have protected tracking values")
+ self.assertTrue(msg_sudo.get('tracking_values'), "should have protected tracking values")
+
+ # test editing the record with user not in the group of the field
+ self.record.invalidate_cache()
+ self.record.clear_caches()
+ record_form = Form(self.record.with_user(self.user_employee))
+ record_form.name = 'TestDoNoCrash'
+ # the employee user must be able to save the fields on which he can write
+ # if we fetch all the tracked fields, ignoring the group of the current user
+ # it will crash and it shouldn't
+ record = record_form.save()
+ self.assertEqual(record.name, 'TestDoNoCrash')
+
+ def test_track_sequence(self):
+ """ Update some tracked fields and check that the mail.tracking.value are ordered according to their tracking_sequence"""
+ self.record.write({
+ 'name': 'Zboub',
+ 'customer_id': self.user_admin.partner_id.id,
+ 'user_id': self.user_admin.id,
+ 'container_id': self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'}).id
+ })
+ self.flush_tracking()
+ self.assertEqual(len(self.record.message_ids), 1, 'should have 1 tracking message')
+
+ tracking_values = self.env['mail.tracking.value'].search([('mail_message_id', '=', self.record.message_ids.id)])
+ self.assertEqual(tracking_values[0].tracking_sequence, 1)
+ self.assertEqual(tracking_values[1].tracking_sequence, 2)
+ self.assertEqual(tracking_values[2].tracking_sequence, 100)
+
+ def test_unlinked_field(self):
+ record_sudo = self.record.sudo()
+ record_sudo.write({'email_from': 'new_value'}) # create a tracking value
+ self.flush_tracking()
+ self.assertEqual(len(record_sudo.message_ids.tracking_value_ids), 1)
+ ir_model_field = self.env['ir.model.fields'].search([
+ ('model', '=', 'mail.test.ticket'),
+ ('name', '=', 'email_from')])
+ ir_model_field.with_context(_force_unlink=True).unlink()
+ self.assertEqual(len(record_sudo.message_ids.tracking_value_ids), 0)
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
new file mode 100644
index 00000000..6862160a
--- /dev/null
+++ b/addons/test_mail/tests/test_performance.py
@@ -0,0 +1,1021 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import base64
+
+from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
+from odoo.tests.common import users, warmup
+from odoo.tests import tagged
+from odoo.tools import mute_logger, formataddr
+
+
+@tagged('mail_performance')
+class BaseMailPerformance(TransactionCaseWithUserDemo):
+
+ def setUp(self):
+ super(BaseMailPerformance, self).setUp()
+ self._quick_create_ctx = {
+ 'no_reset_password': True,
+ 'mail_create_nolog': True,
+ 'mail_create_nosubscribe': True,
+ 'mail_notrack': True,
+ }
+ self.user_employee = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'Ernest Employee',
+ 'login': 'emp',
+ 'email': 'e.e@example.com',
+ 'signature': '--\nErnest',
+ 'notification_type': 'inbox',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id, self.env.ref('base.group_partner_manager').id])],
+ })
+
+ # patch registry to simulate a ready environment
+ self.patch(self.env.registry, 'ready', True)
+
+
+@tagged('mail_performance')
+class TestMailPerformance(BaseMailPerformance):
+
+ def setUp(self):
+ super(TestMailPerformance, self).setUp()
+
+ self.res_partner_3 = self.env['res.partner'].create({
+ 'name': 'Gemini Furniture',
+ 'email': 'gemini.furniture39@example.com',
+ })
+ self.res_partner_4 = self.env['res.partner'].create({
+ 'name': 'Ready Mat',
+ 'email': 'ready.mat28@example.com',
+ })
+ self.res_partner_10 = self.env['res.partner'].create({
+ 'name': 'The Jackson Group',
+ 'email': 'jackson.group82@example.com',
+ })
+ self.res_partner_12 = self.env['res.partner'].create({
+ 'name': 'Azure Interior',
+ 'email': 'azure.Interior24@example.com',
+ })
+ self.env['mail.performance.thread'].create([
+ {
+ 'name': 'Object 0',
+ 'value': 0,
+ 'partner_id': self.res_partner_3.id,
+ }, {
+ 'name': 'Object 1',
+ 'value': 10,
+ 'partner_id': self.res_partner_3.id,
+ }, {
+ 'name': 'Object 2',
+ 'value': 20,
+ 'partner_id': self.res_partner_4.id,
+ }, {
+ 'name': 'Object 3',
+ 'value': 30,
+ 'partner_id': self.res_partner_10.id,
+ }, {
+ 'name': 'Object 4',
+ 'value': 40,
+ 'partner_id': self.res_partner_12.id,
+ }
+ ])
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_read_mail(self):
+ """ Read records inheriting from 'mail.thread'. """
+ records = self.env['mail.performance.thread'].search([])
+ self.assertEqual(len(records), 5)
+
+ with self.assertQueryCount(__system__=2, demo=2):
+ # without cache
+ for record in records:
+ record.partner_id.country_id.name
+
+ with self.assertQueryCount(0):
+ # with cache
+ for record in records:
+ record.partner_id.country_id.name
+
+ with self.assertQueryCount(0):
+ # value_pc must have been prefetched, too
+ for record in records:
+ record.value_pc
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_write_mail(self):
+ """ Write records inheriting from 'mail.thread' (no recomputation). """
+ records = self.env['mail.performance.thread'].search([])
+ self.assertEqual(len(records), 5)
+
+ with self.assertQueryCount(__system__=2, demo=2):
+ records.write({'name': 'X'})
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_write_mail_with_recomputation(self):
+ """ Write records inheriting from 'mail.thread' (with recomputation). """
+ records = self.env['mail.performance.thread'].search([])
+ self.assertEqual(len(records), 5)
+
+ with self.assertQueryCount(__system__=2, demo=2):
+ records.write({'value': 42})
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_write_mail_with_tracking(self):
+ """ Write records inheriting from 'mail.thread' (with field tracking). """
+ record = self.env['mail.performance.thread'].create({
+ 'name': 'Test',
+ 'track': 'Y',
+ 'value': 40,
+ 'partner_id': self.res_partner_12.id,
+ })
+
+ with self.assertQueryCount(__system__=3, demo=3):
+ record.track = 'X'
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_create_mail(self):
+ """ Create records inheriting from 'mail.thread' (without field tracking). """
+ model = self.env['mail.performance.thread']
+
+ with self.assertQueryCount(__system__=2, demo=2):
+ model.with_context(tracking_disable=True).create({'name': 'X'})
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_create_mail_with_tracking(self):
+ """ Create records inheriting from 'mail.thread' (with field tracking). """
+ with self.assertQueryCount(__system__=7, demo=7):
+ self.env['mail.performance.thread'].create({'name': 'X'})
+
+ @users('__system__', 'emp')
+ @warmup
+ def test_create_mail_simple(self):
+ with self.assertQueryCount(__system__=6, emp=6):
+ self.env['mail.test.simple'].create({'name': 'Test'})
+
+ @users('__system__', 'emp')
+ @warmup
+ def test_write_mail_simple(self):
+ rec = self.env['mail.test.simple'].create({'name': 'Test'})
+ with self.assertQueryCount(__system__=1, emp=1):
+ rec.write({
+ 'name': 'Test2',
+ 'email_from': 'test@test.com',
+ })
+
+
+@tagged('mail_performance')
+class TestMailAPIPerformance(BaseMailPerformance):
+
+ def setUp(self):
+ super(TestMailAPIPerformance, self).setUp()
+ self.customer = self.env['res.partner'].with_context(self._quick_create_ctx).create({
+ 'name': 'Test Customer',
+ 'email': 'customer.test@example.com',
+ })
+ self.user_test = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'Paulette Testouille',
+ 'login': 'paul',
+ 'email': 'user.test.paulette@example.com',
+ 'notification_type': 'inbox',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
+ })
+
+ # automatically follow activities, for backward compatibility concerning query count
+ self.env.ref('mail.mt_activities').write({'default': True})
+
+ def _create_test_records(self):
+ self.test_record_full = self.env['mail.test.ticket'].with_context(self._quick_create_ctx).create({
+ 'name': 'TestRecord',
+ 'customer_id': self.customer.id,
+ 'user_id': self.user_test.id,
+ 'email_from': 'nopartner.test@example.com',
+ })
+ self.test_template_full = self.env['mail.template'].create({
+ 'name': 'TestTemplate',
+ 'model_id': self.env['ir.model']._get('mail.test.ticket').id,
+ 'subject': 'About ${object.name}',
+ 'body_html': '<p>Hello ${object.name}</p>',
+ 'email_from': '${object.user_id.email_formatted | safe}',
+ 'partner_to': '${object.customer_id.id}',
+ 'email_to': '${("%s Customer <%s>" % (object.name, object.email_from)) | safe}',
+ })
+
+ @users('__system__', 'emp')
+ @warmup
+ def test_adv_activity(self):
+ model = self.env['mail.test.activity']
+
+ with self.assertQueryCount(__system__=6, emp=6):
+ model.create({'name': 'Test'})
+
+ @users('__system__', 'emp')
+ @warmup
+ @mute_logger('odoo.models.unlink')
+ def test_adv_activity_full(self):
+ record = self.env['mail.test.activity'].create({'name': 'Test'})
+ MailActivity = self.env['mail.activity'].with_context({
+ 'default_res_model': 'mail.test.activity',
+ })
+
+ with self.assertQueryCount(__system__=6, emp=6):
+ activity = MailActivity.create({
+ 'summary': 'Test Activity',
+ 'res_id': record.id,
+ 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
+ })
+ #read activity_type to normalize cache between enterprise and community
+ #voip module read activity_type during create leading to one less query in enterprise on action_feedback
+ category = activity.activity_type_id.category
+
+ with self.assertQueryCount(__system__=16, emp=20):
+ activity.action_feedback(feedback='Zizisse Done !')
+
+ @users('__system__', 'emp')
+ @warmup
+ @mute_logger('odoo.models.unlink')
+ def test_adv_activity_mixin(self):
+ record = self.env['mail.test.activity'].create({'name': 'Test'})
+
+ with self.assertQueryCount(__system__=8, emp=8):
+ activity = record.action_start('Test Start')
+ #read activity_type to normalize cache between enterprise and community
+ #voip module read activity_type during create leading to one less query in enterprise on action_close
+ category = activity.activity_type_id.category
+
+ record.write({'name': 'Dupe write'})
+
+ with self.assertQueryCount(__system__=17, emp=20):
+ record.action_close('Dupe feedback')
+
+ self.assertEqual(record.activity_ids, self.env['mail.activity'])
+
+ @users('__system__', 'emp')
+ @warmup
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer(self):
+ self._create_test_records()
+ test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id)
+ customer_id = self.customer.id
+ with self.assertQueryCount(__system__=2, emp=2):
+ composer = self.env['mail.compose.message'].with_context({
+ 'default_composition_mode': 'comment',
+ 'default_model': test_record._name,
+ 'default_res_id': test_record.id,
+ }).create({
+ 'body': '<p>Test Body</p>',
+ 'partner_ids': [(4, customer_id)],
+ })
+
+ with self.assertQueryCount(__system__=32, emp=39):
+ composer.send_mail()
+
+ @users('__system__', 'emp')
+ @warmup
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail')
+ def test_mail_composer_nodelete(self):
+ self._create_test_records()
+ test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id)
+ customer_id = self.customer.id
+ with self.assertQueryCount(__system__=2, emp=2):
+ composer = self.env['mail.compose.message'].with_context({
+ 'default_composition_mode': 'comment',
+ 'default_model': test_record._name,
+ 'default_res_id': test_record.id,
+ 'mail_auto_delete': False,
+ }).create({
+ 'body': '<p>Test Body</p>',
+ 'partner_ids': [(4, customer_id)],
+ })
+
+ with self.assertQueryCount(__system__=25, emp=32):
+ composer.send_mail()
+
+ @users('__system__', 'emp')
+ @warmup
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ def test_mail_composer_w_template(self):
+ self._create_test_records()
+ test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id)
+ test_template = self.env['mail.template'].browse(self.test_template_full.id)
+ # TODO FIXME non deterministic, last check 25 Mar 2020. Runbot at most +7 compared to local.
+ with self.assertQueryCount(__system__=12, emp=14):
+ composer = self.env['mail.compose.message'].with_context({
+ 'default_composition_mode': 'comment',
+ 'default_model': test_record._name,
+ 'default_res_id': test_record.id,
+ 'default_template_id': test_template.id,
+ }).create({})
+ composer.onchange_template_id_wrapper()
+
+ with self.assertQueryCount(__system__=33, emp=40):
+ composer.send_mail()
+
+ # remove created partner to ensure tests are the same each run
+ self.env['res.partner'].sudo().search([('email', '=', 'nopartner.test@example.com')]).unlink()
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_assignation_email(self):
+ self.user_test.write({'notification_type': 'email'})
+ record = self.env['mail.test.track'].create({'name': 'Test'})
+ with self.assertQueryCount(__system__=37, emp=38):
+ record.write({
+ 'user_id': self.user_test.id,
+ })
+
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_assignation_inbox(self):
+ record = self.env['mail.test.track'].create({'name': 'Test'})
+ with self.assertQueryCount(__system__=18, emp=21):
+ record.write({
+ 'user_id': self.user_test.id,
+ })
+
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_log(self):
+ record = self.env['mail.test.simple'].create({'name': 'Test'})
+
+ with self.assertQueryCount(__system__=1, emp=1):
+ record._message_log(
+ body='<p>Test _message_log</p>',
+ message_type='comment')
+
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_log_with_post(self):
+ record = self.env['mail.test.simple'].create({'name': 'Test'})
+
+ with self.assertQueryCount(__system__=3, emp=6):
+ record.message_post(
+ body='<p>Test message_post as log</p>',
+ subtype_xmlid='mail.mt_note',
+ message_type='comment')
+
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_post_no_notification(self):
+ record = self.env['mail.test.simple'].create({'name': 'Test'})
+
+ with self.assertQueryCount(__system__=3, emp=6):
+ record.message_post(
+ body='<p>Test Post Performances basic</p>',
+ partner_ids=[],
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment')
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_post_one_email_notification(self):
+ record = self.env['mail.test.simple'].create({'name': 'Test'})
+
+ with self.assertQueryCount(__system__=28, emp=31):
+ record.message_post(
+ body='<p>Test Post Performances with an email ping</p>',
+ partner_ids=self.customer.ids,
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment')
+
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_post_one_inbox_notification(self):
+ record = self.env['mail.test.simple'].create({'name': 'Test'})
+
+ with self.assertQueryCount(__system__=12, emp=17):
+ record.message_post(
+ body='<p>Test Post Performances with an inbox ping</p>',
+ partner_ids=self.user_test.partner_id.ids,
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment')
+
+ @mute_logger('odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_subscribe_default(self):
+ record = self.env['mail.test.simple'].create({'name': 'Test'})
+
+ with self.assertQueryCount(__system__=6, emp=6):
+ record.message_subscribe(partner_ids=self.user_test.partner_id.ids)
+
+ with self.assertQueryCount(__system__=3, emp=3):
+ record.message_subscribe(partner_ids=self.user_test.partner_id.ids)
+
+ @mute_logger('odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_subscribe_subtypes(self):
+ record = self.env['mail.test.simple'].create({'name': 'Test'})
+ subtype_ids = (self.env.ref('test_mail.st_mail_test_simple_external') | self.env.ref('mail.mt_comment')).ids
+
+ with self.assertQueryCount(__system__=5, emp=5):
+ record.message_subscribe(partner_ids=self.user_test.partner_id.ids, subtype_ids=subtype_ids)
+
+ with self.assertQueryCount(__system__=2, emp=2):
+ record.message_subscribe(partner_ids=self.user_test.partner_id.ids, subtype_ids=subtype_ids)
+
+ @mute_logger('odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_message_track(self):
+ record = self.env['mail.performance.tracking'].create({'name': 'Zizizatestname'})
+ with self.assertQueryCount(__system__=3, emp=3):
+ record.write({'name': 'Zizizanewtestname'})
+ record.flush()
+
+ with self.assertQueryCount(__system__=5, emp=5):
+ record.write({'field_%s' % (i): 'Tracked Char Fields %s' % (i) for i in range(3)})
+ record.flush()
+
+ with self.assertQueryCount(__system__=6, emp=6):
+ record.write({'field_%s' % (i): 'Field Without Cache %s' % (i) for i in range(3)})
+ record.flush()
+ record.write({'field_%s' % (i): 'Field With Cache %s' % (i) for i in range(3)})
+ record.flush()
+
+
+@tagged('mail_performance')
+class TestMailComplexPerformance(BaseMailPerformance):
+
+ def setUp(self):
+ super(TestMailComplexPerformance, self).setUp()
+ self.user_portal = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'Olivia Portal',
+ 'login': 'port',
+ 'email': 'p.p@example.com',
+ 'signature': '--\nOlivia',
+ 'notification_type': 'email',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_portal').id])],
+ })
+
+ # setup mail gateway
+ self.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'example.com')
+ self.env['ir.config_parameter'].sudo().set_param('mail.catchall.alias', 'test-catchall')
+ self.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', 'test-bounce')
+
+ self.channel = self.env['mail.channel'].with_context(self._quick_create_ctx).create({
+ 'name': 'Listener',
+ })
+
+ # prepare recipients to test for more realistic workload
+ self.customer = self.env['res.partner'].with_context(self._quick_create_ctx).create({
+ 'name': 'Test Customer',
+ 'email': 'test@example.com'
+ })
+ self.container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({
+ 'name': 'Test Container',
+ 'customer_id': self.customer.id,
+ 'alias_name': 'test-alias',
+ })
+ Partners = self.env['res.partner'].with_context(self._quick_create_ctx)
+ self.partners = self.env['res.partner']
+ for x in range(0, 10):
+ self.partners |= Partners.create({'name': 'Test %s' % x, 'email': 'test%s@example.com' % x})
+ self.container.message_subscribe(self.partners.ids, subtype_ids=[
+ self.env.ref('mail.mt_comment').id,
+ self.env.ref('test_mail.st_mail_test_container_child_full').id]
+ )
+ # `test_complex_mail_mail_send`
+ self.container.flush()
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_mail_mail_send(self):
+ message = self.env['mail.message'].sudo().create({
+ 'subject': 'Test',
+ 'body': '<p>Test</p>',
+ 'author_id': self.env.user.partner_id.id,
+ 'email_from': self.env.user.partner_id.email,
+ 'model': 'mail.test.container',
+ 'res_id': self.container.id,
+ })
+ mail = self.env['mail.mail'].sudo().create({
+ 'body_html': '<p>Test</p>',
+ 'mail_message_id': message.id,
+ 'recipient_ids': [(4, pid) for pid in self.partners.ids],
+ })
+ mail_ids = mail.ids
+ with self.assertQueryCount(__system__=7, emp=7):
+ self.env['mail.mail'].sudo().browse(mail_ids).send()
+
+ self.assertEqual(mail.body_html, '<p>Test</p>')
+ self.assertEqual(mail.reply_to, formataddr(('%s %s' % (self.env.company.name, self.container.name), 'test-alias@example.com')))
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_message_post(self):
+ self.container.message_subscribe(self.user_portal.partner_id.ids)
+ record = self.container.with_user(self.env.user)
+
+ with self.assertQueryCount(__system__=63, emp=64):
+ record.message_post(
+ body='<p>Test Post Performances</p>',
+ message_type='comment',
+ subtype_xmlid='mail.mt_comment')
+
+ self.assertEqual(record.message_ids[0].body, '<p>Test Post Performances</p>')
+ self.assertEqual(record.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_message_post_template(self):
+ self.container.message_subscribe(self.user_portal.partner_id.ids)
+ record = self.container.with_user(self.env.user)
+ template_id = self.env.ref('test_mail.mail_test_container_tpl').id
+
+ with self.assertQueryCount(__system__=73, emp=75):
+ record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
+
+ self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name)
+ self.assertEqual(record.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id | self.customer)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_message_subscribe(self):
+ pids = self.partners.ids
+ cids = self.channel.ids
+ subtypes = self.env.ref('mail.mt_comment') | self.env.ref('test_mail.st_mail_test_ticket_container_upd')
+ subtype_ids = subtypes.ids
+ rec = self.env['mail.test.ticket'].create({
+ 'name': 'Test',
+ 'container_id': False,
+ 'customer_id': False,
+ 'user_id': self.user_portal.id,
+ })
+ rec1 = rec.with_context(active_test=False) # to see inactive records
+
+ self.assertEqual(rec1.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id)
+ self.assertEqual(rec1.message_channel_ids, self.env['mail.channel'])
+
+ # subscribe new followers with forced given subtypes
+ with self.assertQueryCount(__system__=8, emp=8):
+ rec.message_subscribe(
+ partner_ids=pids[:4],
+ channel_ids=cids,
+ subtype_ids=subtype_ids
+ )
+
+ self.assertEqual(rec1.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners[:4])
+ self.assertEqual(rec1.message_channel_ids, self.channel)
+
+ # subscribe existing and new followers with force=False, meaning only some new followers will be added
+ with self.assertQueryCount(__system__=6, emp=6):
+ rec.message_subscribe(
+ partner_ids=pids[:6],
+ channel_ids=cids,
+ subtype_ids=None
+ )
+
+ self.assertEqual(rec1.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners[:6])
+ self.assertEqual(rec1.message_channel_ids, self.channel)
+
+ # subscribe existing and new followers with force=True, meaning all will have the same subtypes
+ with self.assertQueryCount(__system__=7, emp=7):
+ rec.message_subscribe(
+ partner_ids=pids,
+ channel_ids=cids,
+ subtype_ids=subtype_ids
+ )
+
+ self.assertEqual(rec1.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners)
+ self.assertEqual(rec1.message_channel_ids, self.channel)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_tracking_assignation(self):
+ """ Assignation performance test on already-created record """
+ rec = self.env['mail.test.ticket'].create({
+ 'name': 'Test',
+ 'container_id': self.container.id,
+ 'customer_id': self.customer.id,
+ 'user_id': self.env.uid,
+ })
+ rec1 = rec.with_context(active_test=False) # to see inactive records
+ self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id)
+ with self.assertQueryCount(__system__=37, emp=38):
+ rec.write({'user_id': self.user_portal.id})
+ self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
+ # write tracking message
+ self.assertEqual(rec1.message_ids[0].subtype_id, self.env.ref('mail.mt_note'))
+ self.assertEqual(rec1.message_ids[0].notified_partner_ids, self.env['res.partner'])
+ # creation message
+ self.assertEqual(rec1.message_ids[1].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd'))
+ self.assertEqual(rec1.message_ids[1].notified_partner_ids, self.partners)
+ self.assertEqual(len(rec1.message_ids), 2)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_tracking_subscription_create(self):
+ """ Creation performance test involving auto subscription, assignation, tracking with subtype and template send. """
+ container_id = self.container.id
+ customer_id = self.customer.id
+ user_id = self.user_portal.id
+
+ with self.assertQueryCount(__system__=108, emp=109):
+ rec = self.env['mail.test.ticket'].create({
+ 'name': 'Test',
+ 'container_id': container_id,
+ 'customer_id': customer_id,
+ 'user_id': user_id,
+ })
+
+ rec1 = rec.with_context(active_test=False) # to see inactive records
+ self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
+ # creation message
+ self.assertEqual(rec1.message_ids[0].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd'))
+ self.assertEqual(rec1.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id)
+ self.assertEqual(len(rec1.message_ids), 1)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_tracking_subscription_subtype(self):
+ """ Write performance test involving auto subscription, tracking with subtype """
+ rec = self.env['mail.test.ticket'].create({
+ 'name': 'Test',
+ 'container_id': False,
+ 'customer_id': False,
+ 'user_id': self.user_portal.id,
+ })
+ rec1 = rec.with_context(active_test=False) # to see inactive records
+ self.assertEqual(rec1.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
+ self.assertEqual(len(rec1.message_ids), 1)
+ with self.assertQueryCount(__system__=77, emp=77):
+ rec.write({
+ 'name': 'Test2',
+ 'container_id': self.container.id,
+ })
+
+ self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
+ # write tracking message
+ self.assertEqual(rec1.message_ids[0].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd'))
+ self.assertEqual(rec1.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id)
+ # creation message
+ self.assertEqual(rec1.message_ids[1].subtype_id, self.env.ref('mail.mt_note'))
+ self.assertEqual(rec1.message_ids[1].notified_partner_ids, self.env['res.partner'])
+ self.assertEqual(len(rec1.message_ids), 2)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_tracking_subscription_write(self):
+ """ Write performance test involving auto subscription, tracking with subtype and template send """
+ container_id = self.container.id
+ customer_id = self.customer.id
+ container2 = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({
+ 'name': 'Test Container 2',
+ 'customer_id': False,
+ 'alias_name': False,
+ })
+
+ rec = self.env['mail.test.ticket'].create({
+ 'name': 'Test',
+ 'container_id': container2.id,
+ 'customer_id': False,
+ 'user_id': self.user_portal.id,
+ })
+ rec1 = rec.with_context(active_test=False) # to see inactive records
+ self.assertEqual(rec1.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
+
+ with self.assertQueryCount(__system__=85, emp=85):
+ rec.write({
+ 'name': 'Test2',
+ 'container_id': container_id,
+ 'customer_id': customer_id,
+ })
+
+ self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
+ # write tracking message
+ self.assertEqual(rec1.message_ids[0].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd'))
+ self.assertEqual(rec1.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id)
+ # creation message
+ self.assertEqual(rec1.message_ids[1].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd'))
+ self.assertEqual(rec1.message_ids[1].notified_partner_ids, self.user_portal.partner_id)
+ self.assertEqual(len(rec1.message_ids), 2)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('__system__', 'emp')
+ @warmup
+ def test_complex_tracking_template(self):
+ """ Write performance test involving assignation, tracking with template """
+ customer_id = self.customer.id
+ self.assertTrue(self.env.registry.ready, "We need to simulate that registery is ready")
+ rec = self.env['mail.test.ticket'].create({
+ 'name': 'Test',
+ 'container_id': self.container.id,
+ 'customer_id': False,
+ 'user_id': self.user_portal.id,
+ 'mail_template': self.env.ref('test_mail.mail_test_ticket_tracking_tpl').id,
+ })
+ rec1 = rec.with_context(active_test=False) # to see inactive records
+ self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
+
+ with self.assertQueryCount(__system__=29, emp=30):
+ rec.write({
+ 'name': 'Test2',
+ 'customer_id': customer_id,
+ 'user_id': self.env.uid,
+ })
+
+ # write template message (sent to customer, mass mailing kept for history)
+ self.assertEqual(rec1.message_ids[0].subtype_id, self.env['mail.message.subtype'])
+ self.assertEqual(rec1.message_ids[0].subject, 'Test Template')
+ # write tracking message
+ self.assertEqual(rec1.message_ids[1].subtype_id, self.env.ref('mail.mt_note'))
+ self.assertEqual(rec1.message_ids[1].notified_partner_ids, self.env['res.partner'])
+ # creation message
+ self.assertEqual(rec1.message_ids[2].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd'))
+ self.assertEqual(rec1.message_ids[2].notified_partner_ids, self.partners | self.user_portal.partner_id)
+ self.assertEqual(len(rec1.message_ids), 3)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('emp')
+ @warmup
+ def test_message_format(self):
+ """Test performance of `_message_format` and of `message_format` with
+ multiple messages with multiple attachments, different authors, various
+ notifications, and different tracking values.
+ Those messages might not make sense functionally but they are crafted to
+ cover as much of the code as possible in regard to number of queries.
+ """
+ name_field = self.env['ir.model.fields']._get(self.container._name, 'name')
+ customer_id_field = self.env['ir.model.fields']._get(self.container._name, 'customer_id')
+
+ messages = self.env['mail.message'].sudo().create([{
+ 'subject': 'Test 0',
+ 'body': '<p>Test 0</p>',
+ 'author_id': self.partners[0].id,
+ 'email_from': self.partners[0].email,
+ 'model': 'mail.test.container',
+ 'res_id': self.container.id,
+ 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment'),
+ 'attachment_ids': [
+ (0, 0, {
+ 'name': 'test file 0 - %d' % j,
+ 'datas': 'data',
+ }) for j in range(2)
+ ],
+ 'notification_ids': [
+ (0, 0, {
+ 'res_partner_id': self.partners[3].id,
+ 'notification_type': 'inbox',
+ }),
+ (0, 0, {
+ 'res_partner_id': self.partners[4].id,
+ 'notification_type': 'email',
+ 'notification_status': 'exception',
+ }),
+ (0, 0, {
+ 'res_partner_id': self.partners[6].id,
+ 'notification_type': 'email',
+ 'notification_status': 'exception',
+ }),
+ ],
+ 'tracking_value_ids': [
+ (0, 0, {
+ 'field': name_field.id,
+ 'field_desc': 'Name',
+ 'old_value_char': 'old 0',
+ 'new_value_char': 'new 0',
+ }),
+ (0, 0, {
+ 'field': customer_id_field.id,
+ 'field_desc': 'Customer',
+ 'old_value_integer': self.partners[7].id,
+ 'new_value_integer': self.partners[8].id,
+ }),
+ ]
+ }, {
+ 'subject': 'Test 1',
+ 'body': '<p>Test 1</p>',
+ 'author_id': self.partners[1].id,
+ 'email_from': self.partners[1].email,
+ 'model': 'mail.test.container',
+ 'res_id': self.container.id,
+ 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
+ 'attachment_ids': [
+ (0, 0, {
+ 'name': 'test file 1 - %d' % j,
+ 'datas': 'data',
+ }) for j in range(2)
+ ],
+ 'notification_ids': [
+ (0, 0, {
+ 'res_partner_id': self.partners[5].id,
+ 'notification_type': 'inbox',
+ }),
+ (0, 0, {
+ 'res_partner_id': self.partners[6].id,
+ 'notification_type': 'email',
+ 'notification_status': 'exception',
+ }),
+ ],
+ 'tracking_value_ids': [
+ (0, 0, {
+ 'field': name_field.id,
+ 'field_desc': 'Name',
+ 'old_value_char': 'old 1',
+ 'new_value_char': 'new 1',
+ }),
+ (0, 0, {
+ 'field': customer_id_field.id,
+ 'field_desc': 'Customer',
+ 'old_value_integer': self.partners[7].id,
+ 'new_value_integer': self.partners[8].id,
+ }),
+ ]
+ }])
+
+ with self.assertQueryCount(emp=5):
+ res = messages.message_format()
+ self.assertEqual(len(res), 2)
+ for message in res:
+ self.assertEqual(len(message['attachment_ids']), 2)
+
+ messages.flush()
+ messages.invalidate_cache()
+
+ with self.assertQueryCount(emp=17):
+ res = messages.message_format()
+ self.assertEqual(len(res), 2)
+ for message in res:
+ self.assertEqual(len(message['attachment_ids']), 2)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('emp')
+ @warmup
+ def test_message_format_group_thread_name_by_model(self):
+ """Ensures the fetch of multiple thread names is grouped by model."""
+ records = []
+ for i in range(5):
+ records.append(self.env['mail.test.simple'].create({'name': 'Test'}))
+ records.append(self.env['mail.test.track'].create({'name': 'Test'}))
+
+ messages = self.env['mail.message'].create([{
+ 'model': record._name,
+ 'res_id': record.id
+ } for record in records])
+
+ with self.assertQueryCount(emp=4):
+ res = messages.message_format()
+ self.assertEqual(len(res), 6)
+
+ messages.flush()
+ messages.invalidate_cache()
+
+ with self.assertQueryCount(emp=14):
+ res = messages.message_format()
+ self.assertEqual(len(res), 6)
+
+
+@tagged('mail_performance')
+class TestMailHeavyPerformancePost(BaseMailPerformance):
+
+ def setUp(self):
+ super(TestMailHeavyPerformancePost, self).setUp()
+
+ # record
+ self.customer = self.env['res.partner'].with_context(self._quick_create_ctx).create({
+ 'name': 'customer',
+ 'email': 'customer@example.com',
+ })
+ self.record = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({
+ 'name': 'Test record',
+ 'customer_id': self.customer.id,
+ 'alias_name': 'test-alias',
+ })
+ # followers
+ self.user_follower_email = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'user_follower_email',
+ 'login': 'user_follower_email',
+ 'email': 'user_follower_email@example.com',
+ 'notification_type': 'email',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
+ })
+ self.user_follower_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'user_follower_inbox',
+ 'login': 'user_follower_inbox',
+ 'email': 'user_follower_inbox@example.com',
+ 'notification_type': 'inbox',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
+ })
+ self.partner_follower = self.env['res.partner'].with_context(self._quick_create_ctx).create({
+ 'name': 'partner_follower',
+ 'email': 'partner_follower@example.com',
+ })
+ self.record.message_subscribe([
+ self.partner_follower.id,
+ self.user_follower_inbox.partner_id.id,
+ self.user_follower_email.partner_id.id
+ ])
+
+ # partner_ids
+ self.user_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'user_inbox',
+ 'login': 'user_inbox',
+ 'email': 'user_inbox@example.com',
+ 'notification_type': 'inbox',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
+ })
+ self.user_email = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'user_email',
+ 'login': 'user_email',
+ 'email': 'user_email@example.com',
+ 'notification_type': 'email',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
+ })
+ self.partner = self.env['res.partner'].with_context(self._quick_create_ctx).create({
+ 'name': 'partner',
+ 'email': 'partner@example.com',
+ })
+ # channels user/partner
+ self.partner_channel_inbox = self.env['res.partner'].with_context(self._quick_create_ctx).create({
+ 'name': 'partner_channel_inbox',
+ 'email': 'partner_channel_inbox@example.com',
+ })
+ self.partner_channel_email = self.env['res.partner'].with_context(self._quick_create_ctx).create({
+ 'name': 'partner_channel_email',
+ 'email': 'partner_channel_email@example.com',
+ })
+ self.user_channel_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'user_channel_inbox',
+ 'login': 'user_channel_inbox',
+ 'email': 'user_channel_inbox@example.com',
+ 'notification_type': 'inbox',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
+ })
+ self.user_channel_email = self.env['res.users'].with_context(self._quick_create_ctx).create({
+ 'name': 'user_channel_email',
+ 'login': 'user_channel_email',
+ 'email': 'user_channel_email@example.com',
+ 'notification_type': 'inbox',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
+ })
+ # channels
+ self.channel_inbox = self.env['mail.channel'].with_context(self._quick_create_ctx).create({
+ 'name': 'channel_inbox',
+ 'channel_partner_ids': [(4, self.partner_channel_inbox.id), (4, self.user_channel_inbox.partner_id.id)]
+ })
+ self.channel_email = self.env['mail.channel'].with_context(self._quick_create_ctx).create({
+ 'name': 'channel_email',
+ 'email_send': True,
+ 'channel_partner_ids': [(4, self.partner_channel_email.id), (4, self.user_channel_email.partner_id.id)]
+ })
+ self.vals = [{
+ 'datas': base64.b64encode(bytes("attachement content %s" % i, 'utf-8')),
+ 'name': 'fileText_test%s.txt' % i,
+ 'mimetype': 'text/plain',
+ 'res_model': 'mail.compose.message',
+ 'res_id': 0,
+ } for i in range(3)]
+
+ self.patch(self.env.registry, 'ready', True)
+
+ @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
+ @users('emp')
+ @warmup
+ def test_complete_message_post(self):
+ # aims to cover as much features of message_post as possible
+ partner_ids = [self.user_inbox.partner_id.id, self.user_email.partner_id.id, self.partner.id]
+ channel_ids = [self.channel_inbox.id, self.channel_email.id]
+ record = self.record.with_user(self.env.user)
+ attachements = [ # not linear on number of attachements
+ ('attach tuple 1', "attachement tupple content 1"),
+ ('attach tuple 2', "attachement tupple content 2", {'cid': 'cid1'}),
+ ('attach tuple 3', "attachement tupple content 3", {'cid': 'cid2'}),
+ ]
+ self.attachements = self.env['ir.attachment'].with_user(self.env.user).create(self.vals)
+ attachement_ids = self.attachements.ids
+ with self.assertQueryCount(emp=80):
+ self.cr.sql_log = self.warm and self.cr.sql_log_count
+ record.with_context({}).message_post(
+ body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',
+ subject='Test Subject',
+ message_type='notification',
+ subtype_xmlid=None,
+ partner_ids=partner_ids,
+ channel_ids=channel_ids,
+ parent_id=False,
+ attachments=attachements,
+ attachment_ids=attachement_ids,
+ add_sign=True,
+ model_description=False,
+ mail_auto_delete=True
+ )
+ self.cr.sql_log = False
+ self.assertTrue(record.message_ids[0].body.startswith('<p>Test body <img src="/web/image/'))
+ self.assertEqual(self.attachements.mapped('res_model'), [record._name for i in range(3)])
+ self.assertEqual(self.attachements.mapped('res_id'), [record.id for i in range(3)])
+ # self.assertEqual(record.message_ids[0].notified_partner_ids, [])
diff --git a/addons/test_mail/tests/test_ui.py b/addons/test_mail/tests/test_ui.py
new file mode 100644
index 00000000..82858b52
--- /dev/null
+++ b/addons/test_mail/tests/test_ui.py
@@ -0,0 +1,10 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+
+
+@odoo.tests.tagged('post_install', '-at_install')
+class TestUi(odoo.tests.HttpCase):
+
+ def test_01_mail_tour(self):
+ self.start_tour("/web", 'mail_tour', login="admin")