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

1082 lines
52 KiB
Python

# -*- 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 freezegun import freeze_time
from psycopg2 import IntegrityError
from unittest.mock import patch
from unittest.mock import DEFAULT
import pytz
import random
from odoo import fields, exceptions, tests
from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon
from odoo.addons.test_mail.models.test_mail_models import MailTestActivity
from odoo.tests import Form, HttpCase, users
from odoo.tools import mute_logger
class TestActivityCommon(MailCommon):
@classmethod
def setUpClass(cls):
super(TestActivityCommon, cls).setUpClass()
cls.test_record, cls.test_record_2 = cls.env['mail.test.activity'].with_context(cls._test_context).create([
{'name': 'Test'}, {'name': 'Test_2'},
])
# 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(records, operation):
""" If employee is test employee, consider they have no access on document """
if records.env.uid == self.user_employee.id and not records.env.su:
return records, lambda: exceptions.AccessError('Hop hop hop Ernest, please step back.')
return DEFAULT
with patch.object(MailTestActivity, '_check_access', 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(records, operation):
""" If employee is test employee, consider they have no access on document """
if records.env.uid == self.user_employee.id and not records.env.su:
raise exceptions.AccessError('Hop hop hop Ernest, please step back.')
return DEFAULT
test_activity = self.env['mail.activity'].with_user(self.user_admin).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_admin.id,
'summary': 'Summary',
})
test_activity.flush_recordset()
# can _search activities if access to the document
self.env['mail.activity'].with_user(self.user_employee)._search(
[('id', '=', test_activity.id)])
# cannot _search activities if no access to the document
with patch.object(MailTestActivity, '_check_access', autospec=True, side_effect=_employee_crash):
with self.assertRaises(exceptions.AccessError):
searched_activity = self.env['mail.activity'].with_user(self.user_employee)._search(
[('id', '=', test_activity.id)])
# can read_group activities if access to the document
read_group_result = self.env['mail.activity'].with_user(self.user_employee).read_group(
[('id', '=', test_activity.id)],
['summary'],
['summary'],
)
self.assertEqual(1, read_group_result[0]['summary_count'])
self.assertEqual('Summary', read_group_result[0]['summary'])
# cannot read_group activities if no access to the document
with patch.object(MailTestActivity, '_check_access', autospec=True, side_effect=_employee_crash):
with self.assertRaises(exceptions.AccessError):
self.env['mail.activity'].with_user(self.user_employee).read_group(
[('id', '=', test_activity.id)],
['summary'],
['summary'],
)
# cannot read activities if no access to the document
with patch.object(MailTestActivity, '_check_access', autospec=True, side_effect=_employee_crash):
with self.assertRaises(exceptions.AccessError):
searched_activity = self.env['mail.activity'].with_user(self.user_employee).search(
[('id', '=', test_activity.id)])
searched_activity.read(['summary'])
# cannot search_read activities if no access to the document
with patch.object(MailTestActivity, '_check_access', autospec=True, side_effect=_employee_crash):
with self.assertRaises(exceptions.AccessError):
self.env['mail.activity'].with_user(self.user_employee).search_read(
[('id', '=', test_activity.id)],
['summary'])
# can create activities for people that cannot access record
with patch.object(MailTestActivity, '_check_access', autospec=True, side_effect=_employee_crash):
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', 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)
test_activity.user_id = self.user_employee
test_activity.flush_recordset()
# user can read activities assigned to him even if he has no access to the document
with patch.object(MailTestActivity, '_check_access', autospec=True, side_effect=_employee_crash):
found = self.env['mail.activity'].with_user(self.user_employee).search(
[('id', '=', test_activity.id)])
self.assertEqual(found, test_activity)
found.read(['summary'])
# user can read_group activities assigned to him even if he has no access to the document
with patch.object(MailTestActivity, '_check_access', autospec=True, side_effect=_employee_crash):
read_group_result = self.env['mail.activity'].with_user(self.user_employee).read_group(
[('id', '=', test_activity.id)],
['summary'],
['summary'],
)
self.assertEqual(1, read_group_result[0]['summary_count'])
self.assertEqual('Summary', read_group_result[0]['summary'])
@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)})
self.assertEqual(test_record.activity_state, 'overdue')
test_record.activity_ids.write({'date_deadline': date.today()})
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 the following 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)
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['ir.model']._get_id('mail.test.activity'), default_res_id=self.test_record.id)) as ActivityForm:
# `res_model_id` and `res_id` are invisible, see view `mail.mail_activity_view_form_popup`
# they must be set using defaults, see `action_feedback_schedule_next`
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)
@mute_logger('odoo.sql_db')
def test_activity_values(self):
""" Test activities are created with right model / res_id values linking
to records without void values. 0 as res_id especially is not wanted. """
# creating activities on a temporary record generates activities with res_id
# being 0, which is annoying -> never create activities in transient mode
temp_record = self.env['mail.test.activity'].new({'name': 'Test'})
with self.assertRaises(IntegrityError):
activity = temp_record.activity_schedule('test_mail.mail_act_test_todo', user_id=self.user_employee.id)
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
with self.assertRaises(IntegrityError):
self.env['mail.activity'].create({
'res_model_id': self.env['ir.model']._get_id(test_record._name),
})
with self.assertRaises(IntegrityError):
self.env['mail.activity'].create({
'res_model_id': self.env['ir.model']._get_id(test_record._name),
'res_id': False,
})
with self.assertRaises(IntegrityError):
self.env['mail.activity'].create({
'res_id': test_record.id,
})
activity = self.env['mail.activity'].create({
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id(test_record._name),
})
with self.assertRaises(IntegrityError):
activity.write({'res_model_id': False})
self.env.flush_all()
with self.assertRaises(IntegrityError):
activity.write({'res_id': False})
self.env.flush_all()
with self.assertRaises(IntegrityError):
activity.write({'res_id': 0})
self.env.flush_all()
@tests.tagged('mail_activity')
class TestActivityMixin(TestActivityCommon):
@classmethod
def setUpClass(cls):
super(TestActivityMixin, cls).setUpClass()
cls.user_utc = mail_new_test_user(
cls.env,
name='User UTC',
login='User UTC',
)
cls.user_utc.tz = 'UTC'
cls.user_australia = mail_new_test_user(
cls.env,
name='user Australia',
login='user Australia',
)
cls.user_australia.tz = 'Australia/Sydney'
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin(self):
self.user_employee.tz = self.user_admin.tz
with self.with_user('employee'):
self.test_record = self.env['mail.test.activity'].browse(self.test_record.id)
self.assertEqual(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_recordset(['activity_ids'])
self.assertEqual(self.test_record.activity_user_id, self.user_employee)
act3 = self.test_record.activity_schedule(
'test_mail.mail_act_test_todo',
today_user + relativedelta(days=3),
user_id=self.user_employee.id)
self.assertEqual(self.test_record.activity_state, 'overdue')
# `activity_user_id` is defined as `fields.Many2one('res.users', 'Responsible User', related='activity_ids.user_id')`
# it therefore relies on the natural order of `activity_ids`, according to which activity comes first.
# As we just created the activity, its not yet in the right order.
# We force it by invalidating it so it gets fetched from database, in the right order.
self.test_record.invalidate_recordset(['activity_ids'])
self.assertEqual(self.test_record.activity_user_id, self.user_employee)
self.test_record.invalidate_recordset()
self.assertEqual(self.test_record.activity_ids, act1 | act2 | act3)
# Perform todo activities for admin
self.test_record.activity_feedback(
['test_mail.mail_act_test_todo'],
user_id=self.user_admin.id,
feedback='Test feedback',)
self.assertEqual(self.test_record.activity_ids, act2 | act3)
self.assertFalse(act1.exists())
# 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')
self.assertFalse(act3.exists())
# 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)
self.assertFalse(self.test_record.activity_state)
self.assertFalse(act2.exists())
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin_archive(self):
rec = self.test_record.with_user(self.user_employee)
new_act = rec.activity_schedule(
'test_mail.mail_act_test_todo',
user_id=self.user_admin.id)
self.assertEqual(rec.activity_ids, new_act)
rec.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_archive_user(self):
"""
Test when archiving an user, we unlink all his related activities
"""
test_users = self.env['res.users']
for i in range(5):
test_users += mail_new_test_user(self.env, name=f'test_user_{i}', login=f'test_password_{i}')
for user in test_users:
self.test_record.activity_schedule(user_id=user.id)
archived_users = self.env['res.users'].browse(map(lambda x: x.id, random.sample(test_users, 2))) # pick 2 users to archive
archived_users.action_archive()
active_users = test_users - archived_users
activities = self.env['mail.activity'].search([('user_id', 'in', archived_users.ids)])
self.assertFalse(activities, "Activities of archived users should be deleted.")
# activities of active users shouldn't be touched, each has exactly 1 activity present
activities = self.env['mail.activity'].search([('user_id', 'in', active_users.ids)])
self.assertEqual(len(activities), 3, "We should have only 3 activities in total linked to our active users")
self.assertEqual(activities.mapped('user_id'), active_users,
"We should have 3 different users linked to the activities of the active users")
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin_reschedule_user(self):
rec = self.test_record.with_user(self.user_employee)
rec.activity_schedule(
'test_mail.mail_act_test_todo',
user_id=self.user_admin.id)
self.assertEqual(rec.activity_ids[0].user_id, self.user_admin)
# reschedule its own should not alter other's activities
rec.activity_reschedule(
['test_mail.mail_act_test_todo'],
user_id=self.user_employee.id,
new_user_id=self.user_employee.id)
self.assertEqual(rec.activity_ids[0].user_id, self.user_admin)
rec.activity_reschedule(
['test_mail.mail_act_test_todo'],
user_id=self.user_admin.id,
new_user_id=self.user_employee.id)
self.assertEqual(rec.activity_ids[0].user_id, self.user_employee)
@users('employee')
def test_feedback_w_attachments(self):
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
activity = self.env['mail.activity'].create({
'activity_type_id': 1,
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
attachments = self.env['ir.attachment'].create([{
'name': 'test',
'res_name': 'test',
'res_model': 'mail.activity',
'res_id': activity.id,
'datas': 'test',
}, {
'name': 'test2',
'res_name': 'test',
'res_model': 'mail.activity',
'res_id': activity.id,
'datas': 'testtest',
}])
# Checking if the attachment has been forwarded to the message
# when marking an activity as "Done"
activity.action_feedback()
activity_message = test_record.message_ids[-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)
@users('employee')
def test_feedback_chained_current_date(self):
frozen_now = datetime(2021, 10, 10, 14, 30, 15)
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
first_activity = self.env['mail.activity'].create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_chained_1').id,
'date_deadline': frozen_now + relativedelta(days=-2),
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
first_activity_id = first_activity.id
with freeze_time(frozen_now):
first_activity.action_feedback(feedback='Done')
self.assertFalse(first_activity.exists())
# check chained activity
new_activity = test_record.activity_ids
self.assertNotEqual(new_activity.id, first_activity_id)
self.assertEqual(new_activity.summary, 'Take the second step.')
self.assertEqual(new_activity.date_deadline, frozen_now.date() + relativedelta(days=10))
@users('employee')
def test_feedback_chained_previous(self):
self.env.ref('test_mail.mail_act_test_chained_2').sudo().write({'delay_from': 'previous_activity'})
frozen_now = datetime(2021, 10, 10, 14, 30, 15)
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
first_activity = self.env['mail.activity'].create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_chained_1').id,
'date_deadline': frozen_now + relativedelta(days=-2),
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
first_activity_id = first_activity.id
with freeze_time(frozen_now):
first_activity.action_feedback(feedback='Done')
self.assertFalse(first_activity.exists())
# check chained activity
new_activity = test_record.activity_ids
self.assertNotEqual(new_activity.id, first_activity_id)
self.assertEqual(new_activity.summary, 'Take the second step.')
self.assertEqual(new_activity.date_deadline, frozen_now.date() + relativedelta(days=8),
'New deadline should take into account original activity deadline, not current date')
def test_mail_activity_state(self):
"""Create 3 activity for 2 different users in 2 different timezones.
User UTC (+0h)
User Australia (+11h)
Today datetime: 1/1/2020 16h
Activity 1 & User UTC
1/1/2020 - 16h UTC -> The state is today
Activity 2 & User Australia
1/1/2020 - 16h UTC
2/1/2020 - 1h Australia -> State is overdue
Activity 3 & User UTC
1/1/2020 - 23h UTC -> The state is today
"""
today_utc = datetime(2020, 1, 1, 16, 0, 0)
class MockedDatetime(datetime):
@classmethod
def utcnow(cls):
return today_utc
record = self.env['mail.test.activity'].create({'name': 'Record'})
with patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime):
activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': 1,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': record.id,
'date_deadline': today_utc,
'user_id': self.user_utc.id,
})
activity_2 = activity_1.copy()
activity_2.user_id = self.user_australia
activity_3 = activity_1.copy()
activity_3.date_deadline += relativedelta(hours=7)
self.assertEqual(activity_1.state, 'today')
self.assertEqual(activity_2.state, 'overdue')
self.assertEqual(activity_3.state, 'today')
@users('employee')
def test_mail_activity_mixin_search_activity_user_id_false(self):
"""Test the search method on the "activity_user_id" when searching for non-set user"""
MailTestActivity = self.env['mail.test.activity']
test_records = self.test_record | self.test_record_2
self.assertFalse(test_records.activity_ids)
self.assertEqual(MailTestActivity.search([('activity_user_id', '=', False)]), test_records)
self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': self.test_record.id,
})
self.assertEqual(MailTestActivity.search([('activity_user_id', '!=', True)]), self.test_record_2)
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
def test_mail_activity_mixin_search_state_basic(self):
"""Test the search method on the "activity_state".
Test all the operators and also test the case where the "activity_state" is
different because of the timezone. There's also a tricky case for which we
"reverse" the domain for performance purpose.
"""
today_utc = datetime(2020, 1, 1, 16, 0, 0)
class MockedDatetime(datetime):
@classmethod
def utcnow(cls):
return today_utc
# Create some records without activity schedule on it for testing
self.env['mail.test.activity'].create([
{'name': 'Record %i' % record_i}
for record_i in range(5)
])
origin_1, origin_2 = self.env['mail.test.activity'].search([], limit=2)
activity_type = self.env.ref('test_mail.mail_act_test_todo')
activity_type.sudo().keep_done = True
with patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime), \
patch('odoo.addons.mail.models.mail_activity_mixin.datetime', MockedDatetime):
origin_1_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': activity_type.id,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_1.id,
'date_deadline': today_utc,
'user_id': self.user_utc.id,
})
origin_1_activity_2 = origin_1_activity_1.copy()
origin_1_activity_2.user_id = self.user_australia
origin_1_activity_3 = origin_1_activity_1.copy()
origin_1_activity_3.date_deadline += relativedelta(hours=8)
self.assertEqual(origin_1_activity_1.state, 'today')
self.assertEqual(origin_1_activity_2.state, 'overdue')
self.assertEqual(origin_1_activity_3.state, 'today')
origin_2_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': activity_type.id,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_2.id,
'date_deadline': today_utc + relativedelta(hours=8),
'user_id': self.user_utc.id,
})
origin_2_activity_2 = origin_2_activity_1.copy()
origin_2_activity_2.user_id = self.user_australia
origin_2_activity_3 = origin_2_activity_1.copy()
origin_2_activity_3.date_deadline -= relativedelta(hours=8)
origin_2_activity_4 = origin_2_activity_1.copy()
origin_2_activity_4.date_deadline = datetime(2020, 1, 2, 0, 0, 0)
self.assertEqual(origin_2_activity_1.state, 'planned')
self.assertEqual(origin_2_activity_2.state, 'today')
self.assertEqual(origin_2_activity_3.state, 'today')
self.assertEqual(origin_2_activity_4.state, 'planned')
all_activity_mixin_record = self.env['mail.test.activity'].search([])
result = self.env['mail.test.activity'].search([('activity_state', '=', 'today')])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state == 'today'))
result = self.env['mail.test.activity'].search([('activity_state', 'in', ('today', 'overdue'))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state in ('today', 'overdue')))
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('today',))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state not in ('today',)))
result = self.env['mail.test.activity'].search([('activity_state', '=', False)])
self.assertTrue(len(result) >= 3, "There is more than 3 records without an activity schedule on it")
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: not p.activity_state))
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('planned', 'overdue', 'today'))])
self.assertTrue(len(result) >= 3, "There is more than 3 records without an activity schedule on it")
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: not p.activity_state))
# test tricky case when the domain will be reversed in the search method
# because of falsy value
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('today', False))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state not in ('today', False)))
result = self.env['mail.test.activity'].search([('activity_state', 'in', ('today', False))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state in ('today', False)))
# Check that activity done are not taken into account by group and search by activity_state.
Model = self.env['mail.test.activity']
search_params = {
'domain': [('id', 'in', (origin_1 | origin_2).ids), ('activity_state', '=', 'overdue')]}
read_group_params = {'domain': [('id', 'in', (origin_1 | origin_2).ids)], 'fields': ['id:array_agg'],
'groupby': ['activity_state']}
self.assertEqual(Model.search(**search_params), origin_1)
self.assertEqual(
{(e['activity_state'], e['activity_state_count']) for e in Model.read_group(**read_group_params)},
{('today', 1), ('overdue', 1)})
origin_1_activity_2.action_feedback(feedback='Done')
self.assertFalse(Model.search(**search_params))
self.assertEqual(
{(e['activity_state'], e['activity_state_count']) for e in Model.read_group(**read_group_params)},
{('today', 2)})
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
def test_mail_activity_mixin_search_state_different_day_but_close_time(self):
"""Test the case where there's less than 24 hours between the deadline and now_tz,
but one day of difference (e.g. 23h 01/01/2020 & 1h 02/02/2020). So the state
should be "planned" and not "today". This case was tricky to implement in SQL
that's why it has its own test.
"""
today_utc = datetime(2020, 1, 1, 23, 0, 0)
class MockedDatetime(datetime):
@classmethod
def utcnow(cls):
return today_utc
# Create some records without activity schedule on it for testing
self.env['mail.test.activity'].create([
{'name': 'Record %i' % record_i}
for record_i in range(5)
])
origin_1 = self.env['mail.test.activity'].search([], limit=1)
with patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime):
origin_1_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': 1,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_1.id,
'date_deadline': today_utc + relativedelta(hours=2),
'user_id': self.user_utc.id,
})
self.assertEqual(origin_1_activity_1.state, 'planned')
result = self.env['mail.test.activity'].search([('activity_state', '=', 'today')])
self.assertNotIn(origin_1, result, 'The activity state miss calculated during the search')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_my_activity_flow_employee(self):
Activity = self.env['mail.activity']
date_today = date.today()
Activity.create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
'date_deadline': date_today,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': self.test_record.id,
'user_id': self.user_admin.id,
})
Activity.create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_call').id,
'date_deadline': date_today + relativedelta(days=1),
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': self.test_record.id,
'user_id': self.user_employee.id,
})
test_record_1 = self.env['mail.test.activity'].with_context(self._test_context).create({'name': 'Test 1'})
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)
@tests.tagged("mail_activity")
class TestActivitySystray(TestActivityCommon, HttpCase):
"""Test for systray_get_activities"""
@freeze_time("2024-01-15 14:00:00 UTC")
@users("employee")
def test_systray_activities_for_archived_records(self):
"""Check that activities made on archived records are shown in the
systray activities."""
self.test_record.action_archive()
self.test_record.activity_schedule(
"test_mail.mail_act_test_todo",
user_id=self.user_employee.id,
)
self.authenticate(self.user_employee.login, self.user_employee.login)
data = self.make_jsonrpc_request("/mail/data", {"systray_get_activities": True})
total_count = sum(
record["total_count"]
for record in data["Store"]["activityGroups"]
if record.get("model") == self.test_record._name
)
self.assertEqual(total_count, 1)
@tests.tagged('mail_activity')
class TestActivityViewHelpers(TestActivityCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.type_todo = cls.env.ref('test_mail.mail_act_test_todo')
cls.type_call = cls.env.ref('test_mail.mail_act_test_call')
cls.type_upload = cls.env.ref('test_mail.mail_act_test_upload_document')
cls.user_employee_2 = mail_new_test_user(
cls.env,
name='Employee2',
login='employee2',
)
cls.attachment_1, cls.attachment_2 = cls.env['ir.attachment'].create([{
'name': f"Uploaded doc_{idx + 1}",
'raw': b'bar',
'res_model': cls.test_record_2._name,
'res_id': cls.test_record_2.id,
} for idx in range(2)])
cls.upload_type = cls.env.ref('test_mail.mail_act_test_upload_document')
cls.upload_type.sudo().keep_done = True
cls.user_employee.tz = cls.user_admin.tz
@freeze_time("2023-10-18 06:00:00")
def test_get_activity_data(self):
get_activity_data = self.env['mail.activity'].get_activity_data
with self.with_user('employee'):
# Setup activities: 3 for the first record, 2 "done" and 2 ongoing for the second
test_record, test_record_2 = self.env['mail.test.activity'].browse(
(self.test_record + self.test_record_2).ids
)
now_utc = datetime.now(pytz.UTC)
now_user = now_utc.astimezone(pytz.timezone(self.env.user.tz or 'UTC'))
today_user = now_user.date()
for days, user_id in ((-1, self.user_employee_2), (0, self.user_employee), (1, self.user_admin)):
test_record.activity_schedule(
'test_mail.mail_act_test_upload_document',
today_user + relativedelta(days=days),
user_id=user_id.id)
for days, user_id in ((-2, self.user_admin), (0, self.user_employee), (2, self.user_employee_2),
(3, self.user_admin)):
test_record_2.activity_schedule(
'test_mail.mail_act_test_upload_document',
today_user + relativedelta(days=days),
user_id=user_id.id)
record_activities = test_record.activity_ids
record_2_activities = test_record_2.activity_ids
record_2_activities[0].action_feedback(feedback='Done', attachment_ids=self.attachment_1.ids)
record_2_activities[1].action_feedback(feedback='Done', attachment_ids=self.attachment_2.ids)
# Check get activity data
activity_data = get_activity_data('mail.test.activity', None, fetch_done=True)
self.assertEqual(activity_data['activity_res_ids'], [test_record.id, test_record_2.id])
self.assertDictEqual(
next((t for t in activity_data['activity_types'] if t['id'] == self.upload_type.id), {}),
{
'id': self.upload_type.id,
'name': 'Upload Document',
'template_ids': [],
'keep_done': True,
})
grouped = activity_data['grouped_activities'][test_record.id][self.upload_type.id]
grouped['ids'] = set(grouped['ids']) # ids order doesn't matter
self.assertDictEqual(grouped, {
'state': 'overdue',
'count_by_state': {'overdue': 1, 'planned': 1, 'today': 1},
'ids': set(record_activities.ids),
'reporting_date': record_activities[0].date_deadline,
'user_assigned_ids': record_activities.user_id.ids,
})
grouped = activity_data['grouped_activities'][test_record_2.id][self.upload_type.id]
grouped['ids'] = set(grouped['ids'])
self.assertDictEqual(grouped, {
'state': 'planned',
'count_by_state': {'done': 2, 'planned': 2},
'ids': set(record_2_activities.ids),
'reporting_date': record_2_activities[2].date_deadline,
'user_assigned_ids': record_2_activities[2:].user_id.ids,
'attachments_info': {
'count': 2, 'most_recent_id': self.attachment_2.id, 'most_recent_name': 'Uploaded doc_2'}
})
# Mark all first record activities as "done" and check activity data
record_activities.action_feedback(feedback='Done', attachment_ids=self.attachment_1.ids)
self.assertEqual(record_activities[2].date_done, date.today()) # Thanks to freeze_time
activity_data = get_activity_data('mail.test.activity', None, fetch_done=True)
grouped = activity_data['grouped_activities'][test_record.id][self.upload_type.id]
grouped['ids'] = set(grouped['ids'])
self.assertDictEqual(grouped, {
'state': 'done',
'count_by_state': {'done': 3},
'ids': set(record_activities.ids),
'reporting_date': record_activities[2].date_done,
'user_assigned_ids': [],
'attachments_info': {
'count': 1, # 1 instead of 3 because all attachments are the same one
'most_recent_id': self.attachment_1.id,
'most_recent_name': self.attachment_1.name,
}
})
self.assertEqual(activity_data['activity_res_ids'], [test_record_2.id, test_record.id])
# Check filters (domain, pagination and fetch_done)
self.assertEqual(
get_activity_data('mail.test.activity', domain=[('id', 'in', test_record.ids)],
fetch_done=True)['activity_res_ids'],
[test_record.id])
self.assertEqual(get_activity_data('mail.test.activity', None, fetch_done=False)['activity_res_ids'],
[test_record_2.id])
# Note that the records are ordered by ids not by deadline (so we get the "wrong" order)
self.assertEqual(
get_activity_data('mail.test.activity', None, offset=1, fetch_done=True)['activity_res_ids'],
[test_record_2.id])
self.assertEqual(
get_activity_data('mail.test.activity', None, limit=1, fetch_done=True)['activity_res_ids'],
[test_record.id])
# Unset keep done and check activity data: record with only "done" activities must not be returned
self.upload_type.sudo().keep_done = False
activity_data = get_activity_data('mail.test.activity', None, fetch_done=True)
self.assertDictEqual(
next((t for t in activity_data['activity_types'] if t['id'] == self.upload_type.id), {}),
{
'id': self.upload_type.id,
'name': 'Upload Document',
'template_ids': [],
'keep_done': False,
})
self.assertEqual(activity_data['activity_res_ids'], [test_record_2.id])
# Unarchiving activities should restore the activity
record_activities.action_unarchive()
self.assertFalse(any(act.date_done for act in record_activities))
self.assertTrue(all(act.date_deadline for act in record_activities))
self.upload_type.sudo().keep_done = True
activity_data = get_activity_data('mail.test.activity', None, fetch_done=True)
grouped = activity_data['grouped_activities'][test_record.id][self.upload_type.id]
self.assertEqual(grouped['state'], 'overdue')
self.assertEqual(grouped['count_by_state'], {'overdue': 1, 'planned': 1, 'today': 1})
self.assertEqual(grouped['reporting_date'], record_activities[0].date_deadline)
self.assertEqual(activity_data['activity_res_ids'], [test_record.id, test_record_2.id])
grouped['ids'] = set(grouped['ids'])
self.assertDictEqual(grouped, {
'state': 'overdue',
'count_by_state': {'overdue': 1, 'planned': 1, 'today': 1},
'ids': set(record_activities.ids),
'reporting_date': record_activities[0].date_deadline,
'user_assigned_ids': record_activities.user_id.ids,
})
@tests.tagged('mail_activity')
class TestORM(TestActivityCommon):
"""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
"""
MailTestActivityCtx = self.env['mail.test.activity'].with_context({"lang": "en_US"})
# Don't mistake fields date and date_deadline:
# * date is just a random value
# * date_deadline defines activity_state
with freeze_time("2024-09-24 10:00:00"):
self.env['mail.test.activity'].create({
'date': '2021-05-02',
'name': "Yesterday, all my troubles seemed so far away",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Make another test super asap (yesterday)",
date_deadline=fields.Date.context_today(MailTestActivityCtx) - timedelta(days=7),
)
self.env['mail.test.activity'].create({
'date': '2021-05-09',
'name': "Things we said today",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Make another test asap",
date_deadline=fields.Date.context_today(MailTestActivityCtx),
)
self.env['mail.test.activity'].create({
'date': '2021-05-16',
'name': "Tomorrow Never Knows",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Make a test tomorrow",
date_deadline=fields.Date.context_today(MailTestActivityCtx) + timedelta(days=7),
)
domain = [('date', "!=", False)]
groupby = "date:week"
progress_bar = {
'field': 'activity_state',
'colors': {
"overdue": 'danger',
"today": 'warning',
"planned": 'success',
}
}
# call read_group to compute group names
groups = MailTestActivityCtx.read_group(domain, fields=['date'], groupby=[groupby])
progressbars = MailTestActivityCtx.read_progress_bar(domain, group_by=groupby, progress_bar=progress_bar)
self.assertEqual(len(groups), 3)
self.assertEqual(len(progressbars), 3)
# format the read_progress_bar result to get a dictionary under this
# format: {activity_state: group_name}; the original format
# (after read_progress_bar) is {group_name: {activity_state: count}}
pg_groups = {
next(state for state, count in data.items() if count): group_name
for group_name, data in progressbars.items()
}
self.assertEqual(groups[0][groupby], pg_groups["overdue"])
self.assertEqual(groups[1][groupby], pg_groups["today"])
self.assertEqual(groups[2][groupby], pg_groups["planned"])
@tests.tagged('post_install', '-at_install')
class TestTours(HttpCase):
def test_activity_view_data_with_offset(self):
self.patch(MailTestActivity, '_order', 'date desc, id desc')
MailTestActivityModel = self.env['mail.test.activity']
MailTestActivityCtx = MailTestActivityModel.with_context({"lang": "en_US"})
MailTestActivityModel.create({
'date': '2021-05-02',
'name': "Task 1",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Activity 1",
date_deadline=fields.Date.context_today(MailTestActivityCtx) - timedelta(days=7),
)
MailTestActivityModel.create({
'date': '2021-05-16',
'name': "Task 1 without activity",
})
MailTestActivityModel.create({
'date': '2021-05-09',
'name': "Task 2",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Activity 2",
date_deadline=fields.Date.context_today(MailTestActivityCtx),
)
MailTestActivityModel.create({
'date': '2021-05-16',
'name': "Task 3",
}).activity_schedule(
'test_mail.mail_act_test_todo',
summary="Activity 3",
date_deadline=fields.Date.context_today(MailTestActivityCtx) + timedelta(days=7),
)
MailTestActivityModel.create({
'date': '2021-05-16',
'name': "Task 2 without activity",
})
self.env["ir.ui.view"].create({
"name": "Test Activity View",
"model": "mail.test.activity",
"type": 'activity',
"arch": """
<activity string="OrderedMailTestActivity">
<templates>
<div t-name="activity-box">
<field name="name"/>
</div>
</templates>
</activity>
""",
})
self.start_tour(
"/odoo?debug=1",
"mail_activity_view",
login="admin",
)