# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import timedelta from dateutil.relativedelta import relativedelta from freezegun import freeze_time from odoo import Command, fields from odoo.addons.mail.tests.test_mail_activity import ActivityScheduleCase from odoo.exceptions import ValidationError from odoo.tests import Form, tagged, users from odoo.tools.misc import format_date @tagged('mail_activity', 'mail_activity_plan') class TestActivitySchedule(ActivityScheduleCase): """ Test plan and activity schedule - activity scheduling on a single record and in batch - plan scheduling on a single record and in batch - plan creation and consistency """ @classmethod def setUpClass(cls): super().setUpClass() # prepare plans cls.plan_party = cls.env['mail.activity.plan'].create({ 'name': 'Test Plan A Party', 'res_model': 'mail.test.activity', 'template_ids': [ Command.create({ 'activity_type_id': cls.activity_type_todo.id, 'delay_count': 1, 'delay_from': 'before_plan_date', 'delay_unit': 'days', 'responsible_type': 'on_demand', 'sequence': 10, 'summary': 'Book a place', }), Command.create({ 'activity_type_id': cls.activity_type_todo.id, 'delay_count': 1, 'delay_from': 'after_plan_date', 'delay_unit': 'weeks', 'responsible_id': cls.user_admin.id, 'responsible_type': 'other', 'sequence': 20, 'summary': 'Invite special guest', }), ], }) cls.plan_onboarding = cls.env['mail.activity.plan'].create({ 'name': 'Test Onboarding', 'res_model': 'mail.test.activity', 'template_ids': [ Command.create({ 'activity_type_id': cls.activity_type_todo.id, 'delay_count': 3, 'delay_from': 'before_plan_date', 'delay_unit': 'days', 'responsible_id': cls.user_admin.id, 'responsible_type': 'other', 'sequence': 10, 'summary': 'Plan training', }), Command.create({ 'activity_type_id': cls.activity_type_todo.id, 'delay_count': 2, 'delay_from': 'after_plan_date', 'delay_unit': 'weeks', 'responsible_id': cls.user_admin.id, 'responsible_type': 'other', 'sequence': 20, 'summary': 'Training', }), ] }) # test records cls.reference_now = fields.Datetime.from_string('2023-09-30 14:00:00') cls.test_records = cls.env['mail.test.activity'].create([ { 'date': cls.reference_now + timedelta(days=(idx - 10)), 'email_from': f'customer.activity.{idx}@test.example.com', 'name': f'test_record_{idx}' } for idx in range(5) ]) @users('employee') def test_activity_schedule(self): """ Test schedule of an activity on a single or multiple records. """ test_records_all = [self.test_records[0], self.test_records[:3]] for test_idx, test_case in enumerate(['mono', 'multi']): test_records = test_records_all[test_idx].with_env(self.env) with self.subTest(test_case=test_case, test_records=test_records): # 1. SCHEDULE ACTIVITIES with freeze_time(self.reference_now): form = self._instantiate_activity_schedule_wizard(test_records) form.summary = 'Write specification' form.note = '

Useful link ...

' form.activity_user_id = self.user_admin with self._mock_activities(): form.save().action_schedule_activities() for record in test_records: self.assertActivityCreatedOnRecord(record, { 'activity_type_id': self.activity_type_todo, 'automated': False, 'date_deadline': self.reference_now.date() + timedelta(days=4), # activity type delay 'note': '

Useful link ...

