356 lines
18 KiB
Python
356 lines
18 KiB
Python
# 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.",
|
|
)
|