328 lines
16 KiB
Python
328 lines
16 KiB
Python
# -*- 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 = '<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
|
|
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("<ul><li>To-Do: Plan training</li><li>To-Do: Training</li></ul>", form.plan_summary)
|
|
self.assertTrue(form._get_modifier('plan_on_demand_user_id', 'invisible'))
|
|
form.plan_id = self.plan_party
|
|
self.assertEqual("<ul><li>To-Do: Book a place</li><li>To-Do: Invite special guest</li></ul>",
|
|
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"<ul><li>To-Do: Book a place ({deadline_1})</li>"
|
|
f"<li>To-Do: Invite special guest ({deadline_2})</li></ul>")
|
|
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))
|