oca-ocb-test/odoo-bringout-oca-ocb-test_mail/test_mail/tests/test_mail_activity_plan.py
Ernad Husremovic d9452d2060 19.0 vanilla
2026-03-09 09:32:39 +01:00

410 lines
20 KiB
Python

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