diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index ad92e946439..c6685d10551 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -558,6 +558,7 @@
+
+
ARRAY[activity_state]
+ """
+
+ self._cr.execute(
+ query,
+ {
+ 'today_utc': pytz.UTC.localize(datetime.utcnow()),
+ 'res_model_table': self._name,
+ 'search_states_int': list(search_states_int)
+ },
+ )
+ return [('id', 'not in' if reverse_search else 'in', [r[0] for r in self._cr.fetchall()])]
+
@api.depends('activity_ids.date_deadline')
def _compute_activity_date_deadline(self):
for record in self:
diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py
index 64f77806164..804a5f5a397 100644
--- a/addons/test_mail/tests/test_mail_activity.py
+++ b/addons/test_mail/tests/test_mail_activity.py
@@ -9,6 +9,7 @@ from unittest.mock import DEFAULT
import pytz
from odoo import exceptions, tests
+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 MailTestActivity
from odoo.tools import mute_logger
@@ -218,6 +219,24 @@ class TestActivityFlow(TestActivityCommon):
@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/ACT'
+
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_activity_mixin(self):
self.user_employee.tz = self.user_admin.tz
@@ -333,3 +352,177 @@ class TestActivityMixin(TestActivityCommon):
user_id=self.user_admin.id,
new_user_id=self.user_employee.id)
self.assertEqual(rec.activity_ids[0].user_id, self.user_employee)
+
+ 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')
+
+ 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)
+
+ 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,
+ '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': 1,
+ '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)))
+
+ 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')