', 'summary': 'Write specification', 'user_id': self.user_admin, }) # 2. LOG DONE ACTIVITIES with freeze_time(self.reference_now): form = self._instantiate_activity_schedule_wizard(test_records) form.activity_type_id = self.activity_type_call with self._mock_activities(), freeze_time(self.reference_now): form.save().with_context( mail_activity_quick_update=True ).action_schedule_activities_done() for record in test_records: self.assertActivityDoneOnRecord(record, self.activity_type_call) # 3. LOG DONE ACTIVITIES, PREPARE SCHEDULE NEXT with freeze_time(self.reference_now): form = self._instantiate_activity_schedule_wizard(test_records) form.activity_type_id = self.activity_type_todo with self._mock_activities(): wizard_res = form.save().with_context( mail_activity_quick_update=True ).action_schedule_activities_done_and_schedule() self.assertDictEqual(wizard_res, { 'name': "Schedule Activity On Selected Records" if len(test_records) > 1 else "Schedule Activity", 'context': { 'active_id': test_records[0].id, 'active_ids': test_records.ids, 'active_model': test_records._name, 'mail_activity_quick_update': True, 'default_previous_activity_type_id': 4, 'activity_previous_deadline': self.reference_now.date() + timedelta(days=4), 'default_res_ids': repr(test_records.ids), 'default_res_model': test_records._name, }, 'view_mode': 'form', 'res_model': 'mail.activity.schedule', 'views': [(False, 'form')], 'type': 'ir.actions.act_window', 'target': 'new', }) for record in test_records: self.assertActivityDoneOnRecord(record, self.activity_type_todo) # 4. CONTINUE WITH SCHEDULE ACTIVITIES # implies deadline addition on top of previous activities with freeze_time(self.reference_now): form = Form(self.env['mail.activity.schedule'].with_context(wizard_res['context'])) form.activity_type_id = self.activity_type_call with self._mock_activities(): form.save().with_context( mail_activity_quick_update=True ).action_schedule_activities() for record in test_records: self.assertActivityCreatedOnRecord(record, { 'activity_type_id': self.activity_type_call, 'automated': False, 'date_deadline': self.reference_now.date() + timedelta(days=5), # both types delays 'note': False, 'summary': False, 'user_id': self.env.user, }) # global activity creation from tests self.assertEqual(len(self.test_records[0].activity_ids), 4) self.assertEqual(len(self.test_records[1].activity_ids), 2) self.assertEqual(len(self.test_records[2].activity_ids), 2) self.assertEqual(len(self.test_records[3].activity_ids), 0) self.assertEqual(len(self.test_records[4].activity_ids), 0) @users('employee') def test_plan_mode(self): """ Test the plan_mode that allows to preselect a compatible plan. """ test_record = self.test_records[0].with_env(self.env) context = { 'active_id': test_record.id, 'active_ids': test_record.ids, 'active_model': test_record._name } plan_mode_context = {**context, 'plan_mode': True} with Form(self.env['mail.activity.schedule'].with_context(context)) as form: self.assertFalse(form.plan_id) with Form(self.env['mail.activity.schedule'].with_context(plan_mode_context)) as form: self.assertEqual(form.plan_id, self.plan_party) # should select only model-plans self.plan_party.res_model = 'res.partner' with Form(self.env['mail.activity.schedule'].with_context(plan_mode_context)) as form: self.assertEqual(form.plan_id, self.plan_onboarding) @users('employee') def test_plan_schedule(self): """ Test schedule of a plan on a single or multiple records. """ test_records_all = [self.test_records[0], self.test_records[:3]] for test_idx, test_case in enumerate(['mono', 'multi']): test_records = test_records_all[test_idx].with_env(self.env) with self.subTest(test_case=test_case, test_records=test_records), \ freeze_time(self.reference_now): # No plan_date specified (-> self.reference_now is used), No responsible specified form = self._instantiate_activity_schedule_wizard(test_records) self.assertFalse(form.plan_summary) form.plan_id = self.plan_onboarding self.assertEqual("", form.plan_summary) self.assertTrue(form._get_modifier('plan_on_demand_user_id', 'invisible')) form.plan_id = self.plan_party self.assertEqual("", form.plan_summary) self.assertFalse(form._get_modifier('plan_on_demand_user_id', 'invisible')) with self._mock_activities(): form.save().action_schedule_plan() self.assertPlanExecution( self.plan_party, test_records, expected_deadlines=[(self.reference_now + relativedelta(days=-1)).date(), (self.reference_now + relativedelta(days=7)).date()]) # plan_date specified, responsible specified plan_date = self.reference_now.date() + relativedelta(days=14) responsible_id = self.user_admin form = self._instantiate_activity_schedule_wizard(test_records) form.plan_id = self.plan_party form.plan_date = plan_date form.plan_on_demand_user_id = self.env['res.users'] self.assertTrue(form.has_error) self.assertIn(f'No responsible specified for {self.activity_type_todo.name}: Book a place', form.error) form.plan_on_demand_user_id = responsible_id self.assertFalse(form.has_error) deadline_1 = format_date(self.env, plan_date + relativedelta(days=-1)) deadline_2 = format_date(self.env, plan_date + relativedelta(days=7)) self.assertEqual( form.plan_summary, f"") with self._mock_activities(): form.save().action_schedule_plan() self.assertPlanExecution( self.plan_party, test_records, expected_deadlines=[plan_date + relativedelta(days=-1), plan_date + relativedelta(days=7)], expected_responsible=responsible_id) @users('admin') def test_plan_setup_model_consistency(self): """ Test the model consistency of a plan. Model consistency between activity_type - activity_template - plan: - a plan is restricted to a model - a plan contains activity plan templates which can be limited to some model through activity type """ # Setup independent activities type to avoid interference with existing data activity_type_1, activity_type_2, activity_type_3 = self.env['mail.activity.type'].create([ {'name': 'Todo'}, {'name': 'Call'}, {'name': 'Partner-specific', 'res_model': 'res.partner'}, ]) test_plan = self.env['mail.activity.plan'].create({ 'name': 'Test Plan', 'res_model': 'mail.test.activity', 'template_ids': [ (0, 0, {'activity_type_id': activity_type_1.id}), (0, 0, {'activity_type_id': activity_type_2.id}) ], }) # ok, all activities generic test_plan.res_model = 'res.partner' test_plan.res_model = 'mail.test.activity' with self.assertRaises( ValidationError, msg='Cannot set activity type to res.partner as linked to a plan of another model'): activity_type_1.res_model = 'res.partner' activity_type_1.res_model = 'mail.test.activity' with self.assertRaises( ValidationError, msg='Cannot set plan to res.partner as using activities linked to another model'): test_plan.res_model = 'res.partner' with self.assertRaises( ValidationError, msg='Cannot create activity template for res.partner as linked to a plan of another model'): self.env['mail.activity.plan.template'].create({ 'activity_type_id': activity_type_3.id, 'plan_id': test_plan.id, }) @users('admin') def test_plan_setup_validation(self): """ Test plan consistency. """ plan = self.env['mail.activity.plan'].create({ 'name': 'test', 'res_model': 'mail.test.activity', }) template = self.env['mail.activity.plan.template'].create({ 'activity_type_id': self.activity_type_todo.id, 'plan_id': plan.id, 'responsible_type': 'other', 'responsible_id': self.user_admin.id, }) template.responsible_type = 'on_demand' self.assertFalse(template.responsible_id) with self.assertRaises( ValidationError, msg='When selecting responsible "other", you must specify a responsible.'): template.responsible_type = 'other' template.write({'responsible_type': 'other', 'responsible_id': self.user_admin}) def test_plan_copy(self): """Test plan copy""" copied_plan = self.plan_onboarding.copy() self.assertEqual(copied_plan.name, f'{self.plan_onboarding.name} (copy)') self.assertEqual(len(copied_plan.template_ids), len(self.plan_onboarding.template_ids))