# Part of Odoo. See LICENSE file for full copyright and licensing details. from freezegun import freeze_time from dateutil.relativedelta import relativedelta from odoo.fields import Datetime from odoo.tests import new_test_user, tagged from odoo.addons.project.tests.test_project_base import TestProjectCommon from odoo.addons.project.models.project_task import CLOSED_STATES @tagged('-at_install', 'post_install') class TestTaskGanttView(TestProjectCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.user_gantt_test_1, cls.user_gantt_test_2, cls.user_gantt_test_3 = ( new_test_user(cls.env, login=f'ganttviewuser{i}', groups='project.group_project_user') for i in range(1, 4) ) cls.project_gantt_test_1, cls.project_gantt_test_2 = cls.env['project.project'].create([{ 'name': 'Project Gantt View Test', }] * 2) cls.tasks = cls.env['project.task'].create([ { 'name': 'Test gantt 1', 'project_id': cls.project_gantt_test_1.id, 'user_ids': False, }, { 'name': 'Test gantt 2', 'project_id': cls.project_gantt_test_1.id, 'user_ids': False, }, ]) cls.env['project.task'].create([ { 'name': 'Task 1', 'project_id': cls.project_gantt_test_1.id, 'partner_id': cls.partner_1.id, 'planned_date_begin': Datetime.to_datetime('2023-01-02'), 'date_deadline': Datetime.to_datetime('2023-01-03'), }, { 'name': 'Task 2', 'project_id': cls.project_gantt_test_2.id, 'partner_id': cls.partner_2.id, 'planned_date_begin': Datetime.to_datetime('2023-01-05'), 'date_deadline': Datetime.to_datetime('2023-01-06'), }, ]) def test_group_expand_project_ids(self): """ Validate that a project with tasks planned in the last or current period is displayed in the Gantt view. Case 1: Default domain - Ensure `self.project_gantt_test_1` is displayed in the Gantt view as its tasks fall within the domain (2023-01-01 to 2023-01-04). - Ensure `self.project_gantt_test_2` is not displayed as its tasks fall outside the domain. Case 2: Explicit filter - Ensure `project_test` is displayed in the Gantt view when explicitly filtered using the domain (`project_id` contains 'Project Test'). """ project_test = self.env['project.project'].create({'name': 'Project Test'}) domain = [ ('planned_date_begin', '>=', Datetime.to_datetime('2023-01-01')), ('date_deadline', '<=', Datetime.to_datetime('2023-01-04')), ] Task = self.env['project.task'].with_context({ 'gantt_start_date': Datetime.to_datetime('2023-02-01'), 'gantt_scale': 'week', }) gantt_projects_data = Task._group_expand_project_ids(None, domain) self.assertTrue(self.project_gantt_test_1 in gantt_projects_data, 'Project 1 should be displayed in the Gantt view') self.assertFalse(self.project_gantt_test_2 in gantt_projects_data, 'Project 2 should not be displayed in the Gantt view') gantt_projects_data = Task._group_expand_project_ids( None, [('project_id', 'ilike', 'Project Test'), ('project_id', '!=', False)] + domain, ) self.assertTrue(project_test in gantt_projects_data, 'Project Test should be displayed in the Gantt view') def test_group_expand_project_ids_dynamic_dates(self): project_test = self.env['project.project'].create({'name': 'Project Test'}) domain = [ ('project_id', 'ilike', 'Project Test'), ('planned_date_begin', '>=', 'today'), ('date_deadline', '>=', 'today +2d'), ] self.env['project.task'].create([ { 'name': 'Task', 'project_id': project_test.id, 'partner_id': self.partner_1.id, 'planned_date_begin': Datetime.now(), 'date_deadline': Datetime.now() + relativedelta(days=5), } ]) Task = self.env['project.task'].with_context({ 'gantt_start_date': Datetime.now(), 'gantt_scale': 'week', }) gantt_projects_data = Task._group_expand_project_ids(None, domain) self.assertEqual(gantt_projects_data, project_test) def test_group_expand_partner_ids(self): """ Validate that a partner with tasks planned in the last or current period is displayed in the Gantt view. The test checks: Case 1: In the first call: - Partner 1 has tasks planned within the period and should be displayed. - Partner 2 has tasks planned outside the period and should not be displayed. Case 2: In the second call (with a partner_id filter applied): - Partner 3, who has no tasks but matches the name filter, should still be displayed. """ Task = self.env['project.task'].with_context({ 'gantt_start_date': Datetime.to_datetime('2023-02-01'), 'gantt_scale': 'week', }) domain = [ ('planned_date_begin', '>=', Datetime.to_datetime('2023-01-01')), ('date_deadline', '<=', Datetime.to_datetime('2023-01-04')), ] gantt_partners_data = Task._group_expand_partner_ids(None, domain) self.assertTrue(self.partner_1 in gantt_partners_data, 'Partner 1 should be displayed in the Gantt view') self.assertFalse(self.partner_2 in gantt_partners_data, 'Partner 2 should be displayed in the Gantt view') gantt_partners_data = Task._group_expand_partner_ids(None, [('partner_id', 'ilike', 'Poilboeuf')] + domain) self.assertEqual(self.partner_3, gantt_partners_data,'Partner 3 should be displayed in the Gantt view') def test_empty_line_task_last_period(self): """ In the gantt view of the tasks of a project, there should be an empty line for a user if they have a task planned in the last or current period for that project, whether or not is open. """ self.env['project.task'].with_context({'mail_create_nolog': True}).create([{ 'name': 'Proute', 'user_ids': user, 'project_id': self.project_gantt_test_1.id, 'state': state, 'planned_date_begin': planned_date, 'date_deadline': planned_date, } for user, state, planned_date in [ (self.user_gantt_test_1, '1_done', Datetime.to_datetime('2023-01-01')), (self.user_gantt_test_2, '01_in_progress', Datetime.to_datetime('2023-01-01')), (self.user_gantt_test_3, '1_done', Datetime.to_datetime('2023-02-15')), ]]) domain = [ ('project_id', '=', self.project_gantt_test_1.id), ('state', 'not in', list(CLOSED_STATES)), ] displayed_gantt_users = self.env['project.task'].with_context({ 'gantt_start_date': Datetime.to_datetime('2023-02-01'), 'gantt_scale': 'month', })._group_expand_user_ids(None, domain) self.assertTrue(self.user_gantt_test_1 in displayed_gantt_users, 'There should be an empty line for test user 1') self.assertTrue(self.user_gantt_test_2 in displayed_gantt_users, 'There should be an empty line for test user 2') self.assertTrue(self.user_gantt_test_3 in displayed_gantt_users, 'There should be an empty line for test user 3') def test_empty_line_task_last_period_all_tasks(self): """ In the gantt view of the 'All Tasks' action, there should be an empty line for a user if they have a task planned in the last or current period for any project (private tasks are excluded), whether or not that task is open. """ self.env['project.task'].with_context({'mail_create_nolog': True}).create([{ 'name': 'Proute', 'user_ids': user, 'project_id': project_id, 'state': '1_done', 'planned_date_begin': planned_date, 'date_deadline': planned_date, } for project_id, user, planned_date in [ (self.project_gantt_test_1.id, self.user_gantt_test_1, Datetime.to_datetime('2023-01-01')), (self.project_gantt_test_2.id, self.user_gantt_test_2, Datetime.to_datetime('2023-01-02')), (False, self.user_gantt_test_3, Datetime.to_datetime('2023-01-01')) ]]) displayed_gantt_users = self.env['project.task'].with_context({ 'gantt_start_date': Datetime.to_datetime('2023-01-02'), 'gantt_scale': 'day', })._group_expand_user_ids(None, [('state', 'not in', list(CLOSED_STATES))]) self.assertTrue(self.user_gantt_test_1 in displayed_gantt_users, 'There should be an empty line for test user 1') self.assertTrue(self.user_gantt_test_2 in displayed_gantt_users, 'There should be an empty line for test user 2') self.assertFalse(self.user_gantt_test_3 in displayed_gantt_users, 'There should be no empty line for test user 3') @freeze_time('2020-01-12') def test_get_all_deadlines(self): (self.project_pigs + self.project_goats).allow_milestones = True self.project_pigs.write({'date_start': '2020-01-05', 'date': '2020-02-10'}) self.project_goats.write({'date_start': '2019-01-01', 'date': '2020-01-15'}) project_pigs_milestone_1, project_pigs_milestone_2, dummy, project_goats_milestone_1, dummy = self.env['project.milestone'].create([ {'name': 'Milestone 1 (project pigs)', 'project_id': self.project_pigs.id, 'deadline': '2020-01-10'}, {'name': 'Milestone 2 (project pigs)', 'project_id': self.project_pigs.id, 'deadline': '2020-01-30'}, {'name': 'Milestone 3 (project pigs)', 'project_id': self.project_pigs.id, 'deadline': '2020-02-10'}, {'name': 'Milestone 1 (project goats)', 'project_id': self.project_goats.id, 'deadline': '2020-01-15'}, {'name': 'Milestone 2 (project goats)', 'project_id': self.project_goats.id, 'deadline': '2020-02-01'}, ]) results = self.env['project.task'].get_all_deadlines("2020-01-01", "2020-01-31") self.assertIsInstance(results, dict) self.assertIn('project_id', results) project_id_results = results['project_id'] self.assertEqual(len(project_id_results), 2) project_pigs_result_expected = { 'id': self.project_pigs.id, 'name': self.project_pigs.name, 'date': self.project_pigs.date, 'date_start': self.project_pigs.date_start, } project_goats_result_expected = { 'id': self.project_goats.id, 'name': self.project_goats.name, 'date': self.project_goats.date, 'date_start': self.project_goats.date_start, } for project_result in project_id_results: self.assertIn('id', project_result) if project_result['id'] == self.project_pigs.id: self.assertDictEqual(project_result, project_pigs_result_expected) else: self.assertDictEqual(project_result, project_goats_result_expected) self.assertIn('milestone_id', results) milestone_id_results = results['milestone_id'] self.assertEqual(len(milestone_id_results), 3) project_pigs_milestone_1_expected = { 'id': project_pigs_milestone_1.id, 'name': project_pigs_milestone_1.name, 'deadline': project_pigs_milestone_1.deadline, 'is_deadline_exceeded': True, 'is_reached': False, 'project_id': (self.project_pigs.id, self.project_pigs.name), } project_pigs_milestone_2_expected = { 'id': project_pigs_milestone_2.id, 'name': project_pigs_milestone_2.name, 'deadline': project_pigs_milestone_2.deadline, 'is_deadline_exceeded': False, 'is_reached': False, 'project_id': (self.project_pigs.id, self.project_pigs.name), } project_goats_milestone_1_expected = { 'id': project_goats_milestone_1.id, 'name': project_goats_milestone_1.name, 'deadline': project_goats_milestone_1.deadline, 'is_deadline_exceeded': False, 'is_reached': False, 'project_id': (self.project_goats.id, self.project_goats.name), } for milestone_result in milestone_id_results: if milestone_result['id'] == project_pigs_milestone_1.id: self.assertDictEqual(milestone_result, project_pigs_milestone_1_expected) elif milestone_result['id'] == project_pigs_milestone_2.id: self.assertDictEqual(milestone_result, project_pigs_milestone_2_expected) else: self.assertDictEqual(milestone_result, project_goats_milestone_1_expected) results = self.env['project.task'].with_context(default_project_id=self.project_pigs.id).get_all_deadlines("2020-01-01", "2020-01-31") self.assertIn('project_id', results) project_id_results = results['project_id'] self.assertEqual(len(project_id_results), 1) self.assertDictEqual(project_id_results[0], project_pigs_result_expected) self.assertIn('milestone_id', results) milestone_id_results = results['milestone_id'] self.assertEqual(len(milestone_id_results), 2) for milestone_result in milestone_id_results: if milestone_result['id'] == project_pigs_milestone_1.id: self.assertDictEqual(milestone_result, project_pigs_milestone_1_expected) else: self.assertDictEqual(milestone_result, project_pigs_milestone_2_expected) def test_plan_tasks_no_assignee_allocated_hours(self): self.tasks.write({ 'planned_date_begin': '2024-03-07 00:00:00', 'date_deadline': '2024-03-08 23:59:59', }) # capacity of 16h self.assertEqual(self.tasks.mapped('allocated_hours'), [16.0, 16.0], 'The allocated hours should be 16.0 both') def test_capacity_split_allocated_hours(self): self.tasks.write({ 'user_ids': [self.project_gantt_test_1.user_id.id], 'planned_date_begin': '2024-03-07 00:00:00', 'date_deadline': '2024-03-08 23:59:59', }) # capacity of 16h, should be split in two (8h for both tasks) self.assertEqual(self.tasks.mapped('allocated_hours'), [8.0, 8.0], 'The allocated hours should be 8.0 both') def test_no_recompute_allocated_hours_present(self): self.tasks.write({ 'allocated_hours': 12.0, # set allocated hours to 12 'planned_date_begin': '2024-03-07 00:00:00', 'date_deadline': '2024-03-08 23:59:59', }) self.assertEqual(self.tasks.mapped('allocated_hours'), [12.0, 12.0], 'The tasks\'s allocated hours shouldn\'t have been recomputed since the allocated hours are being/already set') def test_gantt_view_with_empty_schedule(self): """Ensure the Gantt view loads correctly even if a user has no working schedule""" self.env.company.resource_calendar_id = False project = self.env['project.project'].create({ 'name': 'Test Project', }) no_schedule_user = self.env['res.users'].create({ 'name': 'Test User No Calendar', 'login': 'test_no_calendar', 'group_ids': [(6, 0, [self.env.ref('base.group_user').id])], 'company_id': self.env.company.id, }) task = self.env['project.task'].new({ 'project_id': project.id, 'name': 'Test Task', 'planned_date_begin': Datetime.now() - relativedelta(days=30), 'date_deadline': Datetime.now() + relativedelta(days=30), 'user_ids': [(6, 0, [no_schedule_user.id])], }) task._compute_allocated_hours() def test_gantt_view_company_calendar_flexible_hours(self): """Ensure the Gantt view handles correctly company calendars with flexible hours""" company_calendar = self.env.company.resource_calendar_id company_calendar.write({'flexible_hours': True}) Task = self.env['project.task'] group_by = ['user_ids'] start_date, stop_date = Datetime.now().strftime('%Y-%m-%d 00:00:00'), (Datetime.now() + relativedelta(days=30)).strftime('%Y-%m-%d 23:59:59') gantt_data = Task.get_gantt_data([], group_by, {'display_name': {}}, unavailability_fields=group_by, progress_bar_fields=group_by, start_date=start_date, stop_date=stop_date, scale='month') self.assertEqual(gantt_data["unavailabilities"]['user_ids'][False], [], 'There should be no unavailability intervals when the company calendar has flexible hours.') def test_gantt_unavailability_flexible_employee_no_leaves(self): """Flex employee with no leaves should have no Gantt unavailabilities.""" flex_calendar = self.env['resource.calendar'].create({ 'name': 'Flexible Test Calendar', 'flexible_hours': True, }) self.env['resource.resource'].create({ 'name': self.user_gantt_test_1.name, 'user_id': self.user_gantt_test_1.id, 'calendar_id': flex_calendar.id, 'company_id': self.env.company.id, }) start = Datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) stop = start + relativedelta(days=30) result = self.env['project.task']._gantt_unavailability( 'user_ids', [self.user_gantt_test_1.id], start, stop, 'month' ) self.assertEqual( result[self.user_gantt_test_1.id], [], "Flex employee with no leaves should have no unavailabilities.", )