mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-20 11:22:03 +02:00
19.0 vanilla
This commit is contained in:
parent
a2f74aefd8
commit
4a4d12c333
844 changed files with 212348 additions and 270090 deletions
|
|
@ -4,16 +4,21 @@ from . import test_access_rights
|
|||
from . import test_burndown_chart
|
||||
from . import test_project_base
|
||||
from . import test_project_config
|
||||
from . import test_project_embedded_action_settings
|
||||
from . import test_project_flow
|
||||
from . import test_project_mail_features
|
||||
from . import test_project_milestone
|
||||
from . import test_project_profitability
|
||||
from . import test_project_recurrence
|
||||
from . import test_project_sharing
|
||||
from . import test_project_sharing_portal_access
|
||||
from . import test_project_sharing_ui
|
||||
from . import test_project_stage_multicompany
|
||||
from . import test_project_subtasks
|
||||
from . import test_project_tags_filter
|
||||
from . import test_project_task_type
|
||||
from . import test_project_template
|
||||
from . import test_project_template_ui
|
||||
from . import test_project_ui
|
||||
from . import test_project_update_access_rights
|
||||
from . import test_project_update_flow
|
||||
|
|
@ -21,8 +26,14 @@ from . import test_project_update_ui
|
|||
from . import test_portal
|
||||
from . import test_multicompany
|
||||
from . import test_personal_stages
|
||||
from . import test_res_config_settings
|
||||
from . import test_task_dependencies
|
||||
from . import test_task_follow
|
||||
from . import test_task_templates
|
||||
from . import test_task_templates_ui
|
||||
from . import test_task_tracking
|
||||
from . import test_project_report
|
||||
from . import test_project_task_quick_create
|
||||
from . import test_task_state
|
||||
from . import test_project_task_mail_tracking_duration
|
||||
from . import test_task_link_preview_name
|
||||
from . import test_import_files
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@ class TestCRUDVisibilityPortal(TestAccessRights):
|
|||
self.project_pigs.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
self.task.flush_model()
|
||||
self.task.invalidate_model()
|
||||
self.task.with_user(self.env.user).name
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to read the task"):
|
||||
self.task.with_user(self.env.user).name
|
||||
|
||||
@users('Internal user')
|
||||
def test_task_internal_read(self):
|
||||
|
|
@ -207,8 +208,8 @@ class TestAllowedUsers(TestAccessRights):
|
|||
self.project_pigs.message_unsubscribe(partner_ids=[self.user.partner_id.id])
|
||||
self.assertIn(john.partner_id, self.task.message_partner_ids)
|
||||
self.assertNotIn(john.partner_id, task.message_partner_ids)
|
||||
# Unsubscribing to a project should not cause unsubscription of existing tasks in the project.
|
||||
self.assertIn(self.user.partner_id, task.message_partner_ids)
|
||||
# Unsubscribing to a project should unsubscribing of existing tasks in the project.
|
||||
self.assertNotIn(self.user.partner_id, task.message_partner_ids)
|
||||
self.assertNotIn(self.user.partner_id, self.task.message_partner_ids)
|
||||
|
||||
def test_visibility_changed(self):
|
||||
|
|
@ -219,7 +220,7 @@ class TestAllowedUsers(TestAccessRights):
|
|||
self.assertNotIn(self.portal.partner_id, self.task.message_partner_ids, "Portal user should have been removed from allowed users")
|
||||
|
||||
def test_write_task(self):
|
||||
self.user.groups_id |= self.env.ref('project.group_project_user')
|
||||
self.user.group_ids |= self.env.ref('project.group_project_user')
|
||||
self.assertNotIn(self.user.partner_id, self.project_pigs.message_partner_ids)
|
||||
self.task.message_subscribe(partner_ids=[self.user.partner_id.id])
|
||||
self.project_pigs.invalidate_model()
|
||||
|
|
@ -227,7 +228,7 @@ class TestAllowedUsers(TestAccessRights):
|
|||
self.task.with_user(self.user).name = "I can edit a task!"
|
||||
|
||||
def test_no_write_project(self):
|
||||
self.user.groups_id |= self.env.ref('project.group_project_user')
|
||||
self.user.group_ids |= self.env.ref('project.group_project_user')
|
||||
self.assertNotIn(self.user.partner_id, self.project_pigs.message_partner_ids)
|
||||
with self.assertRaises(AccessError, msg="User should not be able to edit project"):
|
||||
self.project_pigs.with_user(self.user).name = "I can't edit a task!"
|
||||
|
|
@ -235,14 +236,14 @@ class TestAllowedUsers(TestAccessRights):
|
|||
class TestProjectPortalCommon(TestProjectCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestProjectPortalCommon, self).setUp()
|
||||
super().setUp()
|
||||
self.user_noone = self.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True}).create({
|
||||
'name': 'Noemie NoOne',
|
||||
'login': 'noemie',
|
||||
'email': 'n.n@example.com',
|
||||
'signature': '--\nNoemie',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [(6, 0, [])]})
|
||||
'group_ids': [(6, 0, [])]})
|
||||
|
||||
self.task_3 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test3', 'user_ids': self.user_portal, 'project_id': self.project_pigs.id})
|
||||
|
|
@ -413,9 +414,34 @@ class TestAccessRightsPrivateTask(TestAccessRights):
|
|||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).unlink()
|
||||
|
||||
def test_of_setting_root_user_on_private_task(self):
|
||||
test_task = self.env['project.task'].create({
|
||||
'name':'Test Private Task',
|
||||
'user_ids': [Command.link(self.user_projectuser.id)]
|
||||
})
|
||||
self.assertNotEqual(test_task.user_ids, self.env.user, "Created private task should not have odoobot as asignee")
|
||||
|
||||
class TestAccessRightsInvitedUsers(TestAccessRights):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.project_pigs.privacy_visibility = 'invited_users'
|
||||
cls.project_user = mail_new_test_user(cls.env, 'Project user', groups='project.group_project_user')
|
||||
|
||||
@users('admin')
|
||||
def test_admin_access_invited_project(self):
|
||||
self.assertFalse(self.project_pigs.collaborator_ids)
|
||||
self.assertEqual(self.project_pigs.with_user(self.env.user).name, 'Pigs')
|
||||
|
||||
@users('Project user', 'Internal user', 'Portal user')
|
||||
def test_other_users_access_invited_project(self):
|
||||
with self.assertRaises(AccessError, msg="The user is not a follower of the project, he's not supposed to have access to the project."):
|
||||
self.assertEqual(self.project_pigs.with_user(self.env.user).name, 'Pigs')
|
||||
self.project_pigs.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
self.assertEqual(self.project_pigs.with_user(self.env.user).name, 'Pigs', "The user was set as a follower of the project, he's supposed to have access to the project.")
|
||||
|
||||
@users('admin')
|
||||
def test_admin_access_invited_task(self):
|
||||
self.assertEqual(self.task.with_user(self.env.user).name, 'Make the world a better place')
|
||||
|
||||
@users('Project user', 'Internal user', 'Portal user')
|
||||
def test_other_users_access_invited_task(self):
|
||||
with self.assertRaises(AccessError, msg="The user is not a follower of the project, he's not supposed to have access to the project."):
|
||||
self.assertEqual(self.task.with_user(self.env.user).name, 'Make the world a better place')
|
||||
self.task.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
self.assertEqual(self.task.with_user(self.env.user).name, 'Make the world a better place', "The user was set as a follower of the project, he's supposed to have access to the project.")
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from freezegun import freeze_time
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import Command
|
||||
from odoo.osv.expression import AND, OR
|
||||
from odoo.fields import Command, Domain
|
||||
from odoo.tests.common import tagged, HttpCase
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
|
@ -21,48 +19,39 @@ class TestBurndownChartCommon(TestProjectCommon):
|
|||
super().setUpClass()
|
||||
cls.current_year = datetime.now().year
|
||||
create_date = datetime(cls.current_year - 1, 1, 1)
|
||||
kanban_state_vals = {
|
||||
"legend_blocked": 'Blocked',
|
||||
"legend_done": 'Ready',
|
||||
"legend_normal": 'In Progress'
|
||||
}
|
||||
Stage = cls.env['project.task.type']
|
||||
cls.todo_stage = Stage.create({
|
||||
'sequence': 1,
|
||||
'name': 'TODO',
|
||||
**kanban_state_vals,
|
||||
})
|
||||
cls.set_create_date('project_task_type', cls.todo_stage.id, create_date)
|
||||
cls.in_progress_stage = Stage.create({
|
||||
'sequence': 10,
|
||||
'name': 'In Progress',
|
||||
**kanban_state_vals,
|
||||
})
|
||||
cls.set_create_date('project_task_type', cls.in_progress_stage.id, create_date)
|
||||
cls.testing_stage = Stage.create({
|
||||
'sequence': 20,
|
||||
'name': 'Testing',
|
||||
**kanban_state_vals,
|
||||
})
|
||||
cls.set_create_date('project_task_type', cls.testing_stage.id, create_date)
|
||||
cls.done_stage = Stage.create({
|
||||
'sequence': 30,
|
||||
'name': 'Done',
|
||||
**kanban_state_vals,
|
||||
})
|
||||
cls.set_create_date('project_task_type', cls.done_stage.id, create_date)
|
||||
cls.stages = cls.todo_stage + cls.in_progress_stage + cls.testing_stage + cls.done_stage
|
||||
cls.project = cls.env['project.project'].create({
|
||||
'name': 'Burndown Chart Test',
|
||||
'privacy_visibility': 'employees',
|
||||
'alias_name': 'project+burndown_chart',
|
||||
'alias_name': 'project_burndown_chart',
|
||||
'type_ids': [Command.link(stage_id) for stage_id in cls.stages.ids],
|
||||
})
|
||||
cls.set_create_date('project_project', cls.project.id, create_date)
|
||||
cls.project.invalidate_model()
|
||||
cls.milestone = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Milestone',
|
||||
'project_id': cls.project_pigs.id,
|
||||
'project_id': cls.project.id,
|
||||
})
|
||||
cls.task_a = cls.env['project.task'].create({
|
||||
'name': 'Task A',
|
||||
|
|
@ -95,19 +84,19 @@ class TestBurndownChartCommon(TestProjectCommon):
|
|||
cls.set_create_date('project_task', cls.task_e.id, create_date)
|
||||
|
||||
# Create a new task to check if a task without changing its stage is taken into account
|
||||
task_f = cls.env['project.task'].create({
|
||||
cls.task_f = cls.env['project.task'].create({
|
||||
'name': 'Task F',
|
||||
'priority': 0,
|
||||
'project_id': cls.project.id,
|
||||
'milestone_id': cls.milestone.id,
|
||||
'stage_id': cls.todo_stage.id,
|
||||
})
|
||||
cls.set_create_date('project_task', task_f.id, datetime(cls.current_year - 1, 12, 20))
|
||||
cls.set_create_date('project_task', cls.task_f.id, datetime(cls.current_year - 1, 12, 20))
|
||||
|
||||
cls.project_2 = cls.env['project.project'].create({
|
||||
'name': 'Burndown Chart Test 2 mySearchTag',
|
||||
'privacy_visibility': 'employees',
|
||||
'alias_name': 'project+burndown_chart+2',
|
||||
'alias_name': 'project_burndown_chart_2',
|
||||
'type_ids': [Command.link(stage_id) for stage_id in cls.stages.ids],
|
||||
})
|
||||
cls.set_create_date('project_project', cls.project_2.id, create_date)
|
||||
|
|
@ -126,6 +115,41 @@ class TestBurndownChartCommon(TestProjectCommon):
|
|||
})
|
||||
cls.set_create_date('project_task', cls.task_h.id, create_date)
|
||||
|
||||
cls.stage_1, cls.stage_2, cls.stage_3, cls.stage_4 = Stage.create([{
|
||||
'sequence': 1,
|
||||
'name': '1',
|
||||
}, {
|
||||
'sequence': 10,
|
||||
'name': '2',
|
||||
}, {
|
||||
'sequence': 20,
|
||||
'name': '3',
|
||||
}, {
|
||||
'sequence': 20,
|
||||
'name': '4',
|
||||
}])
|
||||
cls.stages_bis = cls.stage_1 | cls.stage_2 | cls.stage_3 | cls.stage_4
|
||||
cls.set_create_date('project_task_type', cls.stage_1.id, create_date)
|
||||
cls.set_create_date('project_task_type', cls.stage_2.id, create_date)
|
||||
cls.set_create_date('project_task_type', cls.stage_3.id, create_date)
|
||||
cls.set_create_date('project_task_type', cls.stage_4.id, create_date)
|
||||
cls.project_1 = cls.env['project.project'].create({
|
||||
'name': 'Burndown Chart Test',
|
||||
'privacy_visibility': 'employees',
|
||||
'alias_name': 'project_burndown_chart_bis',
|
||||
'type_ids': [Command.link(stage_id) for stage_id in cls.stages_bis.ids],
|
||||
})
|
||||
cls.set_create_date('project_project', cls.project_1.id, create_date)
|
||||
cls.task_bis = cls.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'priority': 0,
|
||||
'project_id': cls.project_1.id,
|
||||
'stage_id': cls.stage_1.id,
|
||||
})
|
||||
cls.set_create_date('project_task', cls.task_bis.id, create_date)
|
||||
|
||||
cls.deleted_domain = Domain('project_id', '!=', False) & Domain('project_id', '=', cls.project_1.id)
|
||||
|
||||
# Precommit to have the records in db and allow to rollback at the end of test
|
||||
cls.env.cr.flush()
|
||||
|
||||
|
|
@ -158,178 +182,254 @@ class TestBurndownChartCommon(TestProjectCommon):
|
|||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-08-01' % (cls.current_year - 1)):
|
||||
cls.task_a.write({'stage_id': cls.done_stage.id})
|
||||
cls.task_a.write({'stage_id': cls.done_stage.id, 'state': '1_done'})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-09-10' % (cls.current_year - 1)):
|
||||
cls.task_b.write({'stage_id': cls.done_stage.id})
|
||||
cls.task_b.write({'stage_id': cls.done_stage.id, 'state': '1_done'})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-10-05' % (cls.current_year - 1)):
|
||||
cls.task_c.write({'stage_id': cls.done_stage.id})
|
||||
cls.task_c.write({'stage_id': cls.done_stage.id, 'state': '1_done'})
|
||||
cls.task_a.write({'state': '1_canceled'})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-11-25' % (cls.current_year - 1)):
|
||||
cls.task_d.write({'stage_id': cls.done_stage.id})
|
||||
cls.task_d.write({'stage_id': cls.done_stage.id, 'state': '1_done'})
|
||||
cls.task_b.write({'state': '1_canceled'})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-12-12' % (cls.current_year - 1)):
|
||||
cls.task_e.write({'stage_id': cls.done_stage.id})
|
||||
cls.task_e.write({'stage_id': cls.done_stage.id, 'state': '1_done'})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-12-24' % (cls.current_year - 1)):
|
||||
cls.task_f.write({'state': '1_canceled'})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-02-10' % (cls.current_year - 1)):
|
||||
cls.task_bis.write({'stage_id': cls.stage_2.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-03-10' % (cls.current_year - 1)):
|
||||
(cls.task_bis).write({'stage_id': cls.stage_3.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-04-10' % (cls.current_year - 1)):
|
||||
(cls.task_bis).write({'stage_id': cls.stage_4.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
class TestBurndownChart(TestBurndownChartCommon):
|
||||
|
||||
def map_read_group_result(self, read_group_result):
|
||||
return {(res['date:month'], res['stage_id'][0]): res['__count'] for res in read_group_result if res['stage_id'][1]}
|
||||
return {(res['date:month'][1], res['stage_id'][0]): int(res['__count']) for res in read_group_result if res['stage_id'][1]}
|
||||
|
||||
def map_read_group_is_closed_result(self, read_group_result):
|
||||
return {(res['date:month'][1], res['is_closed']): int(res['__count']) for res in read_group_result}
|
||||
|
||||
def check_read_group_results(self, domain, expected_results_dict):
|
||||
stages_dict = {stage.id: stage.name for stage in self.stages}
|
||||
read_group_result = self.env['project.task.burndown.chart.report'].read_group(
|
||||
domain, ['date', 'stage_id'], ['date:month', 'stage_id'], lazy=False)
|
||||
read_group_result = self.env['project.task.burndown.chart.report'].formatted_read_group(
|
||||
domain, ['date:month', 'stage_id'], ['__count'])
|
||||
read_group_result_dict = self.map_read_group_result(read_group_result)
|
||||
for (month, stage_id), __count in read_group_result_dict.items():
|
||||
expected_count = expected_results_dict.get((month, stage_id), 100000)
|
||||
self.assertEqual(
|
||||
__count,
|
||||
expected_count,
|
||||
'In %s, the number of tasks should be equal to %s in %s stage.' % (month, expected_count, stages_dict.get(stage_id, 'Unknown'))
|
||||
)
|
||||
self.assertDictEqual(read_group_result_dict, expected_results_dict)
|
||||
|
||||
def check_read_group_is_closed_results(self, domain, expected_results_dict):
|
||||
read_group_result = self.env['project.task.burndown.chart.report'].formatted_read_group(
|
||||
domain, ['date:month', 'is_closed'], ['__count'])
|
||||
read_group_result_dict = self.map_read_group_is_closed_result(read_group_result)
|
||||
self.assertDictEqual(read_group_result_dict, expected_results_dict)
|
||||
|
||||
def test_burndown_chart(self):
|
||||
burndown_chart_domain = [('display_project_id', '!=', False)]
|
||||
burndown_chart_domain = [('project_id', '!=', False)]
|
||||
project_domain = [('project_id', '=', self.project.id)]
|
||||
|
||||
# Check that we get the expected results for the complete data of `self.project`.
|
||||
project_expected_dict = {
|
||||
('January %s' % (self.current_year - 1), self.todo_stage.id): 5,
|
||||
('January %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('January %s' % (self.current_year - 1), self.testing_stage.id): 0,
|
||||
('January %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('February %s' % (self.current_year - 1), self.todo_stage.id): 2,
|
||||
('February %s' % (self.current_year - 1), self.in_progress_stage.id): 3,
|
||||
('February %s' % (self.current_year - 1), self.testing_stage.id): 0,
|
||||
('February %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('March %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('March %s' % (self.current_year - 1), self.in_progress_stage.id): 5,
|
||||
('March %s' % (self.current_year - 1), self.testing_stage.id): 0,
|
||||
('March %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('April %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('April %s' % (self.current_year - 1), self.in_progress_stage.id): 3,
|
||||
('April %s' % (self.current_year - 1), self.testing_stage.id): 2,
|
||||
('April %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('May %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('May %s' % (self.current_year - 1), self.in_progress_stage.id): 2,
|
||||
('May %s' % (self.current_year - 1), self.testing_stage.id): 3,
|
||||
('May %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('June %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('June %s' % (self.current_year - 1), self.in_progress_stage.id): 1,
|
||||
('June %s' % (self.current_year - 1), self.testing_stage.id): 4,
|
||||
('June %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('July %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('July %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('July %s' % (self.current_year - 1), self.testing_stage.id): 5,
|
||||
('July %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('August %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('August %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('August %s' % (self.current_year - 1), self.testing_stage.id): 4,
|
||||
('August %s' % (self.current_year - 1), self.done_stage.id): 1,
|
||||
('September %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('September %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('September %s' % (self.current_year - 1), self.testing_stage.id): 3,
|
||||
('September %s' % (self.current_year - 1), self.done_stage.id): 2,
|
||||
('October %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('October %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('October %s' % (self.current_year - 1), self.testing_stage.id): 2,
|
||||
('October %s' % (self.current_year - 1), self.done_stage.id): 3,
|
||||
('November %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('November %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('November %s' % (self.current_year - 1), self.testing_stage.id): 1,
|
||||
('November %s' % (self.current_year - 1), self.done_stage.id): 4,
|
||||
('December %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('December %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('December %s' % (self.current_year - 1), self.done_stage.id): 5,
|
||||
('December %s' % (self.current_year - 1), self.todo_stage.id): 1,
|
||||
('January %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('January %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('January %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('January %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('February %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('February %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('February %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('February %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('March %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('March %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('March %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('March %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('April %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('April %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('April %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('April %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('May %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('May %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('May %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('May %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('June %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('June %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('June %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('June %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('July %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('July %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('July %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('July %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('August %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('August %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('August %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('August %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('September %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('September %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('September %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('September %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('October %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('October %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('October %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('October %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('November %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('November %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('November %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('November %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('December %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('December %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('December %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('December %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('December %s' % (self.current_year - 1), self.done_stage.id): 5,
|
||||
}
|
||||
self.check_read_group_results(AND([burndown_chart_domain, project_domain]), project_expected_dict)
|
||||
project_expected_is_closed_dict = {
|
||||
('January %s' % (self.current_year - 1), 'open'): 5,
|
||||
('February %s' % (self.current_year - 1), 'open'): 5,
|
||||
('March %s' % (self.current_year - 1), 'open'): 5,
|
||||
('April %s' % (self.current_year - 1), 'open'): 5,
|
||||
('May %s' % (self.current_year - 1), 'open'): 5,
|
||||
('June %s' % (self.current_year - 1), 'open'): 5,
|
||||
('July %s' % (self.current_year - 1), 'open'): 5,
|
||||
('August %s' % (self.current_year - 1), 'open'): 4,
|
||||
('August %s' % (self.current_year - 1), 'closed'): 1,
|
||||
('September %s' % (self.current_year - 1), 'open'): 3,
|
||||
('September %s' % (self.current_year - 1), 'closed'): 2,
|
||||
('October %s' % (self.current_year - 1), 'open'): 2,
|
||||
('October %s' % (self.current_year - 1), 'closed'): 3,
|
||||
('November %s' % (self.current_year - 1), 'open'): 1,
|
||||
('November %s' % (self.current_year - 1), 'closed'): 4,
|
||||
('December %s' % (self.current_year - 1), 'closed'): 6,
|
||||
}
|
||||
months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October',
|
||||
'November', 'December']
|
||||
current_month = datetime.now().month
|
||||
for i in range(current_month):
|
||||
month_key = f"{months[i]} {self.current_year}"
|
||||
project_expected_dict[(month_key, self.todo_stage.id)] = 1
|
||||
project_expected_dict[(month_key, self.done_stage.id)] = 5
|
||||
project_expected_is_closed_dict[(month_key, 'closed')] = 6
|
||||
|
||||
# Check that we get the expected results for the complete data of `self.project`.
|
||||
self.check_read_group_results(Domain.AND([burndown_chart_domain, project_domain]), project_expected_dict)
|
||||
self.check_read_group_is_closed_results(Domain.AND([burndown_chart_domain, project_domain]), project_expected_is_closed_dict)
|
||||
|
||||
# Check that we get the expected results for the complete data of `self.project` & `self.project_2` using an
|
||||
# `ilike` in the domain.
|
||||
all_projects_domain_with_ilike = OR([project_domain, [('project_id', 'ilike', 'mySearchTag')]])
|
||||
all_projects_domain_with_ilike = Domain.OR([project_domain, [('project_id', 'ilike', 'mySearchTag')]])
|
||||
project_expected_dict = {key: val if key[1] != self.todo_stage.id else val + 2 for key, val in project_expected_dict.items()}
|
||||
self.check_read_group_results(AND([burndown_chart_domain, all_projects_domain_with_ilike]), project_expected_dict)
|
||||
project_expected_is_closed_dict = {key: val if key[1] == 'closed' else val + 2 for key, val in project_expected_is_closed_dict.items()}
|
||||
for i in range(2, 11):
|
||||
month_key = f"{months[i]} {self.current_year - 1}"
|
||||
project_expected_dict[(month_key, self.todo_stage.id)] = 2
|
||||
project_expected_is_closed_dict[(f"{months[11]} {self.current_year - 1}", 'open')] = 2
|
||||
for i in range(current_month):
|
||||
project_expected_is_closed_dict[(f"{months[i]} {self.current_year}", 'open')] = 2
|
||||
self.check_read_group_results(Domain.AND([burndown_chart_domain, all_projects_domain_with_ilike]), project_expected_dict)
|
||||
self.check_read_group_is_closed_results(Domain.AND([burndown_chart_domain, all_projects_domain_with_ilike]), project_expected_is_closed_dict)
|
||||
|
||||
date_from, date_to = ('%s-01-01' % (self.current_year - 1), '%s-03-01' % (self.current_year - 1))
|
||||
date_from_is_closed, date_to_is_closed = ('%s-10-01' % (self.current_year - 1), '%s-12-01' % (self.current_year - 1))
|
||||
|
||||
date_from, date_to = ('%s-01-01' % (self.current_year - 1), '%s-02-01' % (self.current_year - 1))
|
||||
date_and_user_domain = [('date', '>=', date_from), ('date', '<', date_to), ('user_ids', 'ilike', 'ProjectUser')]
|
||||
complex_domain_expected_dict = {
|
||||
('January %s' % (self.current_year - 1), self.todo_stage.id): 3,
|
||||
('February %s' % (self.current_year - 1), self.todo_stage.id): 1,
|
||||
('February %s' % (self.current_year - 1), self.in_progress_stage.id): 2,
|
||||
}
|
||||
complex_domain = AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain])
|
||||
complex_domain = Domain.AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain])
|
||||
self.check_read_group_results(complex_domain, complex_domain_expected_dict)
|
||||
|
||||
date_and_user_domain = [('date', '>=', date_from_is_closed), ('date', '<', date_to_is_closed), ('user_ids', 'ilike', 'ProjectUser')]
|
||||
complex_domain = Domain.AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain])
|
||||
complex_domain_expected_dict = {
|
||||
('October %s' % (self.current_year - 1), 'closed'): 2.0,
|
||||
('October %s' % (self.current_year - 1), 'open'): 1.0,
|
||||
('November %s' % (self.current_year - 1), 'closed'): 2.0,
|
||||
('November %s' % (self.current_year - 1), 'open'): 1.0
|
||||
}
|
||||
self.check_read_group_is_closed_results(complex_domain, complex_domain_expected_dict)
|
||||
|
||||
date_and_user_domain = [('date', '>=', date_from), ('date', '<', date_to), ('user_ids', 'ilike', 'ProjectManager')]
|
||||
milestone_domain = [('milestone_id', 'ilike', 'Test')]
|
||||
complex_domain = AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain, milestone_domain])
|
||||
complex_domain = Domain.AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain, milestone_domain])
|
||||
complex_domain_expected_dict = {
|
||||
('January %s' % (self.current_year - 1), self.todo_stage.id): 1,
|
||||
('February %s' % (self.current_year - 1), self.todo_stage.id): 1,
|
||||
}
|
||||
self.check_read_group_results(complex_domain, complex_domain_expected_dict)
|
||||
|
||||
date_and_user_domain = [('date', '>=', date_from_is_closed), ('date', '<', date_to_is_closed), ('user_ids', 'ilike', 'ProjectManager')]
|
||||
milestone_domain = [('milestone_id', 'ilike', 'Test')]
|
||||
complex_domain = Domain.AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain, milestone_domain])
|
||||
complex_domain_expected_dict = {
|
||||
('October %s' % (self.current_year - 1), 'open'): 1.0,
|
||||
('November %s' % (self.current_year - 1), 'closed'): 1.0
|
||||
}
|
||||
self.check_read_group_is_closed_results(complex_domain, complex_domain_expected_dict)
|
||||
|
||||
def burndown_chart_stage_delete_stage_1(self):
|
||||
"""
|
||||
Currently, this behavior is not working as expected. The key 'Jan year-1, stage_1.id' is not present as expected, but there's an extra unwanted key
|
||||
'Jan year-1, stage_2.id' is present instead
|
||||
"""
|
||||
with freeze_time('%s-08-10' % (self.current_year - 1)):
|
||||
self.stage_1.unlink()
|
||||
self.env.cr.flush()
|
||||
expected_dict = self.get_expected_dict()
|
||||
del expected_dict[('January %s' % (self.current_year - 1), self.stage_1.id)]
|
||||
self.check_read_group_results(self.deleted_domain, expected_dict)
|
||||
|
||||
def burndown_chart_stage_delete_stage_2(self):
|
||||
"""
|
||||
Currently, this behavior is not working as expected. The key 'Feb year-1, stage_2.id' is not present as expected, but there's an extra unwanted key
|
||||
'Feb year-1, stage_3.id' is present instead
|
||||
"""
|
||||
with freeze_time('%s-08-10' % (self.current_year - 1)):
|
||||
self.stage_2.unlink()
|
||||
self.env.cr.flush()
|
||||
expected_dict = self.get_expected_dict()
|
||||
del expected_dict[('February %s' % (self.current_year - 1), self.stage_2.id)]
|
||||
self.check_read_group_results(self.deleted_domain, expected_dict)
|
||||
|
||||
def test_burndown_chart_stage_deleted_3(self):
|
||||
with freeze_time('%s-08-10' % (self.current_year - 1)):
|
||||
self.stage_3.unlink()
|
||||
self.env.cr.flush()
|
||||
expected_dict = self.get_expected_dict()
|
||||
del expected_dict[('March %s' % (self.current_year - 1), self.stage_3.id)]
|
||||
self.check_read_group_results(self.deleted_domain, expected_dict)
|
||||
|
||||
def burndown_chart_all_stage_deleted(self):
|
||||
"""
|
||||
Currently, this behavior is not working as expected. An extra task is added for every month fetched by the query.
|
||||
e.a. If the expected dict is :
|
||||
{('April 2022', 390): 1, ('May 2022', 390): 1, ('June 2022', 390): 1, ('July 2022', 390): 1, ('August 2022', 390): 1, ('September 2022', 390): 1, etc : 1}
|
||||
The fetched dict will be :
|
||||
{('January 2022', 389): 1, ('February 2022', 389): 1, ('March 2022', 389): 1, ('April 2022', 390): 2, ('May 2022', 390): 2, ('June 2022', 390): 2, ('July 2022', 390): 2,
|
||||
('August 2022', 390): 2, ('September 2022', 390): 2, etc :2 }
|
||||
"""
|
||||
with freeze_time('%s-08-10' % (self.current_year - 1)):
|
||||
(self.stage_1 | self.stage_2 | self.stage_3).unlink()
|
||||
self.env.cr.flush()
|
||||
expected_dict = self.get_expected_dict()
|
||||
del expected_dict[('January %s' % (self.current_year - 1), self.stage_1.id)]
|
||||
del expected_dict[('February %s' % (self.current_year - 1), self.stage_2.id)]
|
||||
del expected_dict[('March %s' % (self.current_year - 1), self.stage_3.id)]
|
||||
self.check_read_group_results(self.deleted_domain, expected_dict)
|
||||
|
||||
def get_expected_dict(self):
|
||||
expected_dict = {
|
||||
('January %s' % (self.current_year - 1), self.stage_1.id): 1,
|
||||
('February %s' % (self.current_year - 1), self.stage_2.id): 1,
|
||||
('March %s' % (self.current_year - 1), self.stage_3.id): 1,
|
||||
('April %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
('May %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
('June %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
('July %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
('August %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
('September %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
('October %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
('November %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
('December %s' % (self.current_year - 1), self.stage_4.id): 1,
|
||||
}
|
||||
months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October',
|
||||
'November', 'December']
|
||||
current_month = datetime.now().month
|
||||
|
||||
for i in range(current_month):
|
||||
month_key = f"{months[i]} {self.current_year}"
|
||||
expected_dict[(month_key, self.stage_4.id)] = 1
|
||||
return expected_dict
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestBurndownChartTour(HttpCase, TestBurndownChartCommon):
|
||||
|
||||
def test_burndown_chart_tour(self):
|
||||
# Test customizing personal stages as a project user
|
||||
self.start_tour('/web', 'burndown_chart_tour', login="admin")
|
||||
self.start_tour('/odoo', 'burndown_chart_tour', login="admin")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import unittest
|
||||
|
||||
from odoo.tests import TransactionCase, can_import, loaded_demo_data, tagged
|
||||
from odoo.tools.misc import file_open
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestImportFiles(TransactionCase):
|
||||
|
||||
@unittest.skipUnless(
|
||||
can_import("xlrd.xlsx") or can_import("openpyxl"), "XLRD/XLSX not available",
|
||||
)
|
||||
def test_import_task_template_xls(self):
|
||||
if not loaded_demo_data(self.env):
|
||||
self.skipTest('Needs demo data to be able to import those files')
|
||||
model = "project.task"
|
||||
filename = "tasks_import_template.xlsx"
|
||||
|
||||
file_content = file_open(f"project/static/xls/{filename}", "rb").read()
|
||||
import_wizard = self.env["base_import.import"].create(
|
||||
{
|
||||
"res_model": model,
|
||||
"file": file_content,
|
||||
"file_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
},
|
||||
)
|
||||
|
||||
result = import_wizard.parse_preview(
|
||||
{
|
||||
"has_headers": True,
|
||||
},
|
||||
)
|
||||
self.assertIsNone(result.get("error"))
|
||||
field_names = ['/'.join(v) for v in result["matches"].values()]
|
||||
results = import_wizard.execute_import(
|
||||
field_names,
|
||||
[r.lower() for r in result["headers"]],
|
||||
{
|
||||
"import_skip_records": [],
|
||||
"import_set_empty_fields": [],
|
||||
"fallback_values": {},
|
||||
"name_create_enabled_fields": {"stage_id": True},
|
||||
"encoding": "",
|
||||
"separator": "",
|
||||
"quoting": '"',
|
||||
"date_format": "",
|
||||
"datetime_format": "",
|
||||
"float_thousand_separator": ",",
|
||||
"float_decimal_separator": ".",
|
||||
"advanced": True,
|
||||
"has_headers": True,
|
||||
"keep_matches": False,
|
||||
"limit": 2000,
|
||||
"skip": 0,
|
||||
"tracking_disable": True,
|
||||
},
|
||||
)
|
||||
self.assertFalse(
|
||||
results["messages"],
|
||||
"results should be empty on successful import of ",
|
||||
)
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from freezegun import freeze_time
|
||||
from lxml import etree
|
||||
|
||||
from odoo.tests.common import TransactionCase, Form
|
||||
from odoo import Command, fields
|
||||
from odoo.tests import Form, TransactionCase
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
|
||||
class TestMultiCompanyCommon(TransactionCase):
|
||||
|
|
@ -41,7 +45,7 @@ class TestMultiCompanyCommon(TransactionCase):
|
|||
'email': 'employee@companya.com',
|
||||
'company_id': cls.company_a.id,
|
||||
'company_ids': [(6, 0, [cls.company_a.id])],
|
||||
'groups_id': [(6, 0, [user_group_employee.id])]
|
||||
'group_ids': [(6, 0, [user_group_employee.id])]
|
||||
})
|
||||
cls.user_manager_company_a = Users.create({
|
||||
'name': 'Manager Company A',
|
||||
|
|
@ -49,7 +53,7 @@ class TestMultiCompanyCommon(TransactionCase):
|
|||
'email': 'manager@companya.com',
|
||||
'company_id': cls.company_a.id,
|
||||
'company_ids': [(6, 0, [cls.company_a.id])],
|
||||
'groups_id': [(6, 0, [user_group_employee.id])]
|
||||
'group_ids': [(6, 0, [user_group_employee.id])]
|
||||
})
|
||||
cls.user_employee_company_b = Users.create({
|
||||
'name': 'Employee Company B',
|
||||
|
|
@ -57,7 +61,7 @@ class TestMultiCompanyCommon(TransactionCase):
|
|||
'email': 'employee@companyb.com',
|
||||
'company_id': cls.company_b.id,
|
||||
'company_ids': [(6, 0, [cls.company_b.id])],
|
||||
'groups_id': [(6, 0, [user_group_employee.id])]
|
||||
'group_ids': [(6, 0, [user_group_employee.id])]
|
||||
})
|
||||
cls.user_manager_company_b = Users.create({
|
||||
'name': 'Manager Company B',
|
||||
|
|
@ -65,7 +69,7 @@ class TestMultiCompanyCommon(TransactionCase):
|
|||
'email': 'manager@companyb.com',
|
||||
'company_id': cls.company_b.id,
|
||||
'company_ids': [(6, 0, [cls.company_b.id])],
|
||||
'groups_id': [(6, 0, [user_group_employee.id])]
|
||||
'group_ids': [(6, 0, [user_group_employee.id])]
|
||||
})
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -130,21 +134,21 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
|||
|
||||
# setup users
|
||||
cls.user_employee_company_a.write({
|
||||
'groups_id': [(4, user_group_project_user.id)]
|
||||
'group_ids': [(4, user_group_project_user.id)]
|
||||
})
|
||||
cls.user_manager_company_a.write({
|
||||
'groups_id': [(4, user_group_project_manager.id)]
|
||||
'group_ids': [(4, user_group_project_manager.id)]
|
||||
})
|
||||
cls.user_employee_company_b.write({
|
||||
'groups_id': [(4, user_group_project_user.id)]
|
||||
'group_ids': [(4, user_group_project_user.id)]
|
||||
})
|
||||
cls.user_manager_company_b.write({
|
||||
'groups_id': [(4, user_group_project_manager.id)]
|
||||
'group_ids': [(4, user_group_project_manager.id)]
|
||||
})
|
||||
|
||||
# create project in both companies
|
||||
Project = cls.env['project.project'].with_context({'mail_create_nolog': True, 'tracking_disable': True})
|
||||
cls.project_company_a = Project.create({
|
||||
cls.Project = cls.env['project.project'].with_context({'mail_create_nolog': True, 'tracking_disable': True})
|
||||
cls.project_company_a = cls.Project.create({
|
||||
'name': 'Project Company A',
|
||||
'alias_name': 'project+companya',
|
||||
'partner_id': cls.partner_1.id,
|
||||
|
|
@ -160,7 +164,7 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
|||
})
|
||||
]
|
||||
})
|
||||
cls.project_company_b = Project.create({
|
||||
cls.project_company_b = cls.Project.create({
|
||||
'name': 'Project Company B',
|
||||
'alias_name': 'project+companyb',
|
||||
'partner_id': cls.partner_1.id,
|
||||
|
|
@ -196,7 +200,7 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
|||
'name': 'Project Company A',
|
||||
'partner_id': self.partner_1.id,
|
||||
})
|
||||
self.assertEqual(project.company_id, self.env.user.company_id, "A newly created project should be in the current user company")
|
||||
self.assertFalse(project.company_id, "A newly created project should have a company set to False by default")
|
||||
|
||||
with self.switch_company(self.company_b):
|
||||
with self.assertRaises(AccessError, msg="Manager can not create project in a company in which he is not allowed"):
|
||||
|
|
@ -220,7 +224,112 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
|||
with self.allow_companies([self.company_a.id, self.company_b.id]):
|
||||
self.project_company_a._create_analytic_account()
|
||||
|
||||
self.assertEqual(self.project_company_a.company_id, self.project_company_a.analytic_account_id.company_id, "The analytic account created from a project should be in the same company")
|
||||
self.assertEqual(self.project_company_a.company_id, self.project_company_a.account_id.company_id, "The analytic account created from a project should be in the same company.")
|
||||
|
||||
project_no_company = self.Project.create({'name': 'Project no company'})
|
||||
#ensures that all the existing plan have a company_id
|
||||
project_no_company._create_analytic_account()
|
||||
self.assertFalse(project_no_company.account_id.company_id, "The analytic account created from a project without company_id should have its company_id field set to False.")
|
||||
|
||||
project_no_company_2 = self.Project.create({'name': 'Project no company 2'})
|
||||
project_no_company_2._create_analytic_account()
|
||||
self.assertNotEqual(project_no_company_2.account_id, project_no_company.account_id, "The analytic account created should be different from the account created for the 1st project.")
|
||||
self.assertEqual(project_no_company_2.account_id.plan_id, project_no_company.account_id.plan_id, "No new analytic should have been created.")
|
||||
|
||||
def test_analytic_account_company_consistency(self):
|
||||
"""
|
||||
This test ensures that the following invariant is kept:
|
||||
If the company of an analytic account is set, all of its project must have the same company.
|
||||
If the company of an analytic account is not set, its project can either have a company set, or none.
|
||||
"""
|
||||
project_no_company = self.Project.create({'name': 'Project no company'})
|
||||
project_no_company._create_analytic_account()
|
||||
account_no_company = project_no_company.account_id
|
||||
self.project_company_a._create_analytic_account()
|
||||
account_a = self.project_company_a.account_id
|
||||
|
||||
# Set the account of the project to a new account without company_id
|
||||
self.project_company_a.account_id = account_no_company
|
||||
self.assertEqual(self.project_company_a.account_id, account_no_company, "The new account should be set on the project.")
|
||||
self.assertFalse(account_no_company.company_id, "The company of the account should not have been updated.")
|
||||
self.project_company_a.account_id = account_a
|
||||
|
||||
# Set the account of the project to a new account with a company_id
|
||||
project_no_company.account_id = account_a
|
||||
self.assertEqual(project_no_company.company_id, self.company_a, "The company of the project should have been updated to the company of its new account.")
|
||||
self.assertEqual(project_no_company.account_id, account_a, "The account of the project should have been updated.")
|
||||
project_no_company.account_id = account_no_company
|
||||
project_no_company.company_id = False
|
||||
|
||||
# Neither the project nor its account have a company_id
|
||||
# set the company of the project
|
||||
project_no_company.company_id = self.company_a
|
||||
self.assertEqual(project_no_company.company_id, self.company_a, "The company of the project should have been updated.")
|
||||
self.assertFalse(account_no_company.company_id, "The company of the account should not have been updated for the company of the project was False before its update.")
|
||||
project_no_company.company_id = False
|
||||
# set the company of the account
|
||||
account_no_company.company_id = self.company_a
|
||||
self.assertEqual(project_no_company.company_id, self.company_a, "The company of the project should have been updated to the company of its new account.")
|
||||
self.assertEqual(account_no_company.company_id, self.company_a, "The company of the account should have been updated.")
|
||||
|
||||
# The project and its account have the same company (company A)
|
||||
# set the company of the project to False
|
||||
self.project_company_a.company_id = False
|
||||
self.assertFalse(self.project_company_a.company_id, "The company of the project should have been updated.")
|
||||
self.assertFalse(account_a.company_id, "The company of the account should be set to False, as it only has one project linked to it and the company of the project was set from company A to False")
|
||||
account_a.company_id = self.company_a
|
||||
# set the company of the project to company B
|
||||
self.project_company_a.company_id = self.company_b
|
||||
self.assertEqual(self.project_company_a.company_id, self.company_b, "The company of the project should have been updated.")
|
||||
self.assertEqual(account_a.company_id, self.company_b, "The company of the account should have been updated, for its company was the same as the one of its project and the company of the project was set before the update.")
|
||||
# set the company of the account to company A
|
||||
account_a.company_id = self.company_a
|
||||
self.assertEqual(self.project_company_a.company_id, self.company_a, "The company of the project should have been updated to the company of its account.")
|
||||
self.assertEqual(account_a.company_id, self.company_a, "The company of the account should have been updated.")
|
||||
# set the company of the account to False
|
||||
account_a.company_id = False
|
||||
self.assertEqual(self.project_company_a.company_id, self.company_a, "The company of the project should not have been updated for the company of its account has been set to False.")
|
||||
self.assertFalse(account_a.company_id, "The company of the account should have been updated.")
|
||||
|
||||
# The project has a company_id set, but not its account
|
||||
# set the company of the account to company B (!= project.company_id)
|
||||
account_a.company_id = self.company_b
|
||||
self.assertEqual(self.project_company_a.company_id, self.company_b, "The company of the project should have been updated to the company of its account even if the new company set on the account is a different one than the one the project.")
|
||||
self.assertEqual(account_a.company_id, self.company_b, "The company of the account should have been updated.")
|
||||
account_a.company_id = False
|
||||
self.project_company_a.company_id = self.company_a
|
||||
# set the company of the account to company A (== project.company_id)
|
||||
account_a.company_id = self.company_a
|
||||
self.assertEqual(self.project_company_a.company_id, self.company_a, "The company of the project should have been updated to the company of its account.")
|
||||
self.assertEqual(account_a.company_id, self.company_a, "The company of the account should have been updated.")
|
||||
account_a.company_id = False
|
||||
# set the company of the project to company B
|
||||
self.project_company_a.company_id = self.company_b
|
||||
self.assertEqual(self.project_company_a.company_id, self.company_b, "The company of the project should have been updated.")
|
||||
self.assertFalse(account_a.company_id, "The company of the account should not have been updated for it is was set to False.")
|
||||
# set the company of the project to False
|
||||
self.project_company_a.company_id = False
|
||||
self.assertFalse(self.project_company_a.company_id, "The company of the project should have been updated.")
|
||||
self.assertFalse(account_a.company_id, "The company of the account should not have been updated for it was set to False.")
|
||||
|
||||
# creates an AAL for the account_a
|
||||
account_a.company_id = self.company_b
|
||||
aal = self.env['account.analytic.line'].create({
|
||||
'name': 'other revenues line',
|
||||
'account_id': account_a.id,
|
||||
'company_id': self.company_b.id,
|
||||
'amount': 100,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.project_company_a.company_id = self.company_a
|
||||
self.assertEqual(self.project_company_a.company_id, self.company_b, "The account of the project contains AAL, its company can not be updated.")
|
||||
aal.unlink()
|
||||
|
||||
project_no_company.account_id = account_a
|
||||
self.assertEqual(project_no_company.company_id, account_a.company_id)
|
||||
with self.assertRaises(UserError):
|
||||
self.project_company_a.company_id = self.company_a
|
||||
self.assertEqual(self.project_company_a.company_id, self.company_b, "The account of the project is linked to more than one project, its company can not be updated.")
|
||||
|
||||
def test_create_task(self):
|
||||
with self.sudo('employee-a'):
|
||||
|
|
@ -232,6 +341,64 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
|||
|
||||
self.assertEqual(task.company_id, self.project_company_a.company_id, "The company of the task should be the one from its project.")
|
||||
|
||||
def test_update_company_id(self):
|
||||
""" this test ensures that:
|
||||
- All the tasks of a project with a company set have the same company as their project. Updating the task set the task to a private state.
|
||||
Updating the company the project update all the tasks even if the company of the project is set to False.
|
||||
- The tasks of a project without company can have any company set. Updating a task does not update the company of its project. Updating the project update
|
||||
all the tasks even if these tasks had a company set.
|
||||
"""
|
||||
project = self.Project.create({'name': 'Project'})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'task no company',
|
||||
'project_id': project.id,
|
||||
})
|
||||
self.assertFalse(task.company_id, "Creating a task in a project without company set its company_id to False.")
|
||||
|
||||
with self.debug_mode():
|
||||
task_form = Form(task)
|
||||
task_form.company_id = self.company_a
|
||||
task = task_form.save()
|
||||
self.assertFalse(project.company_id, "Setting a new company on a task should not update the company of its project.")
|
||||
self.assertEqual(task.company_id, self.company_a, "The company of the task should have been updated.")
|
||||
|
||||
project.company_id = self.company_b
|
||||
self.assertEqual(project.company_id, self.company_b, "The company of the project should have been updated.")
|
||||
self.assertEqual(task.company_id, self.company_b, "The company of the task should have been updated.")
|
||||
|
||||
|
||||
with self.debug_mode():
|
||||
task_form = Form(task)
|
||||
task_form.company_id = self.company_a
|
||||
task = task_form.save()
|
||||
self.assertEqual(task.company_id, self.company_a, "The company of the task should have been updated.")
|
||||
self.assertFalse(task.project_id, "The task should now be a private task.")
|
||||
self.assertEqual(project.company_id, self.company_b, "the company of the project should not have been updated.")
|
||||
|
||||
task_1, task_2, task_3 = self.env['project.task'].create([{
|
||||
'name': 'task 1',
|
||||
'project_id': project.id,
|
||||
}, {
|
||||
'name': 'task 2',
|
||||
'project_id': project.id,
|
||||
}, {
|
||||
'name': 'task 3',
|
||||
'project_id': project.id,
|
||||
}])
|
||||
project.company_id = False
|
||||
for task in project.task_ids:
|
||||
self.assertFalse(task.company_id, "The tasks should not have a company_id set.")
|
||||
task_1.company_id = self.company_a
|
||||
task_2.company_id = self.company_b
|
||||
self.assertEqual(task_1.company_id, self.company_a, "The company of task_1 should have been set to company A.")
|
||||
self.assertEqual(task_2.company_id, self.company_b, "The company of task_2 should have been set to company B.")
|
||||
self.assertFalse(task_3.company_id, "The company of the task_3 should not have been updated.")
|
||||
self.assertFalse(project.company_id, "The company of the project should not have been updated.")
|
||||
company_c = self.env['res.company'].create({'name': 'company C'})
|
||||
project.company_id = company_c
|
||||
for task in project.tasks:
|
||||
self.assertEqual(task.company_id, company_c, "The company of the tasks should have been updated to company C.")
|
||||
|
||||
def test_move_task(self):
|
||||
with self.sudo('employee-a'):
|
||||
with self.allow_companies([self.company_a.id, self.company_b.id]):
|
||||
|
|
@ -248,53 +415,58 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
|||
self.assertEqual(task.company_id, self.company_a, "Moving a task should change its company.")
|
||||
|
||||
def test_create_subtask(self):
|
||||
with self.sudo('employee-a'):
|
||||
with self.allow_companies([self.company_a.id, self.company_b.id]):
|
||||
# create subtask, set parent; the onchange will set the correct company and subtask project
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_parent_id': self.task_1.id, 'default_project_id': self.project_company_b.id})) as task_form:
|
||||
task_form.name = 'Test Subtask in company B'
|
||||
task = task_form.save()
|
||||
self.assertEqual(task.company_id, self.project_company_b.company_id, "The company of the subtask should be the one from its project, and not from its parent.")
|
||||
# 1) Create a subtask and check that every field is correctly set on the subtask
|
||||
with (
|
||||
Form(self.task_1) as task_1_form,
|
||||
task_1_form.child_ids.new() as subtask_line,
|
||||
):
|
||||
self.assertEqual(subtask_line.project_id, self.task_1.project_id, "The task's project should already be set on the subtask.")
|
||||
subtask_line.name = 'Test Subtask'
|
||||
subtask = self.task_1.child_ids[0]
|
||||
self.assertFalse(subtask.display_in_project, "The subtask's field 'display in project' should be unchecked.")
|
||||
self.assertEqual(subtask.company_id, self.task_1.company_id, "The company of the subtask should be the one from its project.")
|
||||
|
||||
# set parent on existing orphan task; the onchange will set the correct company and subtask project
|
||||
self.task_2.write({'project_id': False})
|
||||
# For `parent_id` to be visible in the view, you need
|
||||
# 1. The debug mode
|
||||
# 2. `allow_subtasks` to be true
|
||||
# <field name="parent_id" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
# `allow_subtasks` is a related to `allow_subtasks` on the project
|
||||
# as the point of the test is to test the behavior of the task `_compute_project_id` when there is no project,
|
||||
# `allow_subtasks` is by default invisible, and you shouldn't therefore be able to change it.
|
||||
# So, to make it visible, temporary modify the view to make it visible even when `allow_subtasks` is `False`.
|
||||
view = self.env.ref('project.view_task_form2').sudo()
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="parent_id"][@attrs]'):
|
||||
node.attrib.pop('attrs')
|
||||
view.arch = etree.tostring(tree)
|
||||
with self.debug_mode():
|
||||
with Form(self.task_2) as task_form:
|
||||
task_form.name = 'Test Task 2 becomes child of Task 1 (other company)'
|
||||
task_form.parent_id = self.task_1
|
||||
task = task_form.save()
|
||||
# 2) Change the project of the parent task and check that the subtask follows it
|
||||
with Form(self.task_1) as task_1_form:
|
||||
task_1_form.project_id = self.project_company_b
|
||||
self.assertEqual(subtask.project_id, self.task_1.project_id, "The task's project should already be set on the subtask.")
|
||||
self.assertFalse(subtask.display_in_project, "The subtask's field 'display in project' should be unchecked.")
|
||||
self.assertEqual(subtask.company_id, self.project_company_b.company_id, "The company of the subtask should be the one from its project.")
|
||||
task_1_form.project_id = self.project_company_a
|
||||
|
||||
# 3) Change the parent of the subtask and check that every field is correctly set on it
|
||||
# For `parent_id` to be visible in the view, you need
|
||||
# 1. The debug mode
|
||||
# <field name="parent_id" groups="base.group_no_one"/>
|
||||
view = self.env.ref('project.view_task_form2').sudo()
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="parent_id"][@invisible]'):
|
||||
node.attrib.pop('invisible')
|
||||
view.arch = etree.tostring(tree)
|
||||
with (
|
||||
self.debug_mode(),
|
||||
Form(subtask) as subtask_form
|
||||
):
|
||||
subtask_form.parent_id = self.task_2
|
||||
self.assertEqual(subtask_form.project_id, self.task_2.project_id, "The task's project should already be set on the subtask.")
|
||||
self.assertFalse(subtask.display_in_project, "The subtask's field 'display in project' should be unchecked.")
|
||||
self.assertEqual(subtask.company_id, self.task_2.company_id, "The company of the subtask should be the one from its new project, set from its parent.")
|
||||
|
||||
# 4) Change the project of the subtask and check some fields
|
||||
subtask.project_id = self.project_company_a
|
||||
self.assertTrue(subtask.display_in_project, "The subtask's field 'display in project' should be checked.")
|
||||
self.assertEqual(subtask.company_id, self.project_company_a.company_id, "The company of the subtask should be the one from its project, and not from its parent.")
|
||||
|
||||
self.assertEqual(task.company_id, task.project_id.company_id, "The company of the orphan subtask should be the one from its project.")
|
||||
|
||||
def test_cross_subtask_project(self):
|
||||
# set up default subtask project
|
||||
self.project_company_a.write({'allow_subtasks': True})
|
||||
|
||||
# For `parent_id` to be visible in the view, you need
|
||||
# 1. The debug mode
|
||||
# 2. `allow_subtasks` to be true
|
||||
# <field name="parent_id" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
# `allow_subtasks` is a related to `allow_subtasks` on the project
|
||||
# as the point of the test is to test the behavior of the task `_compute_project_id` when there is no project,
|
||||
# `allow_subtasks` is by default invisible, and you shouldn't therefore be able to change it.
|
||||
# So, to make it visible, temporary modify the view to make it visible even when `allow_subtasks` is `False`.
|
||||
# <field name="parent_id" groups="base.group_no_one"/>
|
||||
view = self.env.ref('project.view_task_form2').sudo()
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="parent_id"][@attrs]'):
|
||||
node.attrib.pop('attrs')
|
||||
for node in tree.xpath('//field[@name="parent_id"][@invisible]'):
|
||||
node.attrib.pop('invisible')
|
||||
view.arch = etree.tostring(tree)
|
||||
|
||||
with self.sudo('employee-a'):
|
||||
|
|
@ -302,15 +474,43 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
|||
with self.debug_mode():
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.name = 'Test Subtask in company B'
|
||||
task_form.project_id = self.task_1.project_id
|
||||
task_form.parent_id = self.task_1
|
||||
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.task_1.project_id, "The default project of a subtask should be the default subtask project of the project from the mother task")
|
||||
self.assertEqual(task.company_id, task.project_id.company_id, "The company of the orphan subtask should be the one from its project.")
|
||||
self.assertEqual(self.task_1.child_ids.ids, [task.id])
|
||||
|
||||
with self.sudo('employee-a'):
|
||||
with self.assertRaises(AccessError):
|
||||
with Form(task) as task_form:
|
||||
task_form.name = "Testing changing name in a company I can not read/write"
|
||||
|
||||
@freeze_time("2019-5-28 08:00:00")
|
||||
def test_date_to_assign_project(self):
|
||||
company_0, company_1 = self.env['res.company'].create([{
|
||||
"name": "Test company 0",
|
||||
},
|
||||
{
|
||||
"name": "Test company 1",
|
||||
}])
|
||||
|
||||
self.env['resource.calendar.leaves'].create([{
|
||||
'name': "Public Holiday for company 0",
|
||||
'company_id': company_0.id,
|
||||
'date_from': datetime(2019, 5, 27, 0, 0, 0),
|
||||
'date_to': datetime(2019, 5, 29, 23, 0, 0),
|
||||
'resource_id': False,
|
||||
'time_type': "leave",
|
||||
}])
|
||||
project = self.env['project.project'].with_company(company_1).create({'name': 'Project for company 1'})
|
||||
task = self.env['project.task'].with_company(company_1).create({
|
||||
'name': 'Task for company 1',
|
||||
'project_id': project.id,
|
||||
'create_date': datetime(2019, 5, 28, 10, 0, 0),
|
||||
'user_ids': False
|
||||
})
|
||||
with freeze_time("2019-05-28 14:00:00"):
|
||||
task.user_ids = [Command.set([self.user_employee_company_a.id])]
|
||||
task.date_assign = fields.Datetime.now()
|
||||
self.assertEqual(task.working_hours_open, 3.0)
|
||||
self.assertEqual(task.working_days_open, 0.375)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import tagged, HttpCase
|
||||
from odoo import Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import HttpCase, tagged, new_test_user
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'personal_stages')
|
||||
class TestPersonalStages(TestProjectCommon):
|
||||
|
||||
|
|
@ -56,43 +59,336 @@ class TestPersonalStages(TestProjectCommon):
|
|||
'The search should only have returned task that are in the inbox personal stage.')
|
||||
|
||||
def test_personal_stage_read_group(self):
|
||||
# Ensure user doesnt have any tasks before hand
|
||||
(self.env['project.task'].sudo().search([("user_ids", "in", (self.task_1.user_ids + self.user_projectmanager).ids)]) - self.task_1).unlink()
|
||||
|
||||
self.task_1.user_ids += self.user_projectmanager
|
||||
self.task_1.with_user(self.user_projectmanager).personal_stage_type_id = self.manager_stages[1]
|
||||
#Makes sure the personal stage for project manager is saved in the database
|
||||
self.env.flush_all()
|
||||
read_group_user = self.env['project.task'].with_user(self.user_projectuser).read_group(
|
||||
[('user_ids', '=', self.user_projectuser.id)], fields=['sequence:avg'], groupby=['personal_stage_type_ids'])
|
||||
read_group_user = self.env['project.task'].with_context(read_group_expand=True).with_user(self.user_projectuser).formatted_read_group(
|
||||
[('user_ids', '=', self.user_projectuser.id)], aggregates=['sequence:avg', '__count'], groupby=['personal_stage_type_ids'])
|
||||
# Check that the result is at least a bit coherent
|
||||
self.assertEqual(len(self.user_stages), len(read_group_user),
|
||||
'read_group should return %d groups' % len(self.user_stages))
|
||||
# User has only one task assigned the sum of all counts should be 1
|
||||
total = 0
|
||||
for group in read_group_user:
|
||||
total += group['personal_stage_type_ids_count']
|
||||
total += group['__count']
|
||||
self.assertEqual(1, total,
|
||||
'read_group should not have returned more tasks than the user is assigned to.')
|
||||
read_group_manager = self.env['project.task'].with_user(self.user_projectmanager).read_group(
|
||||
[('user_ids', '=', self.user_projectmanager.id)], fields=['sequence:avg'], groupby=['personal_stage_type_ids'])
|
||||
read_group_manager = self.env['project.task'].with_context(read_group_expand=True).with_user(self.user_projectmanager).formatted_read_group(
|
||||
[('user_ids', '=', self.user_projectmanager.id)], aggregates=['sequence:avg', '__count'], groupby=['personal_stage_type_ids'])
|
||||
self.assertEqual(len(self.manager_stages), len(read_group_manager),
|
||||
'read_group should return %d groups' % len(self.user_stages))
|
||||
total = 0
|
||||
total_stage_0 = 0
|
||||
total_stage_1 = 0
|
||||
for group in read_group_manager:
|
||||
total += group['personal_stage_type_ids_count']
|
||||
total += group['__count']
|
||||
# Check that we have a task in both stages
|
||||
if group['personal_stage_type_ids'][0] == self.manager_stages[0].id:
|
||||
total_stage_0 += 1
|
||||
elif group['personal_stage_type_ids'][0] == self.manager_stages[1].id:
|
||||
total_stage_1 += 1
|
||||
self.assertEqual(2, total,
|
||||
self.assertEqual(1, total,
|
||||
'read_group should not have returned more tasks than the user is assigned to.')
|
||||
self.assertEqual(1, total_stage_0)
|
||||
self.assertEqual(1, total_stage_1)
|
||||
|
||||
def test_delete_personal_stage(self):
|
||||
"""
|
||||
When deleting personal stages, the task of this stage are transfered to the one following it sequence-wise.
|
||||
The deletion of stages can be done in batch.
|
||||
"""
|
||||
user_1, user_2, user_3 = self.env['res.users'].create([{
|
||||
'login': 'user_1_stages',
|
||||
'name': 'User 1 with personal stages',
|
||||
}, {
|
||||
'login': 'user_2_stages',
|
||||
'name': 'User 2 with personal stages',
|
||||
}, {
|
||||
'login': 'user_3_stages',
|
||||
'name': 'User 3 with personal stages',
|
||||
}])
|
||||
|
||||
# Ensure user doesnt have any personal stages before hand and no onboarding tasks
|
||||
self.env['project.task.type'].sudo().search([("user_id", "in", (user_1 + user_2 + user_3).ids)]).user_id = False
|
||||
self.env['project.task'].sudo().search([("user_ids", "in", (user_1 + user_2 + user_3).ids)]).unlink()
|
||||
|
||||
self.assertEqual(self.env['project.task.type'].search_count([('user_id', '=', user_1.id)]), 0)
|
||||
self.assertEqual(self.env['project.task.type'].search_count([('user_id', '=', user_2.id)]), 0)
|
||||
self.assertEqual(self.env['project.task'].search_count([('user_ids', 'in', user_1.ids)]), 0)
|
||||
self.assertEqual(self.env['project.task'].search_count([('user_ids', 'in', user_2.ids)]), 0)
|
||||
|
||||
# Create 5 personal stages for user 1
|
||||
user_1_stages = self.env['project.task.type'].create([{
|
||||
'user_id': user_1.id,
|
||||
'name': f'User 1 - Stage {i}',
|
||||
'sequence': 10 * i,
|
||||
} for i in range(1, 6)])
|
||||
# Create 3 personal stages for user 2
|
||||
user_2_stages = self.env['project.task.type'].create([{
|
||||
'user_id': user_2.id,
|
||||
'name': f'User 2 - Stage {i}',
|
||||
'sequence': 10 * i,
|
||||
} for i in range(1, 4)])
|
||||
|
||||
# Create private tasks for user 1 and 2
|
||||
private_tasks = self.env['project.task'].create([{
|
||||
'user_ids': [Command.link(user_1.id), Command.link(user_2.id)],
|
||||
'name': 'Task 1',
|
||||
'project_id': False,
|
||||
}, {
|
||||
'user_ids': [Command.link(user_1.id), Command.link(user_2.id)],
|
||||
'name': 'Task 2',
|
||||
'project_id': False,
|
||||
}, {
|
||||
'user_ids': [Command.link(user_1.id)],
|
||||
'name': 'Task 3',
|
||||
'project_id': False,
|
||||
}, {
|
||||
'user_ids': [Command.link(user_1.id)],
|
||||
'name': 'Task 4',
|
||||
'project_id': False,
|
||||
}])
|
||||
|
||||
# Put private tasks in personal stages for user 1
|
||||
private_tasks[0].with_user(user_1.id).personal_stage_type_id = user_1_stages[2].id
|
||||
private_tasks[1].with_user(user_1.id).personal_stage_type_id = user_1_stages[3].id
|
||||
private_tasks[2].with_user(user_1.id).personal_stage_type_id = user_1_stages[4].id
|
||||
private_tasks[3].with_user(user_1.id).personal_stage_type_id = user_1_stages[4].id
|
||||
|
||||
# Put private tasks in personal stages for user 2
|
||||
private_tasks[0].with_user(user_2.id).personal_stage_type_id = user_2_stages[0].id
|
||||
private_tasks[1].with_user(user_2.id).personal_stage_type_id = user_2_stages[1].id
|
||||
|
||||
# ------------------------------------
|
||||
# ------- A. Initial situation ------
|
||||
# ------------------------------------
|
||||
#
|
||||
# For user 1:
|
||||
#
|
||||
# +---------+---------+---------+---------+---------+
|
||||
# | Stage 1 | Stage 2 | Stage 3 | Stage 4 | Stage 5 |
|
||||
# +---------+---------+---------+---------+---------+
|
||||
# | | | Task 1 | Task 2 | Task 3 |
|
||||
# | | | | | Task 4 |
|
||||
# +---------+---------+---------+---------+---------+
|
||||
#
|
||||
# For user 2:
|
||||
#
|
||||
# +---------+---------+---------+
|
||||
# | Stage 1 | Stage 2 | Stage 3 |
|
||||
# +---------+---------+---------+
|
||||
# | Task 1 | Task 2 | |
|
||||
# +---------+---------+---------+
|
||||
|
||||
self.assertEqual(self.env['project.task.type'].with_user(user_1.id).search_count([('project_ids', '=', False), ('user_id', '=', user_1.id)]), 5)
|
||||
self.assertEqual(self.env['project.task'].with_user(user_1.id).search_count([('user_ids', 'in', user_1.ids)]), 4)
|
||||
private_tasks.invalidate_recordset(['personal_stage_type_id'])
|
||||
self.assertEqual(private_tasks[0].with_user(user_1.id).personal_stage_type_id.id, user_1_stages[2].id)
|
||||
self.assertEqual(private_tasks[1].with_user(user_1.id).personal_stage_type_id.id, user_1_stages[3].id)
|
||||
self.assertEqual(private_tasks[2].with_user(user_1.id).personal_stage_type_id.id, user_1_stages[4].id)
|
||||
self.assertEqual(private_tasks[3].with_user(user_1.id).personal_stage_type_id.id, user_1_stages[4].id)
|
||||
self.assertEqual(self.env['project.task.type'].with_user(user_2.id).search_count([('project_ids', '=', False), ('user_id', '=', user_2.id)]), 3)
|
||||
self.assertEqual(self.env['project.task'].with_user(user_2.id).search_count([('user_ids', 'in', user_2.ids)]), 2)
|
||||
private_tasks.invalidate_recordset(['personal_stage_type_id'])
|
||||
self.assertEqual(private_tasks[0].with_user(user_2.id).personal_stage_type_id.id, user_2_stages[0].id)
|
||||
self.assertEqual(private_tasks[1].with_user(user_2.id).personal_stage_type_id.id, user_2_stages[1].id)
|
||||
|
||||
# --------------------------------------------
|
||||
# ---- B. Deleting an empty (own) stage -----
|
||||
# --------------------------------------------
|
||||
#
|
||||
# Deleting stage 3 for user 2
|
||||
# Expected result for user 2:
|
||||
#
|
||||
# +---------+---------+
|
||||
# | Stage 1 | Stage 2 |
|
||||
# +---------+---------+
|
||||
# | Task 1 | Task 2 |
|
||||
# +---------+---------+
|
||||
|
||||
user_2_stages[2].with_user(user_2.id).unlink()
|
||||
self.assertEqual(self.env['project.task.type'].with_user(user_2.id).search_count([('project_ids', '=', False), ('user_id', '=', user_2.id)]), 2,
|
||||
"A user should be able to unlink its own (empty) personal stage.")
|
||||
|
||||
# --------------------------------------------
|
||||
# ---- C. Deleting a single (own) stage -----
|
||||
# --------------------------------------------
|
||||
#
|
||||
# Deleting stage 3 for user 1, the task in this stage should move to stage 2
|
||||
# Expected result for user 1:
|
||||
#
|
||||
# +---------+---------+---------+---------+
|
||||
# | Stage 1 | Stage 2 | Stage 4 | Stage 5 |
|
||||
# +---------+---------+---------+---------+
|
||||
# | | Task 1 | Task 2 | Task 3 |
|
||||
# | | | | Task 4 |
|
||||
# +---------+---------+---------+---------+
|
||||
|
||||
private_tasks.invalidate_recordset(['personal_stage_type_id'])
|
||||
user_1_stages[2].with_user(user_1.id).unlink()
|
||||
self.assertEqual(self.env['project.task.type'].with_user(user_1.id).search_count([('project_ids', '=', False), ('user_id', '=', user_1.id)]), 4,
|
||||
"A user should be able to unlink its own personal stage.")
|
||||
self.assertEqual(self.env['project.task'].with_user(user_1.id).search_count([('user_ids', 'in', user_1.ids)]), 4,
|
||||
"Tasks in a removed personal stage should not be unlinked.")
|
||||
self.assertEqual(private_tasks[0].with_user(user_1.id).personal_stage_type_id.id, user_1_stages[1].id,
|
||||
"Tasks in a removed personal stage should be moved to the stage following it sequence-wise")
|
||||
|
||||
# --------------------------------------------
|
||||
# ---- D. Deleting (own) stage in batch ------
|
||||
# --------------------------------------------
|
||||
#
|
||||
# Deleting stages 2 & 4 for user 1, the task in those stages should move to stage 1
|
||||
# Expected result for user 1:
|
||||
#
|
||||
# +---------+---------+
|
||||
# | Stage 1 | Stage 5 |
|
||||
# +---------+---------+
|
||||
# | Task 1 | Task 3 |
|
||||
# | Task 2 | Task 4 |
|
||||
# +---------+---------+
|
||||
|
||||
user_1_stages.filtered(lambda s: s.id in [user_1_stages[1].id, user_1_stages[3].id]).with_user(user_1.id).unlink()
|
||||
self.assertEqual(self.env['project.task.type'].with_user(user_1.id).search_count([('project_ids', '=', False), ('user_id', '=', user_1.id)]), 2,
|
||||
"A user should be able to unlink its own personal stage in batch.")
|
||||
self.assertEqual(self.env['project.task'].with_user(user_1.id).search_count([('user_ids', 'in', user_1.ids)]), 4,
|
||||
"Tasks in personal stages removed in batch should not be unlinked.")
|
||||
for i in range(2):
|
||||
self.assertEqual(private_tasks[i].with_user(user_1.id).personal_stage_type_id.id, user_1_stages[0].id,
|
||||
"Tasks in a personal stage removed in batch should be moved to the stage following it sequence-wise")
|
||||
|
||||
# ------------------------------------------------------
|
||||
# -- E. Deleting multi-user stages in batch (as sudo) --
|
||||
# ------------------------------------------------------
|
||||
#
|
||||
# Deleting stages 1 user 1 and stage 2 for user 2
|
||||
# Expected result for user 1:
|
||||
#
|
||||
# +---------+
|
||||
# | Stage 5 |
|
||||
# +---------+
|
||||
# | Task 1 |
|
||||
# | Task 2 |
|
||||
# | Task 3 |
|
||||
# | Task 4 |
|
||||
# +---------+
|
||||
#
|
||||
# Expected result for user 2:
|
||||
#
|
||||
# +---------+
|
||||
# | Stage 1 |
|
||||
# +---------+
|
||||
# | Task 1 |
|
||||
# | Task 2 |
|
||||
# +---------+
|
||||
#
|
||||
|
||||
(user_1_stages[0] | user_2_stages[1]).sudo().unlink()
|
||||
self.assertEqual(self.env['project.task.type'].with_user(user_1.id).search_count([('project_ids', '=', False), ('user_id', '=', user_1.id)]), 1,
|
||||
"Superuser should be able to delete personal stages in batch.")
|
||||
self.assertEqual(self.env['project.task.type'].with_user(user_2.id).search_count([('project_ids', '=', False), ('user_id', '=', user_2.id)]), 1,
|
||||
"Superuser should be able to delete personal stages in batch.")
|
||||
self.assertEqual(self.env['project.task'].with_user(user_1.id).search_count([('user_ids', 'in', user_1.ids)]), 4,
|
||||
"Tasks in personal stages removed in batch by superuser should not be unlinked.")
|
||||
for private_task in private_tasks:
|
||||
self.assertEqual(private_task.with_user(user_1.id).personal_stage_type_id.id, user_1_stages[4].id,
|
||||
"Tasks in a personal stage removed in batch should be moved to a stage with a higher sequence if no stage with lower sequence have been found")
|
||||
private_tasks.invalidate_recordset(['personal_stage_type_id'])
|
||||
self.assertEqual(private_tasks[0].with_user(user_2.id).personal_stage_type_id.id, user_2_stages[0].id,
|
||||
"Tasks in a personal stage removed in batch by superuser should be moved to the stage following it sequence-wise")
|
||||
self.assertEqual(private_tasks[1].with_user(user_2.id).personal_stage_type_id.id, user_2_stages[0].id,
|
||||
"Tasks in a personal stage removed in batch by superuser should be moved to the stage following it sequence-wise")
|
||||
|
||||
# ------------------------------------------------------
|
||||
# -- F. Deleting the last personal stage not allowed --
|
||||
# ------------------------------------------------------
|
||||
#
|
||||
# Deleting stage 1 for user 2 should raise an error
|
||||
# Expected result for user 2:
|
||||
#
|
||||
# +---------+
|
||||
# | Stage 1 |
|
||||
# +---------+
|
||||
# | Task 1 |
|
||||
# | Task 2 |
|
||||
# +---------+
|
||||
#
|
||||
|
||||
with self.assertRaises(UserError, msg="Deleting the last personal stage of a user should raise an error"):
|
||||
user_2_stages[0].with_user(user_2.id).unlink()
|
||||
self.assertEqual(self.env['project.task.type'].with_user(user_2.id).search_count([('project_ids', '=', False), ('user_id', '=', user_2.id)]), 1,
|
||||
"Last personal stage of a user should not be deleted by unlink method")
|
||||
private_tasks.invalidate_recordset(['personal_stage_type_id'])
|
||||
self.assertEqual(private_tasks[0].with_user(user_2.id).personal_stage_type_id.id, user_2_stages[0].id,
|
||||
"Last personal stage of a user should not be deleted by unlink method")
|
||||
self.assertEqual(private_tasks[1].with_user(user_2.id).personal_stage_type_id.id, user_2_stages[0].id,
|
||||
"Last personal stage of a user should not be deleted by unlink method")
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# - G. Deleting the last personal stage not allowed (even if empty) -
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
empty_stage_user_3 = self.env['project.task.type'].create({
|
||||
'user_id': user_3.id,
|
||||
'name': 'User 3 - Empty stage',
|
||||
'sequence': 10,
|
||||
})
|
||||
|
||||
with self.assertRaises(UserError, msg="Deleting the last personal stage of a user should raise an error, even if the stage is empty"):
|
||||
empty_stage_user_3.with_user(user_3.id).unlink()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# - H. Mixed scenario: 1 normal stage and 2 personal ones -
|
||||
# ---------------------------------------------------------
|
||||
|
||||
# Create one normal project stage with no task in it two other personal stages for both users that could be deleted
|
||||
empty_stages = self.env['project.task.type'].create([{
|
||||
'user_id': user_1.id,
|
||||
'name': 'User 1 - Empty stage',
|
||||
'sequence': 10,
|
||||
}, {
|
||||
'user_id': user_2.id,
|
||||
'name': 'User 2 - Empty stage',
|
||||
'sequence': 10,
|
||||
}, {
|
||||
'project_ids': self.project_pigs,
|
||||
'name': 'Empty stage in project Pigs',
|
||||
'sequence': 10,
|
||||
}])
|
||||
empty_stages.sudo().unlink()
|
||||
self.assertFalse(self.env['project.task.type'].search_count([('id', 'in', empty_stages.ids)]),
|
||||
"All stages, wether they are personal or not, should be able to be deleted in batch")
|
||||
|
||||
def test_new_personal_stages_created_for_new_users(self):
|
||||
ProjectTaskType = self.env["project.task.type"]
|
||||
|
||||
internal_user = new_test_user(
|
||||
self.env,
|
||||
login="internal_user",
|
||||
groups="base.group_user",
|
||||
)
|
||||
self.assertEqual(7, ProjectTaskType.search_count([("user_id", "=", internal_user.id)]), "Personal stages seems to have a wrong count")
|
||||
|
||||
portal_user = new_test_user(
|
||||
self.env,
|
||||
login="portal_user",
|
||||
groups="base.group_portal",
|
||||
)
|
||||
self.assertEqual(0, ProjectTaskType.search_count([("user_id", "=", portal_user.id)]), "Portal users should never have personal stages when created")
|
||||
|
||||
public_user = new_test_user(
|
||||
self.env,
|
||||
login="public_user",
|
||||
groups="base.group_public",
|
||||
)
|
||||
self.assertEqual(0, ProjectTaskType.search_count([("user_id", "=", public_user.id)]), "Public users should never have personal stages when created")
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestPersonalStageTour(HttpCase, TestProjectCommon):
|
||||
|
||||
def test_personal_stage_tour(self):
|
||||
# Test customizing personal stages as a project user
|
||||
self.start_tour('/web', 'personal_stage_tour', login="armandel")
|
||||
self.start_tour('/odoo', 'personal_stage_tour', login="armandel")
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
from odoo import Command
|
||||
from odoo.addons.project.tests.test_access_rights import TestProjectPortalCommon
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import HttpCase
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestPortalProject(TestProjectPortalCommon):
|
||||
class TestPortalProject(TestProjectPortalCommon, HttpCase):
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_portal_project_access_rights(self):
|
||||
pigs = self.project_pigs
|
||||
|
|
@ -45,11 +46,12 @@ class TestPortalProject(TestProjectPortalCommon):
|
|||
self.project_pigs.privacy_visibility = 'portal'
|
||||
self.assertFalse(self.project_pigs.access_token, 'The access token should not yet available since the project has not been shared yet.')
|
||||
wizard = self.env['project.share.wizard'].create({
|
||||
'access_mode': 'read',
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_pigs.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.partner_1.id),
|
||||
'collaborator_ids': [
|
||||
Command.create({
|
||||
'partner_id': self.partner_1.id,
|
||||
}),
|
||||
]
|
||||
})
|
||||
wizard.action_send_mail()
|
||||
|
|
@ -81,3 +83,74 @@ class TestPortalProject(TestProjectPortalCommon):
|
|||
self.project_pigs.privacy_visibility = 'employees'
|
||||
self.assertFalse(self.project_pigs.access_token, 'The access token should no longer be set since now the project is only available by internal users.')
|
||||
self.assertFalse(all(self.project_pigs.tasks.mapped('access_token')), 'The access token should no longer be set in any tasks linked to the project since now the project is only available by internal users.')
|
||||
|
||||
def test_search_validates_results(self):
|
||||
project_manager = self.env['res.users'].search([
|
||||
('group_ids', 'in', [self.env.ref('project.group_project_manager').id])
|
||||
],limit=1)
|
||||
self.authenticate(project_manager.login, project_manager.login)
|
||||
self.project_1 = self.env['project.project'].create({'name': 'Portal Search Project 1'})
|
||||
self.project_2 = self.env['project.project'].create({'name': 'Portal Search Project 2'})
|
||||
self.task_1 = self.env['project.task'].create({
|
||||
'name': 'Test Task Name Match',
|
||||
'project_id': self.project_1.id,
|
||||
'user_ids': project_manager,
|
||||
})
|
||||
|
||||
self.task_2 = self.env['project.task'].create({
|
||||
'name': 'Another Task For Searching',
|
||||
'project_id': self.project_2.id,
|
||||
'user_ids': project_manager,
|
||||
})
|
||||
|
||||
url = '/my/tasks'
|
||||
response = self.url_open(url)
|
||||
self.assertIn(self.task_1.name, response.text)
|
||||
self.assertIn(self.task_2.name, response.text)
|
||||
|
||||
url = '/my/tasks?search_in=name&search=Test+Task+Name+Match'
|
||||
response = self.url_open(url)
|
||||
self.assertIn(self.task_1.name, response.text)
|
||||
self.assertNotIn(self.task_2.name, response.text)
|
||||
|
||||
url = '/my/tasks?search_in=project_id&search=%s' % (self.project_1.name)
|
||||
response = self.url_open(url)
|
||||
self.assertIn(self.task_1.name, response.text)
|
||||
self.assertNotIn(self.task_2.name, response.text)
|
||||
|
||||
def test_task_templates_visibility_portal(self):
|
||||
"""
|
||||
Verify that a portal user can see regular tasks but not task templates or their subtasks.
|
||||
"""
|
||||
self.authenticate(self.user_portal.login, self.user_portal.login)
|
||||
portal_project = self.env['project.project'].create({'name': 'Portal Project'})
|
||||
portal_project.message_subscribe(partner_ids=[self.user_portal.partner_id.id])
|
||||
task, task_template = self.env['project.task'].create([
|
||||
{
|
||||
'name': 'Visible Task',
|
||||
'project_id': portal_project.id,
|
||||
},
|
||||
{
|
||||
'name': 'Invisible Template Task',
|
||||
'project_id': portal_project.id,
|
||||
'is_template': True,
|
||||
'child_ids': [
|
||||
Command.create({'name': 'Sub Task of Template Task 1'}),
|
||||
Command.create({'name': 'Sub Task of Template Task 2'}),
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
# Check the portal my tasks page
|
||||
my_tasks_response = self.url_open('/my/tasks')
|
||||
self.assertIn(task.name, my_tasks_response.text)
|
||||
self.assertNotIn(task_template.name, my_tasks_response.text)
|
||||
self.assertNotIn(task_template.child_ids[0].name, my_tasks_response.text)
|
||||
self.assertNotIn(task_template.child_ids[1].name, my_tasks_response.text)
|
||||
|
||||
# Check the tasks page for the specific project
|
||||
project_tasks_response = self.url_open('/my/projects/%s' % (portal_project.id))
|
||||
self.assertIn(task.name, project_tasks_response.text)
|
||||
self.assertNotIn(task_template.name, project_tasks_response.text)
|
||||
self.assertNotIn(task_template.child_ids[0].name, project_tasks_response.text)
|
||||
self.assertNotIn(task_template.child_ids[1].name, project_tasks_response.text)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.tests import Form, users
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
|
@ -12,7 +11,9 @@ class TestProjectCommon(TransactionCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestProjectCommon, cls).setUpClass()
|
||||
cls.env.company.resource_calendar_id.tz = "Europe/Brussels"
|
||||
|
||||
user_group_partner_manager = cls.env.ref('base.group_partner_manager')
|
||||
user_group_employee = cls.env.ref('base.group_user')
|
||||
user_group_project_user = cls.env.ref('project.group_project_user')
|
||||
user_group_project_manager = cls.env.ref('project.group_project_manager')
|
||||
|
|
@ -35,26 +36,26 @@ class TestProjectCommon(TransactionCase):
|
|||
'email': 'b.t@example.com',
|
||||
'signature': 'SignBert',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [(6, 0, [cls.env.ref('base.group_public').id])]})
|
||||
'group_ids': [(6, 0, [cls.env.ref('base.group_public').id])]})
|
||||
cls.user_portal = Users.create({
|
||||
'name': 'Chell Gladys',
|
||||
'login': 'chell',
|
||||
'email': 'chell@gladys.portal',
|
||||
'signature': 'SignChell',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [(6, 0, [cls.env.ref('base.group_portal').id])]})
|
||||
'group_ids': [(6, 0, [cls.env.ref('base.group_portal').id])]})
|
||||
cls.user_projectuser = Users.create({
|
||||
'name': 'Armande ProjectUser',
|
||||
'login': 'armandel',
|
||||
'password': 'armandel',
|
||||
'email': 'armande.projectuser@example.com',
|
||||
'groups_id': [(6, 0, [user_group_employee.id, user_group_project_user.id])]
|
||||
'group_ids': [(6, 0, [user_group_employee.id, user_group_project_user.id, user_group_partner_manager.id])]
|
||||
})
|
||||
cls.user_projectmanager = Users.create({
|
||||
'name': 'Bastien ProjectManager',
|
||||
'login': 'bastien',
|
||||
'email': 'bastien.projectmanager@example.com',
|
||||
'groups_id': [(6, 0, [user_group_employee.id, user_group_project_manager.id])]})
|
||||
'group_ids': [(6, 0, [user_group_employee.id, user_group_project_manager.id, user_group_partner_manager.id])]})
|
||||
|
||||
# Test 'Pigs' project
|
||||
cls.project_pigs = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
|
|
@ -89,15 +90,6 @@ class TestProjectCommon(TransactionCase):
|
|||
})]
|
||||
})
|
||||
|
||||
def format_and_process(self, template, to='groups@example.com, other@gmail.com', subject='Frogs',
|
||||
extra='', email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>',
|
||||
cc='', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
|
||||
model=None, target_model='project.task', target_field='name'):
|
||||
self.assertFalse(self.env[target_model].search([(target_field, '=', subject)]))
|
||||
mail = template.format(to=to, subject=subject, cc=cc, extra=extra, email_from=email_from, msg_id=msg_id)
|
||||
self.env['mail.thread'].message_process(model, mail)
|
||||
return self.env[target_model].search([(target_field, '=', subject)])
|
||||
|
||||
|
||||
class TestProjectBase(TestProjectCommon):
|
||||
|
||||
|
|
@ -152,10 +144,400 @@ class TestProjectBase(TestProjectCommon):
|
|||
user2 = self.user_projectuser.copy()
|
||||
user1.notification_type = 'email'
|
||||
user2.notification_type = 'inbox'
|
||||
for user, filter_invisible_expected in ((user1, True), (user2, None)):
|
||||
for user, filter_visible_expected in ((user1, False), (user2, True)):
|
||||
Task = self.env['project.task'].with_user(user)
|
||||
arch = Task.get_view(self.env.ref('project.view_task_search_form').id, 'search')['arch']
|
||||
arch = Task.get_view(self.env.ref('project.view_task_search_form').id)['arch']
|
||||
tree = etree.fromstring(arch)
|
||||
node = tree.xpath('//filter[@name="message_needaction"]')[0]
|
||||
modifiers = json.loads(node.get('modifiers') or '{}')
|
||||
self.assertEqual(modifiers.get('invisible'), filter_invisible_expected)
|
||||
self.assertEqual(bool(tree.xpath('//filter[@name="message_needaction"]')), filter_visible_expected)
|
||||
|
||||
@users('bastien')
|
||||
def test_search_favorite_order(self):
|
||||
""" Test the search method, ordering by favorite projects.
|
||||
"""
|
||||
self.project_goats.favorite_user_ids += self.user_projectmanager
|
||||
self.env.cr.flush()
|
||||
|
||||
Project = self.env['project.project']
|
||||
project_ids = [self.project_pigs.id, self.project_goats.id]
|
||||
domain = [('id', 'in', project_ids)]
|
||||
|
||||
self.assertEqual(Project.search(domain, order='is_favorite desc')[0], self.project_goats)
|
||||
self.assertEqual(Project.search(domain, order='is_favorite')[-1], self.project_goats)
|
||||
|
||||
self.assertTrue(self.project_pigs.id < self.project_goats.id)
|
||||
self.assertEqual(Project.search(domain, order='id').ids, project_ids)
|
||||
|
||||
@users('bastien')
|
||||
def test_edit_favorite(self):
|
||||
project1, project2 = projects = self.env['project.project'].create([{
|
||||
'name': 'Project Test1',
|
||||
}, {
|
||||
'name': 'Project Test2',
|
||||
'is_favorite': True,
|
||||
}])
|
||||
self.assertFalse(project1.is_favorite)
|
||||
self.assertTrue(project2.is_favorite)
|
||||
project1.is_favorite = True
|
||||
project2.is_favorite = False
|
||||
projects.invalidate_recordset(['is_favorite']) # To force 'is_favorite' to recompute
|
||||
self.assertTrue(project1.is_favorite)
|
||||
self.assertFalse(project2.is_favorite)
|
||||
|
||||
@users('bastien')
|
||||
def test_create_favorite_from_project_form(self):
|
||||
Project = self.env['project.project']
|
||||
form1 = Form(Project)
|
||||
form1.name = 'Project Test1'
|
||||
self.assertFalse(form1.is_favorite)
|
||||
project1 = form1.save()
|
||||
self.assertFalse(project1.is_favorite)
|
||||
|
||||
form2 = Form(Project)
|
||||
form2.name = 'Project Test2'
|
||||
form2.is_favorite = True
|
||||
self.assertTrue(form2.is_favorite)
|
||||
project2 = form2.save()
|
||||
self.assertTrue(project2.is_favorite)
|
||||
|
||||
@users('bastien')
|
||||
def test_edit_favorite_from_project_form(self):
|
||||
project1, project2 = self.env['project.project'].create([{
|
||||
'name': 'Project Test1',
|
||||
}, {
|
||||
'name': 'Project Test2',
|
||||
'is_favorite': True,
|
||||
}])
|
||||
with Form(project1) as form:
|
||||
form.is_favorite = True
|
||||
self.assertTrue(project1.is_favorite)
|
||||
|
||||
with Form(project2) as form:
|
||||
form.is_favorite = False
|
||||
self.assertFalse(project2.is_favorite)
|
||||
|
||||
def test_change_project_or_partner_company(self):
|
||||
""" Tests that it is impossible to change the company of a project
|
||||
if the company of the partner is different and vice versa if the company of the project is set.
|
||||
If the company of the project is not set, there are no restriction on its partner company-wise.
|
||||
"""
|
||||
company_1 = self.env.company
|
||||
company_2 = self.env['res.company'].create({'name': 'Company 2'})
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Partner',
|
||||
})
|
||||
self.project_pigs.partner_id = partner
|
||||
|
||||
# Neither the partner nor the project have a company. Their companies can be updated.
|
||||
self.assertFalse(partner.company_id)
|
||||
self.assertFalse(self.project_pigs.company_id)
|
||||
self.project_pigs.company_id = company_1
|
||||
self.assertEqual(self.project_pigs.company_id, company_1, "The company of the project should have been updated.")
|
||||
self.project_pigs.company_id = False
|
||||
# if the partner company is set, the project's should also be set
|
||||
partner.company_id = company_1
|
||||
|
||||
# If the partner has a company, the project must have the same
|
||||
self.assertEqual(partner.company_id, self.project_pigs.company_id, "The company of the project should have been updated.")
|
||||
|
||||
# The partner has a company and the project has a company. The partner's can only be set to False, the project's can not be changed
|
||||
with self.assertRaises(UserError):
|
||||
# Cannot change the company of a project if both the project and its partner have a company
|
||||
self.project_pigs.company_id = company_2
|
||||
with self.assertRaises(UserError):
|
||||
# Cannot change the company of a partner if both the project and its partner have a company
|
||||
partner.company_id = company_2
|
||||
partner.company_id = False
|
||||
self.project_pigs.company_id = False
|
||||
self.assertFalse(self.project_pigs.company_id, "The company of the project should have been set to False.")
|
||||
self.project_pigs.company_id = company_1
|
||||
self.project_goats.company_id = company_1
|
||||
self.project_goats.partner_id = partner
|
||||
with self.assertRaises(UserError):
|
||||
# Cannot change the company of a partner that part of multiple projects with different companies
|
||||
self.project_goats.partner_id.company_id = company_2
|
||||
|
||||
|
||||
# The project has a company, but the partner has none. The partner can only be set to False/project.company but the project can have any new company.
|
||||
with self.assertRaises(UserError):
|
||||
# Cannot change the company of a partner if both the project and its partner have a company
|
||||
partner.company_id = company_2
|
||||
self.project_pigs.company_id = company_2
|
||||
self.assertEqual(self.project_pigs.company_id, company_2, "The company of the project should have been updated.")
|
||||
self.project_pigs.company_id = False
|
||||
self.assertFalse(self.project_pigs.company_id, "The company of the project should have been set to False.")
|
||||
self.project_pigs.company_id = company_1
|
||||
partner.company_id = company_1
|
||||
self.assertEqual(partner.company_id, company_1, "The company of the partner should have been updated.")
|
||||
|
||||
def test_add_customer_rating_project(self):
|
||||
""" Tests that the rating_ids field contains a rating once created
|
||||
"""
|
||||
rate = self.env['rating.rating'].create({
|
||||
'res_id': self.task_1.id,
|
||||
'parent_res_id': self.project_pigs.id,
|
||||
'res_model_id': self.env['ir.model']._get('project.task').id,
|
||||
'parent_res_model_id': self.env['ir.model']._get('project.project').id,
|
||||
})
|
||||
rating = 5
|
||||
|
||||
self.task_1.rating_apply(rating, token=rate.access_token)
|
||||
|
||||
self.project_pigs.invalidate_recordset(['rating_ids'])
|
||||
self.assertEqual(len(self.project_pigs.rating_ids), 1, "There should be 1 rating linked to the project")
|
||||
|
||||
def test_planned_dates_consistency_for_project(self):
|
||||
""" This test ensures that a project can not have date start set,
|
||||
if its date end is False and that it can not have a date end
|
||||
set if its date start is False .
|
||||
"""
|
||||
self.assertFalse(self.project_goats.date_start)
|
||||
self.assertFalse(self.project_goats.date)
|
||||
|
||||
self.project_goats.write({'date_start': '2021-09-27', 'date': '2021-09-28'})
|
||||
self.assertEqual(fields.Date.to_string(self.project_goats.date_start), '2021-09-27', "The start date should be set.")
|
||||
self.assertEqual(fields.Date.to_string(self.project_goats.date), '2021-09-28', "The expiration date should be set.")
|
||||
|
||||
self.project_goats.date_start = False
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date_start), "The start date should be unset.")
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date), "The expiration date should be unset as well.")
|
||||
|
||||
self.project_goats.write({'date_start': '2021-09-27', 'date': '2021-09-28'})
|
||||
self.project_goats.date = False
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date), "The expiration date should be unset.")
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date_start), "The start date should be unset as well.")
|
||||
|
||||
self.project_goats.write({'date_start': '2021-09-27'})
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date_start), "The start date should be unset since expiration date if not set.")
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date), "The expiration date should stay be unset.")
|
||||
|
||||
self.project_goats.write({'date': '2021-09-28'})
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date), "The expiration date should be unset since the start date if not set.")
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date_start), "The start date should be unset.")
|
||||
|
||||
self.project_pigs.write({'date_start': '2021-09-23', 'date': '2021-09-24'})
|
||||
|
||||
# Case 1: one project has date range set and the other one has no date range set.
|
||||
projects = self.project_goats + self.project_pigs
|
||||
projects.write({'date_start': '2021-09-27', 'date': '2021-09-28'})
|
||||
for p in projects:
|
||||
self.assertEqual(fields.Date.to_string(p.date_start), '2021-09-27', f'The start date of {p.name} should be updated.')
|
||||
self.assertEqual(fields.Date.to_string(p.date), '2021-09-28', f'The expiration date of {p.name} should be updated.')
|
||||
self.project_goats.date_start = False
|
||||
projects.write({'date_start': '2021-09-30'})
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date_start), 'The start date should not be updated')
|
||||
self.assertFalse(fields.Date.to_string(self.project_goats.date), 'The expiration date should not be updated')
|
||||
self.assertEqual(fields.Date.to_string(self.project_pigs.date_start), '2021-09-27', 'The start date should not be updated.')
|
||||
self.assertEqual(fields.Date.to_string(self.project_pigs.date), '2021-09-28', 'The expiration date should not be updated.')
|
||||
projects.write({'date_start': False})
|
||||
for p in projects:
|
||||
self.assertFalse(fields.Date.to_string(p.date_start), f'The start date of {p.name} should be set to False.')
|
||||
self.assertFalse(fields.Date.to_string(p.date), f'The expiration date of {p.name} should be set to False.')
|
||||
self.project_pigs.write({'date_start': '2021-09-27', 'date': '2021-09-28'})
|
||||
projects.write({'date': False})
|
||||
for p in projects:
|
||||
self.assertFalse(fields.Date.to_string(p.date_start), f'The start date of {p.name} should be set to False.')
|
||||
self.assertFalse(fields.Date.to_string(p.date), f'The expiration date of {p.name} should be set to False.')
|
||||
|
||||
# Case 2: both projects have no date range set
|
||||
projects.write({'date_start': '2021-09-27'})
|
||||
for p in projects:
|
||||
self.assertFalse(fields.Date.to_string(p.date_start), f'The start date of {p.name} should not be updated.')
|
||||
self.assertFalse(fields.Date.to_string(p.date), f'The expiration date of {p.name} should not be updated.')
|
||||
projects.write({'date': '2021-09-28'})
|
||||
for p in projects:
|
||||
self.assertFalse(fields.Date.to_string(p.date_start), f'The start date of {p.name} should not be updated.')
|
||||
self.assertFalse(fields.Date.to_string(p.date), f'The expiration date of {p.name} should not be updated.')
|
||||
|
||||
projects.write({'date_start': '2021-09-27', 'date': '2021-09-28'})
|
||||
for p in projects:
|
||||
self.assertEqual(fields.Date.to_string(p.date_start), '2021-09-27', f'The start date of {p.name} should be updated.')
|
||||
self.assertEqual(fields.Date.to_string(p.date), '2021-09-28', f'The expiration date of {p.name} should be updated.')
|
||||
|
||||
# Case 3: both projects have a different date range set
|
||||
self.project_pigs.write({'date_start': '2021-09-23', 'date': '2021-09-30'})
|
||||
projects.write({'date_start': '2021-09-22'})
|
||||
self.assertEqual(fields.Date.to_string(self.project_goats.date_start), '2021-09-22', 'The start date should be updated.')
|
||||
self.assertEqual(fields.Date.to_string(self.project_goats.date), '2021-09-28', 'The expiration date should not be updated.')
|
||||
self.assertEqual(fields.Date.to_string(self.project_pigs.date_start), '2021-09-22', 'The start date should be updated.')
|
||||
self.assertEqual(fields.Date.to_string(self.project_pigs.date), '2021-09-30', 'The expiration date should not be updated.')
|
||||
projects.write({'date': '2021-09-29'})
|
||||
self.assertEqual(fields.Date.to_string(self.project_goats.date_start), '2021-09-22', 'The start date should not be updated.')
|
||||
self.assertEqual(fields.Date.to_string(self.project_goats.date), '2021-09-29', 'The expiration date should be updated.')
|
||||
self.assertEqual(fields.Date.to_string(self.project_pigs.date_start), '2021-09-22', 'The start date should not be updated.')
|
||||
self.assertEqual(fields.Date.to_string(self.project_pigs.date), '2021-09-29', 'The expiration date should be updated.')
|
||||
projects.write({'date_start': False})
|
||||
for p in projects:
|
||||
self.assertFalse(fields.Date.to_string(p.date_start), f'The start date of {p.name} should be set to False.')
|
||||
self.assertFalse(fields.Date.to_string(p.date), f'The expiration date of {p.name} should be set to False.')
|
||||
self.project_goats.write({'date_start': '2021-09-27', 'date': '2021-09-28'})
|
||||
self.project_pigs.write({'date_start': '2021-09-23', 'date': '2021-09-30'})
|
||||
projects.write({'date': False})
|
||||
for p in projects:
|
||||
self.assertFalse(fields.Date.to_string(p.date_start), f'The start date of {p.name} should be set to False.')
|
||||
self.assertFalse(fields.Date.to_string(p.date), f'The expiration date of {p.name} should be set to False.')
|
||||
self.project_goats.write({'date_start': '2021-09-27', 'date': '2021-09-28'})
|
||||
self.project_pigs.write({'date_start': '2021-09-23', 'date': '2021-09-30'})
|
||||
projects.write({'date_start': '2021-09-25', 'date': '2021-09-26'})
|
||||
for p in projects:
|
||||
self.assertEqual(fields.Date.to_string(p.date_start), '2021-09-25', f'The start date of {p.name} should be updated.')
|
||||
self.assertEqual(fields.Date.to_string(p.date), '2021-09-26', f'The expiration date of {p.name} should be updated.')
|
||||
|
||||
def test_create_task_in_batch_with_email_cc(self):
|
||||
user_a, user_b, user_c = self.env['res.users'].create([{
|
||||
'name': 'user A',
|
||||
'login': 'loginA',
|
||||
'email': 'email@bisous1',
|
||||
}, {
|
||||
'name': 'user B',
|
||||
'login': 'loginB',
|
||||
'email': 'email@bisous2',
|
||||
}, {
|
||||
'name': 'user C',
|
||||
'login': 'loginC',
|
||||
'email': 'email@bisous3',
|
||||
}])
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'partner',
|
||||
'email': 'email@bisous4',
|
||||
})
|
||||
task_1, task_2 = self.env['project.task'].with_context({'mail_create_nolog': True}).create([{
|
||||
'name': 'task 1',
|
||||
'project_id': self.project_pigs.id,
|
||||
'email_cc': 'email@bisous1, email@bisous2, email@bisous4'
|
||||
}, {
|
||||
'name': 'task 2',
|
||||
'project_id': self.project_pigs.id,
|
||||
'email_cc': 'email@bisous3, email@bisous2, email@bisous4'
|
||||
}])
|
||||
self.assertTrue(user_a.partner_id in task_1.message_partner_ids)
|
||||
self.assertTrue(user_b.partner_id in task_1.message_partner_ids)
|
||||
self.assertFalse(user_c.partner_id in task_1.message_partner_ids)
|
||||
self.assertFalse(partner in task_1.message_partner_ids)
|
||||
self.assertFalse(user_a.partner_id in task_2.message_partner_ids)
|
||||
self.assertTrue(user_b.partner_id in task_2.message_partner_ids)
|
||||
self.assertTrue(user_c.partner_id in task_2.message_partner_ids)
|
||||
self.assertFalse(partner in task_2.message_partner_ids)
|
||||
|
||||
def test_create_private_task_in_batch(self):
|
||||
""" This test ensures that copying private task in batch can be done correctly."""
|
||||
|
||||
task_0, task_1 = self.env['project.task'].create([{
|
||||
'name': f'task {i}',
|
||||
'user_ids': self.env.user.ids,
|
||||
'project_id': False,
|
||||
} for i in range(2)]).copy()
|
||||
self.assertEqual(task_0.name, 'task 0 (copy)')
|
||||
self.assertEqual(task_1.name, 'task 1 (copy)')
|
||||
|
||||
def test_duplicate_project_with_tasks(self):
|
||||
""" Test to check duplication of projects tasks active state. """
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project',
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'project_id': project.id,
|
||||
})
|
||||
|
||||
# Duplicate active project with active task
|
||||
project_dup = project.copy()
|
||||
self.assertTrue(project_dup.active, "Active project should remain active when duplicating an active project")
|
||||
self.assertEqual(project_dup.task_count, 1, "Duplicated project should have as many tasks as orginial project")
|
||||
self.assertTrue(project_dup.tasks.active, "Active task should remain active when duplicating an active project")
|
||||
|
||||
# Duplicate active project with archived task
|
||||
task.active = False
|
||||
project_dup = project.copy()
|
||||
self.assertTrue(project_dup.active, "Active project should remain active when duplicating an active project")
|
||||
self.assertFalse(project_dup.tasks.active, "Archived task should remain archived when duplicating an active project")
|
||||
|
||||
# Duplicate archived project with archived task
|
||||
project.active = False
|
||||
project_dup = project.copy()
|
||||
self.assertTrue(project_dup.active, "The new project should be active by default")
|
||||
self.assertTrue(project_dup.tasks.active, "Archived task should be active when duplicating an archived project")
|
||||
|
||||
def test_create_analytic_account_batch(self):
|
||||
""" This test will check that the '_create_analytic_account' method assigns the accounts to the projects in the right order. """
|
||||
projects = self.env["project.project"].create([{
|
||||
"name": f"Project {x}",
|
||||
} for x in range(10)])
|
||||
projects._create_analytic_account()
|
||||
self.assertEqual(projects.mapped("name"), projects.account_id.mapped("name"), "The analytic accounts names should match with the projects.")
|
||||
|
||||
def test_task_count(self):
|
||||
project1, project2 = self.env['project.project'].create([
|
||||
{'name': 'project1'},
|
||||
{'name': 'project2'},
|
||||
])
|
||||
self.env['project.task'].with_context(default_project_id=project1.id).create([
|
||||
{'name': 'task1'},
|
||||
{'name': 'task2', 'state': '1_done'},
|
||||
{'name': 'task3', 'child_ids': [
|
||||
Command.create({'name': 'subtask1', 'project_id': project1.id}),
|
||||
Command.create({'name': 'subtask2', 'project_id': project1.id, 'state': '1_canceled'}),
|
||||
Command.create({'name': 'subtask3', 'project_id': project2.id}),
|
||||
Command.create({'name': 'subtask4', 'project_id': project1.id, 'child_ids': [
|
||||
Command.create({'name': 'subsubtask41', 'project_id': project2.id}),
|
||||
Command.create({'name': 'subsubtask42', 'project_id': project1.id})
|
||||
]}),
|
||||
Command.create({'name': 'subtask5', 'state': '1_done', 'project_id': project1.id, 'child_ids': [
|
||||
Command.create({'name': 'subsubtask51', 'project_id': project1.id, 'state': '1_done'}),
|
||||
]}),
|
||||
]}
|
||||
])
|
||||
self.assertEqual(project1.task_count, 9)
|
||||
self.assertEqual(project1.open_task_count, 5)
|
||||
self.assertEqual(project1.closed_task_count, 4)
|
||||
self.assertEqual(project2.task_count, 2)
|
||||
self.assertEqual(project2.open_task_count, 2)
|
||||
self.assertEqual(project2.closed_task_count, 0)
|
||||
|
||||
def test_archived_duplicate_task(self):
|
||||
""" Test to check duplication of an archived task.
|
||||
The duplicate of an archived task should be active.
|
||||
"""
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project',
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'project_id': project.id,
|
||||
})
|
||||
copy_task1 = task.copy()
|
||||
self.assertTrue(copy_task1.active, "Active task should be active when duplicating an active task")
|
||||
task.active = False
|
||||
copy_task2 = task.copy()
|
||||
self.assertTrue(copy_task2.active, "Archived task should be active when duplicating an archived task")
|
||||
|
||||
def test_duplicate_doesnt_copy_date(self):
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project',
|
||||
'date_start': '2021-09-20',
|
||||
'date': '2021-09-28',
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'project_id': project.id,
|
||||
'date_deadline': '2021-09-26',
|
||||
})
|
||||
project_copy = project.copy()
|
||||
self.assertFalse(project_copy.date_start, "The project's date fields shouldn't be copied on project duplication")
|
||||
self.assertFalse(project_copy.date, "The project's date fields shouldn't be copied on project duplication")
|
||||
self.assertFalse(project_copy.task_ids.date_deadline, "The task's date fields shouldn't be copied on project duplication")
|
||||
self.assertFalse(task.copy().date_deadline, "The task's date fields shouldn't be copied on task duplication")
|
||||
|
||||
def test_archived_subtask_not_copied_during_parent_task_duplication(self):
|
||||
"""Test that archived subtasks are not copied when duplicating a parent task."""
|
||||
parent_task = self.env['project.task'].create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_pigs.id,
|
||||
'child_ids': [
|
||||
Command.create({
|
||||
'name': 'child task',
|
||||
'project_id': self.project_pigs.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
parent_task.child_ids.active = False
|
||||
self.assertFalse(parent_task.copy().child_ids, "Archived subtask should not be copied")
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestProjectConfig(TestProjectCommon):
|
||||
"""Test module configuration and its effects on projects."""
|
||||
|
|
@ -13,49 +7,120 @@ class TestProjectConfig(TestProjectCommon):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestProjectConfig, cls).setUpClass()
|
||||
cls.Project = cls.env["project.project"]
|
||||
cls.Settings = cls.env["res.config.settings"]
|
||||
cls.features = (
|
||||
# Pairs of associated (config_flag, project_flag)
|
||||
("group_subtask_project", "allow_subtasks"),
|
||||
("group_project_recurring_tasks", "allow_recurring_tasks"),
|
||||
("group_project_rating", "rating_active"),
|
||||
)
|
||||
cls.user_settings_project_manager = cls.env['res.users.settings']._find_or_create_for_user(cls.user_projectmanager)
|
||||
cls.user_settings_project_user = cls.env['res.users.settings']._find_or_create_for_user(cls.user_projectuser)
|
||||
|
||||
# Start with a known value on feature flags to ensure validity of tests
|
||||
cls._set_feature_status(is_enabled=False)
|
||||
|
||||
@classmethod
|
||||
def _set_feature_status(cls, is_enabled):
|
||||
"""Set enabled/disabled status of all optional features in the
|
||||
project app config to is_enabled (boolean).
|
||||
def test_project_stages_feature_enable_views(self):
|
||||
"""Check that the Gantt, Calendar and Activities views are
|
||||
enabled when the 'Project Stage' feature is enabled.
|
||||
"""
|
||||
features_config = cls.Settings.create(
|
||||
{feature[0]: is_enabled for feature in cls.features})
|
||||
features_config.execute()
|
||||
self.Settings.create({"group_project_stages": True}).execute() # enabling feature
|
||||
menu_ids = set([self.env.ref('project.menu_projects').id, self.env.ref('project.menu_projects_config').id])
|
||||
menu_loaded = set(self.env['ir.ui.menu']._load_menus_blacklist())
|
||||
self.assertTrue(menu_ids.issubset(menu_loaded), "The menu project and menu projects config should be loaded")
|
||||
|
||||
def test_existing_projects_enable_features(self):
|
||||
"""Check that *existing* projects have features enabled when
|
||||
the user enables them in the module configuration.
|
||||
def test_copy_project_manager_embedded_config_as_default(self):
|
||||
"""
|
||||
self._set_feature_status(is_enabled=True)
|
||||
for config_flag, project_flag in self.features:
|
||||
self.assertTrue(
|
||||
self.project_pigs[project_flag],
|
||||
"Existing project failed to adopt activation of "
|
||||
f"{config_flag}/{project_flag} feature")
|
||||
Test that when a user gets the embedded actions configs of a projet, he gets the configs of
|
||||
the project manager as default if he has no personal configuration yet for a particular action.
|
||||
The configs he gets should also be copied in its user's settings.
|
||||
"""
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Test Project',
|
||||
'user_id': self.user_projectmanager.id,
|
||||
})
|
||||
project_action = self.env['ir.actions.act_window'].create({
|
||||
'name': 'Test Project Action',
|
||||
'res_model': 'project.project',
|
||||
})
|
||||
# Create the embedded action config for the project manager
|
||||
project_manager_config = self.env['res.users.settings.embedded.action'].create({
|
||||
'user_setting_id': self.user_settings_project_manager.id,
|
||||
'action_id': project_action.id,
|
||||
'res_model': 'project.project',
|
||||
'res_id': project.id,
|
||||
'embedded_actions_order': 'false,3,2,1',
|
||||
'embedded_actions_visibility': '1,2,3',
|
||||
'embedded_visibility': True,
|
||||
})
|
||||
|
||||
def test_new_projects_enable_features(self):
|
||||
"""Check that after the user enables features in the module
|
||||
configuration, *newly created* projects have those features
|
||||
enabled as well.
|
||||
# The project user has no personal configuration yet
|
||||
self.assertFalse(self.user_settings_project_user.embedded_actions_config_ids)
|
||||
# He should get the project manager configuration as default
|
||||
project_user_config = self.user_settings_project_user.with_context(
|
||||
res_model='project.project',
|
||||
res_id=project.id,
|
||||
).get_embedded_actions_settings()
|
||||
self.assertDictEqual(
|
||||
project_user_config[f'{project_action.id}+{project.id}'],
|
||||
{
|
||||
'embedded_actions_order': [False, 3, 2, 1],
|
||||
'embedded_actions_visibility': [1, 2, 3],
|
||||
'embedded_visibility': True,
|
||||
},
|
||||
)
|
||||
|
||||
# Check that the config has indeed been copied for the project user
|
||||
project_user_config = self.user_settings_project_user.embedded_actions_config_ids
|
||||
self.assertEqual(len(project_user_config), 1, 'There should be one embedded action setting created.')
|
||||
self.assertEqual(project_user_config.action_id, project_manager_config.action_id, 'The action should match the one set in the project manager config.')
|
||||
self.assertEqual(project_user_config.res_id, project_manager_config.res_id, 'The res_id should match the one set in the project manager config.')
|
||||
self.assertEqual(project_user_config.res_model, project_manager_config.res_model, 'The res_model should match the one set in the project manager config.')
|
||||
self.assertEqual(project_user_config.embedded_actions_order, project_manager_config.embedded_actions_order, 'The embedded actions order should match the one set in the project manager config.')
|
||||
self.assertEqual(project_user_config.embedded_actions_visibility, project_manager_config.embedded_actions_visibility, 'The embedded actions visibility should match the one set in the project manager config.')
|
||||
self.assertEqual(project_user_config.embedded_visibility, project_manager_config.embedded_visibility, 'The embedded visibility should match the one set in the project manager config.')
|
||||
|
||||
def test_no_copy_project_manager_embedded_config_as_default(self):
|
||||
"""
|
||||
self._set_feature_status(is_enabled=True)
|
||||
project_cows = self.Project.create({
|
||||
"name": "Cows",
|
||||
"partner_id": self.partner_1.id})
|
||||
for config_flag, project_flag in self.features:
|
||||
self.assertTrue(
|
||||
project_cows[project_flag],
|
||||
f"Newly created project failed to adopt activation of "
|
||||
f"{config_flag}/{project_flag} feature")
|
||||
Test that when a user gets the embedded actions configs of a projet, he does not get the configs of
|
||||
the project manager as default if he already has a personal configuration for some actions.
|
||||
"""
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Test Project',
|
||||
'user_id': self.user_projectmanager.id,
|
||||
})
|
||||
project_action = self.env['ir.actions.act_window'].create({
|
||||
'name': 'Test Project Action',
|
||||
'res_model': 'project.project',
|
||||
})
|
||||
# Create the embedded action config for the project manager
|
||||
self.env['res.users.settings.embedded.action'].create({
|
||||
'user_setting_id': self.user_settings_project_manager.id,
|
||||
'action_id': project_action.id,
|
||||
'res_model': 'project.project',
|
||||
'res_id': project.id,
|
||||
'embedded_actions_order': 'false,3,2,1',
|
||||
'embedded_actions_visibility': '1,2,3',
|
||||
'embedded_visibility': True,
|
||||
})
|
||||
# The project user already has a personal configuration
|
||||
user_config = self.env['res.users.settings.embedded.action'].create({
|
||||
'user_setting_id': self.user_settings_project_user.id,
|
||||
'action_id': project_action.id,
|
||||
'res_model': 'project.project',
|
||||
'res_id': project.id,
|
||||
'embedded_actions_order': 'false,1,2,3',
|
||||
'embedded_actions_visibility': '2,3',
|
||||
'embedded_visibility': False,
|
||||
})
|
||||
self.assertEqual(len(self.user_settings_project_user.embedded_actions_config_ids), 1)
|
||||
|
||||
# He should get his personal configuration as default
|
||||
project_user_config = self.user_settings_project_user.with_context(
|
||||
res_model='project.project',
|
||||
res_id=project.id,
|
||||
).get_embedded_actions_settings()
|
||||
self.assertDictEqual(
|
||||
project_user_config[f'{project_action.id}+{project.id}'],
|
||||
{
|
||||
'embedded_actions_order': [False, 1, 2, 3],
|
||||
'embedded_actions_visibility': [2, 3],
|
||||
'embedded_visibility': False,
|
||||
},
|
||||
)
|
||||
|
||||
# Check that no new config has been created for the project user
|
||||
project_user_config = self.user_settings_project_user.embedded_actions_config_ids
|
||||
self.assertEqual(len(project_user_config), 1, 'There should still be only one embedded action setting created.')
|
||||
self.assertEqual(project_user_config, user_config, 'The existing user config should be unchanged.')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
class TestProjectEmbeddedActionSettings(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user = cls.env['res.users'].create({
|
||||
'name': 'Test User',
|
||||
'login': 'test_user',
|
||||
'password': 'test_password',
|
||||
})
|
||||
cls.user_settings = cls.env['res.users.settings']._find_or_create_for_user(cls.user)
|
||||
cls.window_action = cls.env['ir.actions.act_window'].create({
|
||||
'name': 'Test Action',
|
||||
'res_model': 'project.project',
|
||||
})
|
||||
cls.embedded_action_1, cls.embedded_action_2 = cls.env['ir.embedded.actions'].create([
|
||||
{
|
||||
'name': 'Embedded Action 1',
|
||||
'parent_res_model': 'project.project',
|
||||
'parent_action_id': cls.window_action.id,
|
||||
'action_id': cls.window_action.id,
|
||||
},
|
||||
{
|
||||
'name': 'Embedded Action 2',
|
||||
'parent_res_model': 'project.project',
|
||||
'parent_action_id': cls.window_action.id,
|
||||
'action_id': cls.window_action.id,
|
||||
},
|
||||
])
|
||||
cls.embedded_action_settings = cls.env['res.users.settings.embedded.action'].create({
|
||||
'user_setting_id': cls.user_settings.id,
|
||||
'action_id': cls.window_action.id,
|
||||
'res_model': 'project.project',
|
||||
'res_id': cls.project_goats.id,
|
||||
'embedded_actions_order': f'{cls.embedded_action_2.id},false,{cls.embedded_action_1.id}',
|
||||
'embedded_actions_visibility': f'{cls.embedded_action_1.id},{cls.embedded_action_2.id}',
|
||||
'embedded_visibility': True,
|
||||
})
|
||||
|
||||
def test_copy_embedded_action_settings(self):
|
||||
'''
|
||||
Test that embedded action settings are copied correctly when a project is copied.
|
||||
'''
|
||||
copied_project = self.project_goats.copy()
|
||||
# Check if the embedded action settings are copied
|
||||
copied_settings = self.env['res.users.settings.embedded.action'].search([
|
||||
('user_setting_id', '=', self.user_settings.id),
|
||||
('action_id', '=', self.window_action.id),
|
||||
('res_id', '=', copied_project.id),
|
||||
])
|
||||
self.assertEqual(len(copied_settings), 1, 'There should be one embedded action setting for the copied project.')
|
||||
self.assertEqual(len(self.user_settings.embedded_actions_config_ids), 2, 'There should be two embedded action settings after copying the project.')
|
||||
self.assertEqual(copied_settings.action_id, self.embedded_action_settings.action_id, 'The action_id should match the original.')
|
||||
self.assertEqual(copied_settings.res_id, copied_project.id, 'The res_id should match the copied project id.')
|
||||
self.assertEqual(copied_settings.res_model, self.embedded_action_settings.res_model, 'The res_model should match the original.')
|
||||
self.assertEqual(copied_settings.embedded_actions_order, self.embedded_action_settings.embedded_actions_order, 'The embedded actions order should match the original.')
|
||||
self.assertEqual(copied_settings.embedded_actions_visibility, self.embedded_action_settings.embedded_actions_visibility, 'The embedded actions visibility should match the original.')
|
||||
self.assertEqual(copied_settings.embedded_visibility, self.embedded_action_settings.embedded_visibility, 'The embedded visibility should match the original.')
|
||||
|
||||
def test_copy_custom_embedded_action_settings(self):
|
||||
'''
|
||||
Test that the user-specific actions are not copied in the settings when a project is copied.
|
||||
'''
|
||||
# Create user-specific and shared embedded actions
|
||||
user_specific_embedded_action, shared_embedded_action = self.env['ir.embedded.actions'].create([
|
||||
{
|
||||
'name': 'Custom User-Specific Action',
|
||||
'parent_res_model': 'project.project',
|
||||
'parent_action_id': self.window_action.id,
|
||||
'action_id': self.window_action.id,
|
||||
'parent_res_id': self.project_pigs.id,
|
||||
'user_id': self.user.id, # User-specific action
|
||||
},
|
||||
{
|
||||
'name': 'Custom Shared Action',
|
||||
'parent_res_model': 'project.project',
|
||||
'parent_action_id': self.window_action.id,
|
||||
'action_id': self.window_action.id,
|
||||
'parent_res_id': self.project_pigs.id,
|
||||
'user_id': False, # Shared action
|
||||
},
|
||||
])
|
||||
# Create settings containing those embedded actions
|
||||
self.env['res.users.settings.embedded.action'].create({
|
||||
'user_setting_id': self.user_settings.id,
|
||||
'action_id': self.window_action.id,
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_pigs.id,
|
||||
'embedded_actions_order': f'{shared_embedded_action.id},{user_specific_embedded_action.id}',
|
||||
'embedded_actions_visibility': f'{user_specific_embedded_action.id},{shared_embedded_action.id}',
|
||||
'embedded_visibility': True,
|
||||
})
|
||||
# Copy the project
|
||||
copied_project = self.project_pigs.copy()
|
||||
# Get the copied shared embedded action
|
||||
copied_shared_embedded_action = self.env['ir.embedded.actions'].search([
|
||||
('parent_res_model', '=', 'project.project'),
|
||||
('parent_res_id', '=', copied_project.id),
|
||||
('user_id', '=', False) # Ensure it's a shared action
|
||||
], limit=1)
|
||||
# Check if the shared embedded action settings are correctly copied
|
||||
copied_settings = self.env['res.users.settings.embedded.action'].search([
|
||||
('user_setting_id', '=', self.user_settings.id),
|
||||
('action_id', '=', self.window_action.id),
|
||||
('res_id', '=', copied_project.id),
|
||||
])
|
||||
self.assertEqual(len(copied_settings), 1, 'There should be one embedded action setting for the copied project.')
|
||||
self.assertEqual(len(self.user_settings.embedded_actions_config_ids), 3, 'There should be three embedded action settings after copying the project.')
|
||||
self.assertEqual(copied_settings.action_id, self.embedded_action_settings.action_id, 'The action_id should match the original.')
|
||||
self.assertEqual(copied_settings.res_id, copied_project.id, 'The res_id should match the copied project id.')
|
||||
self.assertEqual(copied_settings.res_model, self.embedded_action_settings.res_model, 'The res_model should match the original.')
|
||||
self.assertEqual(copied_settings.embedded_actions_order, str(copied_shared_embedded_action.id), 'Only the shared embedded action should be copied in the actions order.')
|
||||
self.assertEqual(copied_settings.embedded_actions_visibility, str(copied_shared_embedded_action.id), 'Only the shared embedded action should be copied in the visible actions.')
|
||||
self.assertEqual(copied_settings.embedded_visibility, self.embedded_action_settings.embedded_visibility, 'The embedded visibility should match the original.')
|
||||
|
||||
def test_unlink_project_removes_embedded_action_settings(self):
|
||||
'''
|
||||
Test that unlinking a project removes the embedded action settings associated with it.
|
||||
'''
|
||||
self.assertTrue(self.embedded_action_settings.exists(), 'The embedded action settings should exist before unlinking the project.')
|
||||
self.project_goats.unlink()
|
||||
# Check if the embedded action settings are removed
|
||||
self.assertFalse(self.embedded_action_settings.exists(), 'The embedded action settings should be removed when the project is unlinked.')
|
||||
remaining_settings = self.env['res.users.settings.embedded.action'].search([
|
||||
('user_setting_id', '=', self.user_settings.id),
|
||||
('action_id', '=', self.window_action.id),
|
||||
('res_id', '=', self.project_goats.id),
|
||||
])
|
||||
self.assertEqual(len(remaining_settings), 0, 'There should be no more embedded action settings for the unlinked project.')
|
||||
|
|
@ -1,138 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
from odoo import Command
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.mail.tests.common import MailCase
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
EMAIL_TPL = """Return-Path: <whatever-2a840@postmaster.twitter.com>
|
||||
X-Original-To: {to}
|
||||
Delivered-To: {to}
|
||||
To: {to}
|
||||
cc: {cc}
|
||||
Received: by mail1.odoo.com (Postfix, from userid 10002)
|
||||
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
|
||||
Message-ID: {msg_id}
|
||||
Date: Tue, 29 Nov 2011 12:43:21 +0530
|
||||
From: {email_from}
|
||||
MIME-Version: 1.0
|
||||
Subject: {subject}
|
||||
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
|
||||
|
||||
Hello,
|
||||
|
||||
This email should create a new entry in your module. Please check that it
|
||||
effectively works.
|
||||
|
||||
Thanks,
|
||||
|
||||
--
|
||||
Raoul Boitempoils
|
||||
Integrator at Agrolait"""
|
||||
|
||||
|
||||
class TestProjectFlow(TestProjectCommon, MailCommon):
|
||||
class TestProjectFlow(TestProjectCommon, MailCase):
|
||||
|
||||
def test_project_process_project_manager_duplicate(self):
|
||||
pigs = self.project_pigs.with_user(self.user_projectmanager)
|
||||
dogs = pigs.copy()
|
||||
self.assertEqual(len(dogs.tasks), 2, 'project: duplicating a project must duplicate its tasks')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread')
|
||||
def test_task_process_without_stage(self):
|
||||
# Do: incoming mail from an unknown partner on an alias creates a new task 'Frogs'
|
||||
task = self.format_and_process(
|
||||
EMAIL_TPL, to='project+pigs@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
|
||||
email_from='%s' % self.user_projectuser.email,
|
||||
subject='Frogs', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
|
||||
target_model='project.task')
|
||||
|
||||
# Test: one task created by mailgateway administrator
|
||||
self.assertEqual(len(task), 1, 'project: message_process: a new project.task should have been created')
|
||||
# Test: check partner in message followers
|
||||
self.assertIn(self.partner_2, task.message_partner_ids, "Partner in message cc is not added as a task followers.")
|
||||
# Test: messages
|
||||
self.assertEqual(len(task.message_ids), 1,
|
||||
'project: message_process: newly created task should have 1 message: email')
|
||||
self.assertEqual(task.message_ids.subtype_id, self.env.ref('project.mt_task_new'),
|
||||
'project: message_process: first message of new task should have Task Created subtype')
|
||||
self.assertEqual(task.message_ids.author_id, self.user_projectuser.partner_id,
|
||||
'project: message_process: second message should be the one from Agrolait (partner failed)')
|
||||
self.assertEqual(task.message_ids.subject, 'Frogs',
|
||||
'project: message_process: second message should be the one from Agrolait (subject failed)')
|
||||
# Test: task content
|
||||
self.assertEqual(task.name, 'Frogs', 'project_task: name should be the email subject')
|
||||
self.assertEqual(task.project_id, self.project_pigs, 'project_task: incorrect project')
|
||||
self.assertEqual(task.stage_id.sequence, False, "project_task: shouldn't have a stage, i.e. sequence=False")
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread')
|
||||
def test_task_process_with_stages(self):
|
||||
# Do: incoming mail from an unknown partner on an alias creates a new task 'Cats'
|
||||
task = self.format_and_process(
|
||||
EMAIL_TPL, to='project+goats@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
|
||||
email_from='%s' % self.user_projectuser.email,
|
||||
subject='Cats', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
|
||||
target_model='project.task')
|
||||
|
||||
# Test: one task created by mailgateway administrator
|
||||
self.assertEqual(len(task), 1, 'project: message_process: a new project.task should have been created')
|
||||
# Test: check partner in message followers
|
||||
self.assertIn(self.partner_2, task.message_partner_ids, "Partner in message cc is not added as a task followers.")
|
||||
# Test: messages
|
||||
self.assertEqual(len(task.message_ids), 1,
|
||||
'project: message_process: newly created task should have 1 messages: email')
|
||||
self.assertEqual(task.message_ids.subtype_id, self.env.ref('project.mt_task_new'),
|
||||
'project: message_process: first message of new task should have Task Created subtype')
|
||||
self.assertEqual(task.message_ids.author_id, self.user_projectuser.partner_id,
|
||||
'project: message_process: first message should be the one from Agrolait (partner failed)')
|
||||
self.assertEqual(task.message_ids.subject, 'Cats',
|
||||
'project: message_process: first message should be the one from Agrolait (subject failed)')
|
||||
# Test: task content
|
||||
self.assertEqual(task.name, 'Cats', 'project_task: name should be the email subject')
|
||||
self.assertEqual(task.project_id, self.project_goats, 'project_task: incorrect project')
|
||||
self.assertEqual(task.stage_id.sequence, 1, "project_task: should have a stage with sequence=1")
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread')
|
||||
def test_task_from_email_alias(self):
|
||||
# Do: incoming mail from a known partner email on an alias creates a new task 'Super Frog'
|
||||
task = self.format_and_process(
|
||||
EMAIL_TPL, to='project+goats@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
|
||||
email_from='%s' % self.user_portal.email,
|
||||
subject='Super Frog', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
|
||||
target_model='project.task')
|
||||
|
||||
# Test: one task created by mailgateway administrator
|
||||
self.assertEqual(len(task), 1, 'project: message_process: a new project.task should have been created')
|
||||
# Test: check partner in message followers
|
||||
self.assertIn(self.partner_2, task.message_partner_ids, "Partner in message cc is not added as a task followers.")
|
||||
# Test: check partner has not been assgined
|
||||
self.assertFalse(task.user_ids, "Partner is not added as an assignees")
|
||||
# Test: messages
|
||||
self.assertEqual(len(task.message_ids), 1,
|
||||
'project: message_process: newly created task should have 1 messages: email')
|
||||
self.assertEqual(task.message_ids.subtype_id, self.env.ref('project.mt_task_new'),
|
||||
'project: message_process: first message of new task should have Task Created subtype')
|
||||
self.assertEqual(task.message_ids.author_id, self.user_portal.partner_id,
|
||||
'project: message_process: first message should be the one from Agrolait (partner failed)')
|
||||
self.assertEqual(task.message_ids.subject, 'Super Frog',
|
||||
'project: message_process: first message should be the one from Agrolait (subject failed)')
|
||||
# Test: task content
|
||||
self.assertEqual(task.name, 'Super Frog', 'project_task: name should be the email subject')
|
||||
self.assertEqual(task.project_id, self.project_goats, 'project_task: incorrect project')
|
||||
self.assertEqual(task.stage_id.sequence, 1, "project_task: should have a stage with sequence=1")
|
||||
self.assertEqual(
|
||||
task.description,
|
||||
Markup(
|
||||
'<pre>Hello,\n\nThis email should create a new entry in your module. Please check that it\neffectively works.\n\nThanks,\n<span data-o-mail-quote="1">\n--\nRaoul Boitempoils\nIntegrator at Agrolait</span></pre>\n'
|
||||
),
|
||||
'The task description should be the email content.',
|
||||
)
|
||||
|
||||
def test_subtask_process(self):
|
||||
"""
|
||||
Check subtask mecanism and change it from project.
|
||||
|
|
@ -153,7 +31,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
'user_ids': self.user_projectuser,
|
||||
'project_id': self.project_pigs.id,
|
||||
'partner_id': self.partner_2.id,
|
||||
'planned_hours': 12,
|
||||
'allocated_hours': 12,
|
||||
})
|
||||
|
||||
another_parent_task = Task.create({
|
||||
|
|
@ -161,7 +39,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
'user_ids': self.user_projectuser,
|
||||
'project_id': self.project_pigs.id,
|
||||
'partner_id': self.partner_3.id,
|
||||
'planned_hours': 0,
|
||||
'allocated_hours': 0,
|
||||
})
|
||||
|
||||
# remove the partner_id of the 'goats' project
|
||||
|
|
@ -174,7 +52,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
# the child task 1 is linked to a project without partner_id (goats project)
|
||||
child_task_1 = Task.with_context(default_project_id=self.project_goats.id, default_parent_id=parent_task.id).create({
|
||||
'name': 'Task Child with project',
|
||||
'planned_hours': 3,
|
||||
'allocated_hours': 3,
|
||||
})
|
||||
|
||||
# the child task 2 is linked to a project with a partner_id (pigs project)
|
||||
|
|
@ -182,8 +60,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
'name': 'Task Child without project',
|
||||
'parent_id': parent_task.id,
|
||||
'project_id': self.project_pigs.id,
|
||||
'display_project_id': self.project_pigs.id,
|
||||
'planned_hours': 5,
|
||||
'allocated_hours': 5,
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
|
|
@ -199,7 +76,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
"Parent task should have 2 children")
|
||||
|
||||
self.assertEqual(
|
||||
parent_task.subtask_planned_hours, 8,
|
||||
parent_task.subtask_allocated_hours, 8,
|
||||
"Planned hours of subtask should impact parent task")
|
||||
|
||||
# change the parent of a subtask without a project partner_id
|
||||
|
|
@ -222,7 +99,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
|
||||
# set a project with partner_id to a subtask without project partner_id
|
||||
child_task_1.write({
|
||||
'display_project_id': self.project_pigs.id
|
||||
'project_id': self.project_pigs.id
|
||||
})
|
||||
|
||||
self.assertNotEqual(
|
||||
|
|
@ -236,7 +113,7 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
|
||||
# set a project with partner_id to a subtask with a project partner_id
|
||||
child_task_2.write({
|
||||
'display_project_id': self.project_goats.id
|
||||
'project_id': self.project_goats.id
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
|
|
@ -359,11 +236,11 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
self.assertEqual(task.personal_stage_id.stage_id.name, stages[0].get('name'), "tasks assigned to the current user should be in the right default stage")
|
||||
|
||||
def test_send_rating_review(self):
|
||||
project_settings = self.env["res.config.settings"].create({'group_project_rating': True})
|
||||
project_settings.execute()
|
||||
self.assertTrue(self.project_goats.rating_active, 'The customer ratings should be enabled in this project.')
|
||||
|
||||
won_stage = self.project_goats.type_ids[-1]
|
||||
won_stage.write({
|
||||
'rating_active': True,
|
||||
'rating_status': 'stage',
|
||||
})
|
||||
rating_request_mail_template = self.env.ref('project.rating_project_request_email_template')
|
||||
won_stage.write({'rating_template_id': rating_request_mail_template.id})
|
||||
tasks = self.env['project.task'].with_context(mail_create_nolog=True, default_project_id=self.project_goats.id).create([
|
||||
|
|
@ -427,42 +304,173 @@ class TestProjectFlow(TestProjectCommon, MailCommon):
|
|||
self.assertEqual(len(project_A.message_ids), init_nb_log + 2,
|
||||
"should have 2 new messages: one for tracking, one for template")
|
||||
|
||||
def test_project_notify_get_recipients_groups(self):
|
||||
projects = self.env['project.project'].create([
|
||||
{
|
||||
'name': 'public project',
|
||||
'privacy_visibility': 'portal',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
{
|
||||
'name': 'internal project',
|
||||
'privacy_visibility': 'employees',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
{
|
||||
'name': 'private project',
|
||||
'privacy_visibility': 'followers',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
])
|
||||
for project in projects:
|
||||
groups = project._notify_get_recipients_groups()
|
||||
groups_per_key = {g[0]: g for g in groups}
|
||||
for key, group in groups_per_key.items():
|
||||
has_button_access = group[2]['has_button_access']
|
||||
if key in ['portal', 'portal_customer']:
|
||||
self.assertEqual(
|
||||
has_button_access,
|
||||
project.name == 'public project',
|
||||
"Only the public project should have its name clickable in the email sent to the customer when an email is sent via a email template set in the project stage for instance."
|
||||
)
|
||||
elif key == 'user':
|
||||
self.assertTrue(has_button_access)
|
||||
|
||||
def test_private_task_search_tag(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Test Private Task',
|
||||
})
|
||||
# Tag name_search should not raise Error if project_id is False
|
||||
task.tag_ids.with_context(project_id=task.project_id.id).name_search(
|
||||
args=["!", ["id", "in", []]])
|
||||
domain=["!", ["id", "in", []]])
|
||||
|
||||
def test_copy_project_with_default_name(self):
|
||||
""" Test the new project after the duplication got the exepected name
|
||||
|
||||
Test Cases:
|
||||
==========
|
||||
1. Duplicate a project
|
||||
2. Check the new project got the name of the project to copy plus `(copy)`
|
||||
3. Duplicate a project with default name
|
||||
4. Check the new project got the name defined in the default
|
||||
"""
|
||||
project = self.project_pigs.copy()
|
||||
self.assertEqual(project.name, 'Pigs (copy)', "The name of the copied project should be 'Pigs (copy)'")
|
||||
|
||||
project = self.project_pigs.copy({'name': 'Pigs 2'})
|
||||
self.assertEqual(project.name, 'Pigs 2', "The name of the copied project should be 'Pigs 2'")
|
||||
|
||||
def test_description_field_history_on_update(self):
|
||||
"""Test updating 'description' field in project task and checking history content at revision id."""
|
||||
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Test Task',
|
||||
'description': 'Hello',
|
||||
})
|
||||
task.description = False
|
||||
self.assertEqual(task.html_field_history_get_content_at_revision('description', 1), '<p>Hello</p>', "should recover previous text for description")
|
||||
|
||||
def test_copy_project_with_embedded_actions(self):
|
||||
project_pigs_milestone_action = self.env['ir.actions.act_window'].create({
|
||||
'name': 'Milestones',
|
||||
'res_model': 'project.milestone',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': f"[('project_id', '=', {self.project_pigs.id})]",
|
||||
})
|
||||
task_action = self.env['ir.actions.act_window'].create({
|
||||
'name': 'Tasks',
|
||||
'res_model': 'project.task',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': "[('project_id', '=', active_id), ('display_in_project', '=', True)]",
|
||||
'context': "{'default_project_id': active_id}",
|
||||
})
|
||||
task_embedded_action = self.env['ir.embedded.actions'].create({
|
||||
'parent_res_model': 'project.project',
|
||||
'parent_res_id': self.project_pigs.id,
|
||||
'action_id': project_pigs_milestone_action.id,
|
||||
'parent_action_id': task_action.id,
|
||||
})
|
||||
project_model = self.env['ir.model'].search([('model', '=', 'project.task')])
|
||||
task_embedded_filter = self.env['ir.filters'].create({
|
||||
'name': 'filter',
|
||||
'embedded_action_id': task_embedded_action.id,
|
||||
'embedded_parent_res_id': self.project_pigs.id,
|
||||
'action_id': project_pigs_milestone_action.id,
|
||||
'model_id': project_model.id,
|
||||
})
|
||||
|
||||
new_project_pigs = self.project_pigs.copy()
|
||||
embedded_action = self.env['ir.embedded.actions'].search([
|
||||
('parent_res_model', '=', 'project.project'),
|
||||
('parent_res_id', '=', new_project_pigs.id),
|
||||
])
|
||||
self.assertTrue(
|
||||
embedded_action,
|
||||
'The embedded action linked to project pigs should also be copied.',
|
||||
)
|
||||
self.assertEqual(
|
||||
embedded_action.action_id,
|
||||
task_embedded_action.action_id,
|
||||
"The new embedded action should have the same action than the one copied.",
|
||||
)
|
||||
self.assertEqual(
|
||||
embedded_action.parent_res_model,
|
||||
task_embedded_action.parent_res_model,
|
||||
)
|
||||
self.assertEqual(
|
||||
embedded_action.parent_action_id,
|
||||
task_embedded_action.parent_action_id,
|
||||
)
|
||||
duplicated_task_embedded_filter = embedded_action.filter_ids
|
||||
self.assertEqual(
|
||||
len(duplicated_task_embedded_filter),
|
||||
1,
|
||||
"The filter linked to the original embedded action should also be copied."
|
||||
)
|
||||
self.assertEqual(duplicated_task_embedded_filter.name, f"{task_embedded_filter.name} (copy)")
|
||||
self.assertEqual(duplicated_task_embedded_filter.embedded_action_id, embedded_action)
|
||||
self.assertEqual(duplicated_task_embedded_filter.embedded_parent_res_id, new_project_pigs.id)
|
||||
self.assertEqual(duplicated_task_embedded_filter.action_id, task_embedded_filter.action_id)
|
||||
self.assertEqual(duplicated_task_embedded_filter.model_id, task_embedded_filter.model_id)
|
||||
|
||||
def test_do_not_copy_project_stage(self):
|
||||
stage = self.env['project.project.stage'].create({'name': 'Custom stage'}) # Default sequence is 50
|
||||
self.project_pigs.stage_id = stage.id
|
||||
project_copy = self.project_pigs.with_context(default_stage_id=stage.id).copy()
|
||||
self.assertNotEqual(project_copy.stage_id, self.project_pigs.stage_id, 'Copied project should have lowest sequence stage')
|
||||
|
||||
def test_project_task_copy_without_archive_user(self):
|
||||
self.user_projectuser.action_archive()
|
||||
task = self.task_1.copy()
|
||||
self.assertFalse(task.user_ids)
|
||||
task_b = self.task_2.copy({
|
||||
'name': 'Task B',
|
||||
'user_ids': [Command.set([self.user_projectuser.id, self.user_projectmanager.id])],
|
||||
})
|
||||
self.assertEqual(self.user_projectuser + self.user_projectmanager, task_b.user_ids)
|
||||
|
||||
def test_project_sub_task_copy_without_archive_user(self):
|
||||
self.task_1.write({
|
||||
'child_ids': [self.task_2.id]
|
||||
})
|
||||
self.user_projectmanager.action_archive()
|
||||
task_1_copy = self.task_1.copy()
|
||||
self.assertFalse(task_1_copy.child_ids.user_ids)
|
||||
|
||||
def test_task_email_context_with_subtitles(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'user_ids': [Command.set([self.user_projectuser.id])],
|
||||
'project_id': self.project_goats.id,
|
||||
})
|
||||
self.assertFalse(self.project_goats.message_follower_ids)
|
||||
self.assertEqual(self.project_goats.privacy_visibility, 'followers')
|
||||
self.project_goats.invalidate_recordset()
|
||||
render_context = task.with_user(self.user_projectuser)._notify_by_email_prepare_rendering_context(task.message_ids, {})
|
||||
self.assertListEqual(render_context['subtitles'], ['Task', 'Project: Goats, Stage: New'])
|
||||
|
||||
def test_project_multi_tasks_copy_with_archive_user(self):
|
||||
"""
|
||||
Step 1: Create new an active project user
|
||||
Step 2: Create three tasks
|
||||
- Task 1: No users assigned.
|
||||
- Task 2: Assigned to two active users.
|
||||
- Task 3: Assigned to one active and one soon-to-be-archived user.
|
||||
Step 3: Archive one of the users
|
||||
Step 4: Copy all tasks
|
||||
Step 5: Validate expected user_ids on copied tasks
|
||||
- Task1 had no users → expect no users in the copied task.
|
||||
- Task2 had 2 active users → both should be preserved.
|
||||
- Task3 had one active + one archived user → only active (self.user_projectuser) should be preserved in the copy.
|
||||
"""
|
||||
|
||||
user_projectuser = self.user_projectuser.copy()
|
||||
|
||||
tasks = self.env['project.task'].create([{
|
||||
'name': 'Task1',
|
||||
'project_id': self.project_goats.id,
|
||||
}, {
|
||||
'name': 'Task2',
|
||||
'user_ids': [Command.set([self.user_projectuser.id, self.user_projectmanager.id])],
|
||||
'project_id': self.project_goats.id,
|
||||
}, {
|
||||
'name': 'Task3',
|
||||
'user_ids': [Command.set([self.user_projectuser.id, user_projectuser.id])],
|
||||
'project_id': self.project_goats.id,
|
||||
}])
|
||||
|
||||
user_projectuser.action_archive()
|
||||
|
||||
task1, task_2, task_3 = tasks.copy()
|
||||
|
||||
self.assertFalse(task1.user_ids)
|
||||
self.assertEqual(self.user_projectuser + self.user_projectmanager, task_2.user_ids)
|
||||
self.assertEqual(self.user_projectuser, task_3.user_ids)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,704 @@
|
|||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE
|
||||
from odoo.tests import tagged, users, new_test_user
|
||||
from odoo.tools import formataddr, mute_logger
|
||||
from odoo.fields import Command
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'mail_flow', 'mail_tools')
|
||||
class TestProjectMailFeatures(TestProjectCommon, MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# set high threshold to be sure to not hit mail limit during tests for a model
|
||||
cls.env['ir.config_parameter'].sudo().set_param('mail.gateway.loop.threshold', 50)
|
||||
|
||||
# be sure to test emails
|
||||
cls.user_employee.notification_type = 'email'
|
||||
cls.user_projectuser.notification_type = 'email'
|
||||
cls.user_projectmanager.notification_type = 'inbox'
|
||||
|
||||
# simple template used in auto acknowledgement
|
||||
cls.test_template = cls.env['mail.template'].create({
|
||||
'auto_delete': True,
|
||||
'body_html': '<p>Hello <t t-out="object.partner_id.name"/></p>',
|
||||
'lang': '{{ object.partner_id.lang or object.user_ids[:1].lang or user.lang }}',
|
||||
'model_id': cls.env['ir.model']._get_id('project.task'),
|
||||
'name': 'Test Acknowledge',
|
||||
'subject': 'Test Acknowledge {{ object.name }}',
|
||||
'use_default_to': True,
|
||||
})
|
||||
|
||||
# Test followers-based project
|
||||
cls.project_followers = cls.env['project.project'].create({
|
||||
'alias_name': 'help',
|
||||
'name': 'Goats',
|
||||
'partner_id': cls.partner_1.id,
|
||||
'privacy_visibility': 'followers',
|
||||
'type_ids': [
|
||||
(0, 0, {
|
||||
'mail_template_id': cls.test_template.id,
|
||||
'name': 'New',
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Validated',
|
||||
'sequence': 10,
|
||||
})],
|
||||
})
|
||||
cls.project_followers_alias = cls.project_followers.alias_id
|
||||
# add some project followers to check followers propagation notably
|
||||
cls.project_followers.message_subscribe(
|
||||
partner_ids=(cls.user_projectuser.partner_id + cls.user_projectmanager.partner_id).ids,
|
||||
# follow 'new tasks' to receive notification for incoming emails directly
|
||||
subtype_ids=(cls.env.ref('mail.mt_comment') + cls.env.ref('project.mt_project_task_new')).ids
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
with mute_logger('odoo.addons.mail.models.mail_thread'):
|
||||
self.test_task = self.format_and_process(
|
||||
MAIL_TEMPLATE, self.user_portal.email_formatted,
|
||||
self.project_followers_alias.alias_full_name,
|
||||
cc=self.partner_2.email_formatted,
|
||||
subject='Data Test Task',
|
||||
target_model='project.task',
|
||||
)
|
||||
self.flush_tracking()
|
||||
|
||||
def test_assert_initial_values(self):
|
||||
""" Check base values coherency for tests clarity """
|
||||
self.assertEqual(
|
||||
self.project_followers.message_partner_ids,
|
||||
self.user_projectuser.partner_id + self.user_projectmanager.partner_id)
|
||||
self.assertEqual(self.test_task.project_id, self.project_followers)
|
||||
|
||||
# check for partner creation, should not pre-exist
|
||||
self.assertFalse(self.env['res.partner'].search(
|
||||
[('email_normalized', 'in', {'new.cc@test.agrolait.com', 'new.customer@test.agrolait.com', 'new.author@test.agrolait.com'})])
|
||||
)
|
||||
|
||||
def test_project_notify_get_recipients_groups(self):
|
||||
projects = self.env['project.project'].create([
|
||||
{
|
||||
'name': 'public project',
|
||||
'privacy_visibility': 'portal',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
{
|
||||
'name': 'internal project',
|
||||
'privacy_visibility': 'employees',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
{
|
||||
'name': 'private project',
|
||||
'privacy_visibility': 'followers',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
])
|
||||
for project in projects:
|
||||
groups = project._notify_get_recipients_groups(self.env['mail.message'], False)
|
||||
groups_per_key = {g[0]: g for g in groups}
|
||||
for key, group in groups_per_key.items():
|
||||
has_button_access = group[2]['has_button_access']
|
||||
if key in ['portal', 'portal_customer']:
|
||||
self.assertEqual(
|
||||
has_button_access,
|
||||
project.name == 'public project',
|
||||
"Only the public project should have its name clickable in the email sent to the customer when an email is sent via a email template set in the project stage for instance."
|
||||
)
|
||||
elif key == 'user':
|
||||
self.assertTrue(has_button_access)
|
||||
|
||||
def test_task_creation_no_stage(self):
|
||||
""" Test receiving email in a project without stage, should create task as intended """
|
||||
internal_followers = self.user_projectuser.partner_id + self.user_projectmanager.partner_id
|
||||
self.project_followers.type_ids = [(5, 0)]
|
||||
|
||||
incoming_cc = f'"New Cc" <new.cc@test.agrolait.com>, {self.partner_2.email_formatted}'
|
||||
incoming_to = f'{self.project_followers_alias.alias_full_name}, {self.partner_1.email_formatted}, "New Customer" <new.customer@test.agrolait.com>'
|
||||
incoming_to_filtered = f'{self.partner_1.email_formatted}, "New Customer" <new.customer@test.agrolait.com>'
|
||||
with self.mock_mail_gateway():
|
||||
task = self.format_and_process(
|
||||
MAIL_TEMPLATE,
|
||||
self.user_portal.email_formatted,
|
||||
incoming_to,
|
||||
cc=incoming_cc,
|
||||
subject=f'Test from {self.user_portal.name}',
|
||||
target_model='project.task',
|
||||
)
|
||||
self.flush_tracking()
|
||||
self.assertEqual(task.project_id, self.project_followers)
|
||||
self.assertFalse(task.stage_id)
|
||||
|
||||
self.assertEqual(len(task.message_ids), 1)
|
||||
self.assertMailNotifications(
|
||||
task.message_ids,
|
||||
[
|
||||
{
|
||||
'content': 'Please call me as soon as possible',
|
||||
'message_type': 'email',
|
||||
'message_values': {
|
||||
'author_id': self.user_portal.partner_id,
|
||||
'email_from': self.user_portal.email_formatted,
|
||||
# coming from incoming email
|
||||
'incoming_email_cc': incoming_cc,
|
||||
'incoming_email_to': incoming_to_filtered,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# followers of 'new task' subtype (but not original To as they
|
||||
# already received the email)
|
||||
'notified_partner_ids': internal_followers,
|
||||
# deduced from 'To' and 'Cc' (recognized only)
|
||||
'partner_ids': self.partner_1 + self.partner_2,
|
||||
'parent_id': self.env['mail.message'],
|
||||
'reply_to': formataddr((
|
||||
self.user_portal.name,
|
||||
self.project_followers_alias.alias_full_name
|
||||
)),
|
||||
'subject': f'Test from {self.user_portal.name}',
|
||||
'subtype_id': self.env.ref('project.mt_task_new'),
|
||||
},
|
||||
'notif': [
|
||||
{'partner': self.user_projectmanager.partner_id, 'type': 'inbox',},
|
||||
{'partner': self.user_projectuser.partner_id, 'type': 'email',},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_task_creation_notifies_author(self):
|
||||
""" Check auto acknowledgment mail sent at new task. It should notify
|
||||
task creator, based on stage template. """
|
||||
internal_followers = self.user_projectuser.partner_id + self.user_projectmanager.partner_id
|
||||
new_partner_email = '"New Author" <new.author@test.agrolait.com>'
|
||||
|
||||
incoming_cc = f'"New Cc" <new.cc@test.agrolait.com>, {self.partner_2.email_formatted}'
|
||||
incoming_to = f'{self.project_followers_alias.alias_full_name}, {self.partner_1.email_formatted}, "New Customer" <new.customer@test.agrolait.com>'
|
||||
incoming_to_filtered = f'{self.partner_1.email_formatted}, "New Customer" <new.customer@test.agrolait.com>'
|
||||
for test_user in (self.user_employee, self.user_portal, False):
|
||||
with self.subTest(user_name=test_user.name if test_user else new_partner_email):
|
||||
email_from = test_user.email_formatted if test_user else new_partner_email
|
||||
with self.mock_mail_gateway():
|
||||
task = self.format_and_process(
|
||||
MAIL_TEMPLATE, email_from,
|
||||
incoming_to,
|
||||
cc=incoming_cc,
|
||||
subject=f'Test from {email_from}',
|
||||
target_model='project.task',
|
||||
)
|
||||
self.flush_tracking()
|
||||
|
||||
if test_user:
|
||||
author = test_user.partner_id
|
||||
else:
|
||||
author = self.env['res.partner'].search([('email_normalized', '=', 'new.author@test.agrolait.com')])
|
||||
self.assertTrue(author, 'Project automatically creates a partner for incoming email')
|
||||
self.assertEqual(author.email, 'new.author@test.agrolait.com', 'Should parse name/email correctly')
|
||||
self.assertEqual(author.name, 'New Author', 'Should parse name/email correctly')
|
||||
|
||||
# do not converts Cc into partners, used only to populate email_cc field
|
||||
new_partner_cc = self.env['res.partner'].search([('email_normalized', '=', 'new.cc@test.agrolait.com')])
|
||||
self.assertFalse(new_partner_cc)
|
||||
# do not convert other people in To, simply recognized if they exist
|
||||
new_partner_customer = self.env['res.partner'].search([('email_normalized', '=', 'new.customer@test.agrolait.com')])
|
||||
self.assertFalse(new_partner_customer)
|
||||
|
||||
self.assertIn('Please call me as soon as possible', task.description)
|
||||
self.assertEqual(task.email_cc, f'"New Cc" <new.cc@test.agrolait.com>, {self.partner_2.email_formatted}, {self.partner_1.email_formatted}, "New Customer" <new.customer@test.agrolait.com>')
|
||||
self.assertEqual(task.name, f'Test from {author.email_formatted}')
|
||||
self.assertEqual(task.partner_id, author)
|
||||
self.assertEqual(task.project_id, self.project_followers)
|
||||
self.assertEqual(task.stage_id, self.project_followers.type_ids[0])
|
||||
# followers: email cc is added in followers at creation time, aka only recognized partners
|
||||
self.assertEqual(task.message_partner_ids, internal_followers + author + self.partner_1 + self.partner_2)
|
||||
# messages
|
||||
self.assertEqual(len(task.message_ids), 2)
|
||||
# first message: incoming email: sent to email followers
|
||||
incoming_email = task.message_ids[1]
|
||||
self.assertMailNotifications(
|
||||
incoming_email,
|
||||
[
|
||||
{
|
||||
'content': 'Please call me as soon as possible',
|
||||
'message_type': 'email',
|
||||
'message_values': {
|
||||
'author_id': author,
|
||||
'email_from': formataddr((author.name, author.email_normalized)),
|
||||
# coming from incoming email
|
||||
'incoming_email_cc': incoming_cc,
|
||||
'incoming_email_to': incoming_to_filtered,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# followers of 'new task' subtype (but not original To as they
|
||||
# already received the email)
|
||||
'notified_partner_ids': internal_followers,
|
||||
# deduced from 'To' and 'Cc' (recognized partners)
|
||||
'partner_ids': self.partner_1 + self.partner_2,
|
||||
'parent_id': self.env['mail.message'],
|
||||
'reply_to': formataddr((author.name, self.project_followers_alias.alias_full_name)),
|
||||
'subject': f'Test from {author.email_formatted}',
|
||||
'subtype_id': self.env.ref('project.mt_task_new'),
|
||||
},
|
||||
'notif': [
|
||||
{'partner': self.user_projectmanager.partner_id, 'type': 'inbox',},
|
||||
{'partner': self.user_projectuser.partner_id, 'type': 'email',},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# second message: acknowledgment: sent to email author
|
||||
acknowledgement = task.message_ids[0]
|
||||
# task created by odoobot if not incoming user -> odoobot author of ack email
|
||||
acknowledgement_author = test_user.partner_id if test_user else self.partner_root
|
||||
self.assertMailNotifications(
|
||||
acknowledgement,
|
||||
[
|
||||
{
|
||||
'content': f'Hello {author.name}',
|
||||
'message_type': 'auto_comment',
|
||||
'message_values': {
|
||||
'author_id': acknowledgement_author,
|
||||
'email_from': acknowledgement_author.email_formatted,
|
||||
'incoming_email_cc': False,
|
||||
'incoming_email_to': False,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# default recipients: partner_id, no note followers
|
||||
'notified_partner_ids': author,
|
||||
# default recipients: partner_id
|
||||
'partner_ids': author,
|
||||
'parent_id': incoming_email,
|
||||
'reply_to': formataddr((acknowledgement_author.name, self.project_followers_alias.alias_full_name)),
|
||||
'subject': f'Test Acknowledge {task.name}',
|
||||
# defined by _track_template
|
||||
'subtype_id': self.env.ref('mail.mt_note'),
|
||||
},
|
||||
'notif': [
|
||||
# specific email for portal customer, due to portal mixin
|
||||
{'partner': author, 'type': 'email', 'group': 'portal_customer',},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# uses Chatter: fetches suggested recipients, post a message
|
||||
# - checks all suggested: email_cc field, primary email
|
||||
# ------------------------------------------------------------
|
||||
suggested_all = task.with_user(self.user_projectuser)._message_get_suggested_recipients(
|
||||
reply_discussion=True, no_create=False,
|
||||
)
|
||||
new_partner_cc = self.env['res.partner'].search(
|
||||
[('email_normalized', '=', 'new.cc@test.agrolait.com')]
|
||||
)
|
||||
self.assertEqual(new_partner_cc.email, 'new.cc@test.agrolait.com')
|
||||
self.assertEqual(new_partner_cc.name, 'New Cc')
|
||||
new_partner_customer = self.env['res.partner'].search(
|
||||
[('email_normalized', '=', 'new.customer@test.agrolait.com')]
|
||||
)
|
||||
self.assertEqual(new_partner_customer.email, 'new.customer@test.agrolait.com')
|
||||
self.assertEqual(new_partner_customer.name, 'New Customer')
|
||||
expected_all = []
|
||||
if not test_user:
|
||||
expected_all = [
|
||||
{ # last message recipient is proposed
|
||||
'create_values': {},
|
||||
'email': 'new.author@test.agrolait.com',
|
||||
'name': 'New Author',
|
||||
'partner_id': author.id, # already created by project upon initial email reception
|
||||
}
|
||||
]
|
||||
elif test_user == self.user_portal:
|
||||
expected_all = [
|
||||
{ # customer is proposed, even if follower, because shared
|
||||
'create_values': {},
|
||||
'email': self.user_portal.email_normalized,
|
||||
'name': self.user_portal.name,
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
}
|
||||
]
|
||||
expected_all += [
|
||||
{ # mail.thread.cc: email_cc field
|
||||
'create_values': {},
|
||||
'email': 'new.cc@test.agrolait.com',
|
||||
'name': 'New Cc',
|
||||
'partner_id': new_partner_cc.id,
|
||||
},
|
||||
{ # incoming email other recipients (new.customer)
|
||||
'create_values': {},
|
||||
'email': 'new.customer@test.agrolait.com',
|
||||
'name': 'New Customer',
|
||||
'partner_id': new_partner_customer.id,
|
||||
},
|
||||
# other CC (partner_2) and customer (partner_id) already follower
|
||||
]
|
||||
for suggested, expected in zip(suggested_all, expected_all, strict=True):
|
||||
self.assertDictEqual(suggested, expected)
|
||||
|
||||
# finally post the message with recipients
|
||||
with self.mock_mail_gateway():
|
||||
recipients = new_partner_cc + new_partner_customer
|
||||
if not test_user:
|
||||
recipients += author
|
||||
responsible_answer = task.with_user(self.user_projectuser).message_post(
|
||||
body='<p>Well received !',
|
||||
partner_ids=recipients.ids,
|
||||
message_type='comment',
|
||||
subject=f'Re: {task.name}',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
self.assertEqual(task.message_partner_ids, internal_followers + author + self.partner_1 + self.partner_2)
|
||||
|
||||
external_partners = self.partner_1 + self.partner_2 + new_partner_cc + new_partner_customer
|
||||
self.assertMailNotifications(
|
||||
responsible_answer,
|
||||
[
|
||||
{
|
||||
'content': 'Well received !',
|
||||
'mail_mail_values': {
|
||||
'mail_server_id': self.env['ir.mail_server'], # no specified server
|
||||
},
|
||||
'message_type': 'comment',
|
||||
'message_values': {
|
||||
'author_id': self.user_projectuser.partner_id,
|
||||
'email_from': self.user_projectuser.partner_id.email_formatted,
|
||||
'incoming_email_cc': False,
|
||||
'incoming_email_to': False,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# projectuser not notified of its own message, even if follower
|
||||
'notified_partner_ids': self.user_projectmanager.partner_id + author + external_partners,
|
||||
'parent_id': incoming_email,
|
||||
# coming from post
|
||||
'partner_ids': recipients,
|
||||
'reply_to': formataddr((self.user_projectuser.name, self.project_followers_alias.alias_full_name)),
|
||||
'subject': f'Re: {task.name}',
|
||||
'subtype_id': self.env.ref('mail.mt_comment'),
|
||||
},
|
||||
'notif': [
|
||||
# original author has a specific email with links and tokens
|
||||
{'partner': author, 'type': 'email', 'group': 'portal_customer'},
|
||||
{'partner': self.partner_1, 'type': 'email'},
|
||||
{'partner': self.partner_2, 'type': 'email'},
|
||||
{'partner': new_partner_cc, 'type': 'email'},
|
||||
{'partner': new_partner_customer, 'type': 'email'},
|
||||
{'partner': self.user_projectmanager.partner_id, 'type': 'inbox'},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# SMTP emails really sent (not Inbox guy then)
|
||||
# expected Msg['To'] : Reply-All behavior: actual recipient, then
|
||||
# all "not internal partners" and catchall (to receive answers)
|
||||
for partner in (responsible_answer.notified_partner_ids - self.user_projectmanager.partner_id):
|
||||
exp_msg_to_partners = partner | external_partners
|
||||
if author != self.user_employee.partner_id: # external only !
|
||||
exp_msg_to_partners |= author
|
||||
exp_msg_to = exp_msg_to_partners.mapped('email_formatted')
|
||||
with self.subTest(name=partner.name):
|
||||
self.assertSMTPEmailsSent(
|
||||
mail_server=self.mail_server_notification,
|
||||
msg_from=formataddr((self.user_projectuser.name, f'{self.default_from}@{self.alias_domain}')),
|
||||
smtp_from=self.mail_server_notification.from_filter,
|
||||
smtp_to_list=[partner.email_normalized],
|
||||
msg_to_lst=exp_msg_to,
|
||||
)
|
||||
|
||||
# customer replies using "Reply All" + adds new people
|
||||
# ------------------------------------------------------------
|
||||
self.gateway_mail_reply_from_smtp_email(
|
||||
MAIL_TEMPLATE, [author.email_normalized], reply_all=True,
|
||||
cc=f'"Another Cc" <another.cc@test.agrolait.com>, {self.partner_3.email}',
|
||||
target_model='project.task',
|
||||
)
|
||||
self.assertEqual(
|
||||
task.email_cc,
|
||||
'"Another Cc" <another.cc@test.agrolait.com>, valid.poilboeuf@gmail.com, "New Cc" <new.cc@test.agrolait.com>, '
|
||||
'"Valid Poilvache" <valid.other@gmail.com>, "Valid Lelitre" <valid.lelitre@agrolait.com>, "New Customer" <new.customer@test.agrolait.com>',
|
||||
'Updated with new Cc')
|
||||
self.assertEqual(len(task.message_ids), 4, 'Incoming email + acknowledgement + chatter reply + customer reply')
|
||||
self.assertEqual(
|
||||
task.message_partner_ids,
|
||||
internal_followers + author + self.partner_1 + self.partner_2 + self.partner_3 + new_partner_cc + new_partner_customer,
|
||||
'Project adds recognized recipients as followers')
|
||||
|
||||
self.assertMailNotifications(
|
||||
task.message_ids[0],
|
||||
[
|
||||
{
|
||||
'content': 'Please call me as soon as possible',
|
||||
'message_type': 'email',
|
||||
'message_values': {
|
||||
'author_id': author,
|
||||
'email_from': author.email_formatted,
|
||||
# coming from incoming email
|
||||
'incoming_email_cc': f'"Another Cc" <another.cc@test.agrolait.com>, {self.partner_3.email}',
|
||||
# To: received email Msg-To - customer who replies, without email Reply-To
|
||||
'incoming_email_to': ', '.join(external_partners.mapped('email_formatted')),
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# notified: followers - already emailed, aka internal only
|
||||
'notified_partner_ids': internal_followers,
|
||||
'parent_id': responsible_answer,
|
||||
# same reasoning as email_to/cc
|
||||
'partner_ids': external_partners + self.partner_3,
|
||||
'reply_to': formataddr((author.name, self.project_followers_alias.alias_full_name)),
|
||||
'subject': f'Re: Re: {task.name}',
|
||||
'subtype_id': self.env.ref('mail.mt_comment'),
|
||||
},
|
||||
'notif': [
|
||||
{'partner': self.user_projectuser.partner_id, 'type': 'email',},
|
||||
{'partner': self.user_projectmanager.partner_id, 'type': 'inbox',},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# clear for other loops
|
||||
(new_partner_cc + new_partner_customer).unlink()
|
||||
|
||||
@users('bastien')
|
||||
def test_task_notification_on_project_update(self):
|
||||
""" Test changing task's project notifies people following 'New Task' """
|
||||
test_task = self.test_task.with_user(self.env.user)
|
||||
with self.mock_mail_gateway():
|
||||
test_task.project_id = False
|
||||
self.flush_tracking()
|
||||
# voiding project should not do anything
|
||||
self.assertNotSentEmail()
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
test_task.project_id = self.project_goats.id
|
||||
self.flush_tracking()
|
||||
self.assertNotSentEmail()
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
test_task.project_id = self.project_followers.id
|
||||
self.flush_tracking()
|
||||
|
||||
# find notification, not in message_ids as it is a personal message
|
||||
notification_msg = self.env['mail.message'].search([
|
||||
('model', '=', 'project.task'), ('res_id', '=', test_task.id),
|
||||
('body', 'ilike', 'Transferred from Project')
|
||||
])
|
||||
self.assertTrue(notification_msg)
|
||||
|
||||
# should trigger a notification
|
||||
self.assertSentEmail(self.env.user.email_formatted, [self.user_projectuser.email_formatted])
|
||||
|
||||
self.assertMailNotifications(
|
||||
notification_msg,
|
||||
[
|
||||
{
|
||||
'content': 'Transferred from Project',
|
||||
'message_type': 'user_notification',
|
||||
'message_values': {
|
||||
'author_id': self.user_projectmanager.partner_id,
|
||||
'email_from': self.user_projectmanager.partner_id.email_formatted,
|
||||
'mail_server_id': self.env['ir.mail_server'],
|
||||
# followers of 'new task' type but not author itself
|
||||
'notified_partner_ids': self.user_projectuser.partner_id,
|
||||
# followers of 'new task' type
|
||||
'partner_ids': (self.user_projectuser + self.user_projectmanager).partner_id,
|
||||
'parent_id': self.env['mail.message'],
|
||||
'reply_to': formataddr((self.user_projectmanager.name, self.project_followers_alias.alias_full_name
|
||||
)),
|
||||
'subject': test_task.name,
|
||||
'subtype_id': self.env.ref('mail.mt_note'),
|
||||
},
|
||||
'notif': [
|
||||
{'partner': self.user_projectuser.partner_id, 'type': 'email',},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_task_notification_on_user_ids_update(self):
|
||||
""" This test will check that an assignment mail is sent when adding an assignee to a task """
|
||||
# avoid messing with followers to ease notif check
|
||||
self.project_followers.message_unsubscribe(partner_ids=self.project_followers.message_partner_ids.ids)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
test_task = self.env['project.task'].create({
|
||||
'name': 'Mail Task',
|
||||
'user_ids': self.user_projectuser,
|
||||
'project_id': self.project_followers.id
|
||||
})
|
||||
self.flush_tracking()
|
||||
self.assertSentEmail(self.env.user.email_formatted, [self.user_projectuser.email_formatted])
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
test_task.copy()
|
||||
self.flush_tracking()
|
||||
# check that no mail was received for the assignee of the task
|
||||
self.assertNotSentEmail(self.user_projectuser.email_formatted)
|
||||
|
||||
def test_copy_task_logs_chatter(self):
|
||||
"""Test that copying a task logs a message in the chatter."""
|
||||
copied_task = self.task_1.copy()
|
||||
|
||||
# Ensure only one message is logged in chatter
|
||||
self.assertEqual(
|
||||
'Task Created', copied_task.message_ids[0].preview,
|
||||
"Expected 'Task Created' message not found in copied task's chatter."
|
||||
)
|
||||
|
||||
def test_task_portal_share_adds_followers(self):
|
||||
""" Test that sharing a task through the portal share wizard adds recipients as followers.
|
||||
|
||||
Test Cases:
|
||||
===========
|
||||
1) Verify that the portal user is not a follower of the task.
|
||||
2) Create and execute a portal share wizard to share the task with the portal user.
|
||||
3) Verify that the portal user has been added as a follower after sharing.
|
||||
"""
|
||||
|
||||
self.assertNotIn(self.user_portal.partner_id, self.task_1.message_partner_ids,
|
||||
"Portal user's partner should not be a follower initially")
|
||||
|
||||
share_wizard = self.env['portal.share'].create({
|
||||
'res_model': 'project.task',
|
||||
'res_id': self.task_1.id,
|
||||
'partner_ids': [Command.set(self.user_portal.partner_id.ids)]
|
||||
})
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
share_wizard.action_send_mail()
|
||||
|
||||
self.assertIn(self.user_portal.partner_id, self.task_1.message_partner_ids,
|
||||
"Portal user's partner should be added as a follower after sharing")
|
||||
|
||||
def test_mail_alais_assignees_from_recipient_list(self):
|
||||
# including all types of users in recipient list
|
||||
new_user = new_test_user(self.env, 'int_user')
|
||||
|
||||
# format: Name <some@email.com>
|
||||
incoming_to_emails_with_name = (
|
||||
f"\"{self.project_goats.name}\" <{self.project_goats.alias_name}@{self.project_goats.alias_domain_id.name}>"
|
||||
f"\"{self.user_public.name}\" <{self.user_public.email}>,"
|
||||
f"\"{self.user_projectmanager.name}\" <{self.user_projectmanager.email}>,"
|
||||
f"\"{self.user_portal.name}\" <{self.user_portal.email}>,"
|
||||
f"\"{self.user_projectuser.name}\" <{self.user_projectuser.email}>,"
|
||||
)
|
||||
# format: some@email.com
|
||||
incoming_to_emails = (
|
||||
f"{self.project_goats.alias_name}@{self.project_goats.alias_domain_id.name},"
|
||||
f"{self.user_public.email},"
|
||||
f"{self.user_projectmanager.email},"
|
||||
f"{self.user_portal.email},"
|
||||
f"{self.user_projectuser.email},"
|
||||
)
|
||||
|
||||
for incoming_to in [incoming_to_emails_with_name, incoming_to_emails]:
|
||||
with self.mock_mail_gateway():
|
||||
task = self.format_and_process(
|
||||
MAIL_TEMPLATE,
|
||||
self.user_employee.email,
|
||||
incoming_to,
|
||||
cc=f"{new_user.email}",
|
||||
subject=f'Test task assignees from email to address with {incoming_to}',
|
||||
target_model='project.task',
|
||||
)
|
||||
self.flush_tracking()
|
||||
self.assertTrue(task, "Task has not been created from a incoming email")
|
||||
# only internal users are set as asssignees
|
||||
self.assertEqual(task.user_ids, self.user_projectmanager + self.user_projectuser, "Assignees have not been set from the to address of the mail")
|
||||
# public and portal users are ignored
|
||||
self.assertNotIn(task.user_ids, self.user_public + self.user_portal, "Assignees should not be set for user other than internal users")
|
||||
# sender should not be added as user in the task
|
||||
self.assertNotIn(task.user_ids, self.user_employee, "Sender can never be in assignees")
|
||||
# internal users in cc of mail shoudl be added in email_cc field
|
||||
self.assertEqual(task.email_cc, new_user.email, "The internal user in CC is not added into email_cc field")
|
||||
|
||||
def test_task_creation_removes_email_signatures(self):
|
||||
"""
|
||||
Tests that email signature is correctly removed from a task
|
||||
description when a task is created from an email alias.
|
||||
"""
|
||||
|
||||
gmail_email_source = f"""From: "{self.user_portal.name}" <{self.user_portal.email_formatted}>
|
||||
To: {self.project_followers_alias.alias_full_name}
|
||||
Subject: Test Gmail Signature Removal
|
||||
Content-Type: text/html;
|
||||
|
||||
<p>This is the main email content that should be kept.</p>
|
||||
<p>Some more important content here.</p>
|
||||
<span>--</span>
|
||||
<div data-smartmail="gmail_signature">
|
||||
<p>John Doe</p>
|
||||
<p>Software Engineer</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
outlook_email_source = f"""From: "{self.user_portal.name}" <{self.user_portal.email_formatted}>
|
||||
To: {self.project_followers_alias.alias_full_name}
|
||||
Subject: Test Outlook Signature Removal
|
||||
Content-Type: text/html;
|
||||
|
||||
<p>This is the main email content that should be kept.</p>
|
||||
<p>Some more important content here.</p>
|
||||
<div id="Signature">
|
||||
<p>John Smith</p>
|
||||
<p>Software Developer</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
gmail_task_id = self.env['mail.thread'].message_process(
|
||||
model='project.task',
|
||||
message=gmail_email_source,
|
||||
custom_values={'project_id': self.project_followers.id}
|
||||
)
|
||||
outlook_task_id = self.env['mail.thread'].message_process(
|
||||
model='project.task',
|
||||
message=outlook_email_source,
|
||||
custom_values={'project_id': self.project_followers.id}
|
||||
)
|
||||
|
||||
# Verify Gmail signature removal
|
||||
self.assertTrue(gmail_task_id, "Gmail task creation should return a valid ID.")
|
||||
gmail_task = self.env['project.task'].browse(gmail_task_id)
|
||||
|
||||
self.assertIn("This is the main email content that should be kept", gmail_task.description)
|
||||
self.assertNotIn("--", gmail_task.description, "The Gmail signature separator should have been removed.")
|
||||
self.assertNotIn("John Doe", gmail_task.description, "The Gmail signature should have been removed.")
|
||||
self.assertNotIn("Software Engineer", gmail_task.description, "The Gmail signature should have been removed.")
|
||||
|
||||
# Verify Outlook signature removal
|
||||
self.assertTrue(outlook_task_id, "Outlook task creation should return a valid ID.")
|
||||
outlook_task = self.env['project.task'].browse(outlook_task_id)
|
||||
|
||||
self.assertIn("This is the main email content that should be kept", outlook_task.description)
|
||||
self.assertNotIn("John Smith", outlook_task.description, "The Outlook signature should have been removed.")
|
||||
self.assertNotIn("Software Developer", outlook_task.description, "The Outlook signature should have been removed.")
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread')
|
||||
def test_task_creation_from_mail(self):
|
||||
""" This test checks a `default_` key passed in the context with an invalid field doesn't prevent the task
|
||||
creation.
|
||||
|
||||
This is related to the `_ensure_fields_write` method checking field write access rights
|
||||
for collaborator portals
|
||||
"""
|
||||
server = self.env['fetchmail.server'].create({
|
||||
'name': 'Test server',
|
||||
'user': 'test@example.com',
|
||||
'password': '',
|
||||
})
|
||||
task_id = self.env["mail.thread"].with_context(
|
||||
default_fetchmail_server_id=server.id
|
||||
).message_process(
|
||||
server.object_id.model,
|
||||
self.format(
|
||||
MAIL_TEMPLATE,
|
||||
email_from="chell@gladys.portal",
|
||||
to=f"project+pigs@{self.alias_domain}",
|
||||
subject="In a cage",
|
||||
msg_id="<on.antibiotics@example.com>",
|
||||
),
|
||||
save_original=server.original,
|
||||
strip_attachments=not server.attach,
|
||||
)
|
||||
task = self.env['project.task'].browse(task_id)
|
||||
self.assertEqual(task.name, "In a cage")
|
||||
self.assertEqual(task.project_id, self.project_pigs)
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import Form, tagged
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
|
@ -11,45 +13,89 @@ class TestProjectMilestone(TestProjectCommon):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.milestone = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Milestone',
|
||||
cls.env.user.group_ids |= cls.env.ref('project.group_project_milestone')
|
||||
cls.milestone_pigs, cls.milestone_goats = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create([{
|
||||
'name': 'Milestone Pigs',
|
||||
'project_id': cls.project_pigs.id,
|
||||
})
|
||||
}, {
|
||||
'name': 'Milestone Goats',
|
||||
'project_id': cls.project_goats.id,
|
||||
}])
|
||||
|
||||
def test_milestones_settings_change(self):
|
||||
# To be sure the feature is disabled globally to begin the test.
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
self.assertFalse(self.env.user.has_group('project.group_project_milestone'), 'The "Milestones" feature should not be globally enabled by default.')
|
||||
self.assertFalse(self.project_pigs.allow_milestones, 'The "Milestones" feature should not be enabled by default.')
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
self.assertTrue(self.env.user.has_group('project.group_project_milestone'), 'The "Milestones" feature should globally be enabled.')
|
||||
self.assertTrue(self.project_pigs.allow_milestones, 'The "Milestones" feature should be enabled by default on the project when the feature is enabled.')
|
||||
project = self.env['project.project'].create({'name': 'Test allow_milestones on New Project'})
|
||||
self.assertTrue(project.allow_milestones, 'The "Milestones" feature should be enabled by default when the feature is enabled globally.')
|
||||
|
||||
self.env.user.group_ids -= self.env.ref('project.group_project_milestone')
|
||||
project1 = self.env['project.project'].create({'name': 'Test allow_milestones on New Project'})
|
||||
self.assertFalse(project1.allow_milestones, 'The "Milestones" feature should be disabled by default when the feature is disabled globally.')
|
||||
with Form(self.env['project.project']) as project_form:
|
||||
project_form.name = 'My Mouses Project'
|
||||
self.assertTrue(project_form.allow_milestones, 'New projects allow_milestones should be True by default.')
|
||||
self.assertFalse(project_form.allow_milestones, 'New projects allow_milestones should be False by default.')
|
||||
|
||||
# Now, enable the feature
|
||||
self.env.user.group_ids |= self.env.ref('project.group_project_milestone')
|
||||
project2 = self.env['project.project'].create({'name': 'Test allow_milestones on New Project'})
|
||||
self.assertFalse(project2.allow_milestones, 'The "Milestones" feature should still be disabled by default when the feature is enabled globally.')
|
||||
with Form(self.env['project.project']) as project_form:
|
||||
project_form.name = 'My Mouses Project'
|
||||
self.assertFalse(project_form.allow_milestones, 'New projects allow_milestones should be False by default.')
|
||||
|
||||
def test_change_project_in_task(self):
|
||||
""" Test when a task is linked to a milestone and when we change its project the milestone is removed
|
||||
""" Test when a task is linked to a milestone and when we change its project the milestone is removed (and
|
||||
we fallback on the parent milestone if it belongs to the same project)
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Set a milestone on the task
|
||||
2) Change the project of that task
|
||||
3) Check no milestone is linked to the task
|
||||
3) Check no milestone is linked to the task (or the one of its parent is used if relevant)
|
||||
"""
|
||||
self.task_1.milestone_id = self.milestone
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone)
|
||||
# A. No parent task
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs)
|
||||
|
||||
|
||||
self.task_1.project_id = self.project_goats
|
||||
self.assertFalse(self.task_1.milestone_id, 'No milestone should be linked to the task since its project has changed')
|
||||
|
||||
# B. Parent task with no milestone set
|
||||
task_2 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Child MilestoneTask',
|
||||
'user_ids': self.user_projectmanager,
|
||||
'project_id': self.project_pigs.id,
|
||||
'parent_id': self.task_1.id,
|
||||
'milestone_id': self.milestone_pigs.id,
|
||||
})
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
|
||||
task_2.project_id = self.project_goats
|
||||
self.assertFalse(task_2.milestone_id, 'No milestone should be linked to the task since its project has changed and its parent task has no milestone')
|
||||
|
||||
# C. Parent task with a milestone set but on a different project
|
||||
self.task_1.project_id = self.project_pigs
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
task_2.project_id = self.project_pigs
|
||||
task_2.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
|
||||
task_2.project_id = self.project_goats
|
||||
self.assertFalse(task_2.milestone_id, 'No milestone should be linked to the task since its project has changed and its parent task belongs to another project')
|
||||
|
||||
# D. Parent task with a milestone set on the same project
|
||||
self.task_1.project_id = self.project_goats
|
||||
self.task_1.milestone_id = self.milestone_goats
|
||||
task_2.project_id = self.project_pigs
|
||||
task_2.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
task_2.project_id = self.project_goats
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_goats,
|
||||
'The milestone of the task should be replaced by the one of its parent task as they now belong to the same project')
|
||||
|
||||
# E. No milestone for private task
|
||||
task_2.parent_id = False
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_goats)
|
||||
task_2.project_id = False
|
||||
self.assertFalse(task_2.milestone_id, 'No milestone should be linked to a private task')
|
||||
|
||||
def test_duplicate_project_duplicates_milestones_on_tasks(self):
|
||||
"""
|
||||
Test when we duplicate the project with tasks linked to its' milestones,
|
||||
|
|
@ -96,4 +142,228 @@ class TestProjectMilestone(TestProjectCommon):
|
|||
self.assertNotEqual(copied_task.milestone_id, False,
|
||||
"We should copy the milestone and it shouldn't be reset to false from _compute_milestone_id")
|
||||
self.assertEqual(orig_task.milestone_id.name, copied_task.milestone_id.name,
|
||||
"the copied milestone should be a copy if the original ")
|
||||
"the copied milestone should be a copy of the original ")
|
||||
|
||||
def test_duplicate_project_with_milestones_disabled(self):
|
||||
""" This test ensures that when a project that has some milestones linked to it but with the milesones feature disabled is copied,
|
||||
the copy does not have the feature enabled, and none of the milestones have been copied."""
|
||||
extra_milestone_pigs = self.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Extra Milestone',
|
||||
'project_id': self.project_pigs.id,
|
||||
})
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.task_2.milestone_id = extra_milestone_pigs
|
||||
self.project_pigs.allow_milestones = False
|
||||
project_copied = self.project_pigs.copy()
|
||||
project_copied.task_ids._compute_milestone_id()
|
||||
self.assertFalse(project_copied.allow_milestones, "The project copied should have the milestone feature disabled")
|
||||
self.assertFalse(project_copied.milestone_ids, "The project copied should not have any milestone")
|
||||
self.assertFalse(project_copied.task_ids.milestone_id, "None of the project's task should have a milestone")
|
||||
|
||||
def test_basic_milestone_write(self):
|
||||
""" Testing basic milestone/project write operation on task, i.e:
|
||||
1. Set/change the milestone of a task
|
||||
2. Change the milestone/project of a task simultaneously
|
||||
3. Set/change to an invalid milestone
|
||||
"""
|
||||
extra_milestone_pigs = self.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Extra Milestone',
|
||||
'project_id': self.project_pigs.id,
|
||||
})
|
||||
|
||||
# 1. Set/change the milestone of a task
|
||||
self.task_1.project_id = self.project_pigs
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs)
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs,
|
||||
"Assignation of a valid milestone to a task with no milestone is not working properly.")
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs,
|
||||
"Change of the milestone of a task to a milestone from the same project is not working properly.")
|
||||
|
||||
# 2. Change the milestone/project of a task simultaneously
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'milestone_id': self.milestone_goats.id,
|
||||
'project_id': self.project_goats.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_goats,
|
||||
"Changing the project of a task and its milestone simultaneously is not working properly.")
|
||||
self.assertEqual(self.task_1.project_id, self.project_goats,
|
||||
"Changing the project of a task and its milestone simultaneously is not working properly.")
|
||||
|
||||
# 3. Set/change to an invalid milestone
|
||||
self.assertEqual(self.task_1.project_id, self.project_goats)
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_goats)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertFalse(self.task_1.milestone_id,
|
||||
"Setting the milestone of a task to an invalid value should reset the value of milestone_id.")
|
||||
|
||||
|
||||
def test_set_milestone_parent_task(self):
|
||||
""" When a milestone is set on a parent task, it is set as well on its child tasks if they have no milestone set yet and
|
||||
if they belong to the same project (or they have no project set).
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Set a milestone on the task (or not)
|
||||
2) Change the milestone of its parent
|
||||
3) Check the result
|
||||
"""
|
||||
# A. Child task with no milestone set and belonging to the same project
|
||||
task_2, task_3 = self.env['project.task'].with_context({'mail_create_nolog': True}).create([{
|
||||
'name': 'Child MilestoneTask',
|
||||
'user_ids': self.user_projectmanager,
|
||||
'project_id': self.project_pigs.id,
|
||||
'parent_id': self.task_1.id,
|
||||
}, {
|
||||
'name': 'Grand-child MilestoneTask',
|
||||
'user_ids': self.user_projectmanager,
|
||||
'project_id': self.project_pigs.id,
|
||||
}])
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
self.assertFalse(task_2.milestone_id)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs,
|
||||
"The milestone of the parent task should be set to its subtasks if they belong to the same project (or the subtask has not project set) and the subtask has no milestone already set.")
|
||||
|
||||
# B. Child task with a milestone already set
|
||||
extra_milestone_pigs = self.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Extra Milestone Pigs',
|
||||
'project_id': self.project_pigs.id,
|
||||
})
|
||||
self.task_1.milestone_id = False
|
||||
task_2.milestone_id = extra_milestone_pigs
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs, "The milestone of the child task should not be modified has it has already one set.")
|
||||
|
||||
# C. Child task with no milestone set but belonging to another project
|
||||
task_2.project_id = self.project_goats
|
||||
self.assertFalse(task_2.milestone_id)
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertFalse(task_2.milestone_id, "The milestone of the parent task should not be set to its child task as they belong to different projects.")
|
||||
|
||||
# D. Recursion test (grand-parent task's milestone set to grand-child task)
|
||||
task_3.parent_id = task_2
|
||||
self.task_1.project_id = task_2.project_id = task_3.project_id = self.project_pigs
|
||||
self.task_1.milestone_id = task_2.milestone_id = task_3.milestone_id = False
|
||||
self.assertFalse(task_2.milestone_id)
|
||||
self.assertFalse(task_3.milestone_id)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_3.milestone_id, self.milestone_pigs, "The milestone of the parent task should be set to its (grand)child tasks recursively.")
|
||||
|
||||
# E. Recursion test 2 (grand-child task has no milestone but first level child does)
|
||||
self.task_1.milestone_id = False
|
||||
task_2.milestone_id = self.milestone_pigs
|
||||
task_3.milestone_id = False
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
self.assertFalse(task_3.milestone_id)
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
self.assertFalse(task_3.milestone_id,
|
||||
"The milestone of the parent task should be set to its (grand)child tasks recursively. If a child task milestone should not be updated, it stops the recursion.")
|
||||
|
||||
# F. Update of the parent's milestone, trigger the update of the subtask's milestone if they were the same before change
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs)
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs,
|
||||
"If parent and child tasks share the same milestone, the update of the parent's milestone should trigger the update of its child's milestone.")
|
||||
|
||||
# G. Same as F but project and milestone of the parent task are changed at the same time -> The milestone of the child change as it will follow its parent in the other project
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'project_id': self.project_goats.id,
|
||||
'milestone_id': self.milestone_goats.id,
|
||||
})
|
||||
self.assertTrue(
|
||||
task_2.milestone_id == self.task_1.milestone_id == self.milestone_goats,
|
||||
"The child milestone should be updated if the parent task's project is changed.",
|
||||
)
|
||||
|
||||
# H. Same as G but project the project writen value is the same as the previous one -> No actual change of project_id so update the subtask milestone
|
||||
self.task_1.project_id = self.project_pigs
|
||||
task_2._compute_milestone_id() # For some reason, though it should be auto triggered, because task_2.project_id changes with previous line, it's not
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'project_id': self.project_pigs.id,
|
||||
'milestone_id': self.milestone_pigs.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs)
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs,
|
||||
"The child milestone should be updated as the project of the parent task does not actually change.")
|
||||
|
||||
# I. Same case as G but the display_on_project is set to False on the child task -> Both project and milestone of the subtask should be updated
|
||||
self.task_1.write({
|
||||
'project_id': self.project_pigs.id,
|
||||
'milestone_id': extra_milestone_pigs.id,
|
||||
})
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'project_id': self.project_goats.id,
|
||||
'milestone_id': self.milestone_goats.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_goats)
|
||||
self.assertEqual(task_2.project_id, self.project_goats)
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_goats,
|
||||
"The child milestone should be updated if the parent task's project is changed only if dislay_on_project is set to False for the subtask.")
|
||||
|
||||
# J. Same case as F but subtask is closed -> no update of its milestone
|
||||
self.task_1.project_id = task_2.project_id = self.project_pigs
|
||||
self.task_1.milestone_id = task_2.milestone_id = self.milestone_pigs
|
||||
task_2.state = '1_done'
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'milestone_id': extra_milestone_pigs.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs,
|
||||
"The child milestone should not be updated if it is closed.")
|
||||
|
||||
def test_project_milestone_color(self):
|
||||
"""
|
||||
Test Steps:
|
||||
1. Assign `milestone_pigs` to `task_1` and mark it as 'done'.
|
||||
2. Set `milestone_goats` deadline to yesterday.
|
||||
3. Compute the next milestone for `project_pigs` and `project_goats`.
|
||||
4. Validate that:
|
||||
- `project_goats` has exceeded the milestone deadline.
|
||||
- `project_pigs` has not exceeded the milestone deadline.
|
||||
- `project_pigs` can mark the milestone as done.
|
||||
- `project_goats` cannot mark the milestone as done.
|
||||
"""
|
||||
self.task_1.write({
|
||||
'milestone_id': self.milestone_pigs.id,
|
||||
'state': '1_done',
|
||||
})
|
||||
self.milestone_goats.write({'deadline': fields.Date.today() + relativedelta(days=-1)})
|
||||
|
||||
(self.project_pigs | self.project_goats)._compute_next_milestone_id()
|
||||
|
||||
self.assertTrue(self.project_goats.is_milestone_deadline_exceeded, "Expected project_goats to have exceeded the milestone deadline.")
|
||||
self.assertFalse(self.project_pigs.is_milestone_deadline_exceeded, "Expected project_pigs to not have exceeded the milestone deadline.")
|
||||
self.assertTrue(self.project_pigs.can_mark_milestone_as_done, "Expected project_pigs to be able to mark the milestone as done.")
|
||||
self.assertFalse(self.project_goats.can_mark_milestone_as_done, "Expected project_goats to not be able to mark the milestone as done.")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class TestProjectProfitabilityCommon(TransactionCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.user.group_ids += cls.env.ref('project.group_project_manager')
|
||||
|
||||
cls.partner = cls.env['res.partner'].create({
|
||||
'name': 'Georges',
|
||||
|
|
@ -17,7 +18,6 @@ class TestProjectProfitabilityCommon(TransactionCase):
|
|||
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan A',
|
||||
'company_id': False,
|
||||
})
|
||||
cls.analytic_account = cls.env['account.analytic.account'].create({
|
||||
'name': 'Project - AA',
|
||||
|
|
@ -27,7 +27,7 @@ class TestProjectProfitabilityCommon(TransactionCase):
|
|||
cls.project = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Project',
|
||||
'partner_id': cls.partner.id,
|
||||
'analytic_account_id': cls.analytic_account.id,
|
||||
'account_id': cls.analytic_account.id,
|
||||
})
|
||||
cls.task = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Task',
|
||||
|
|
@ -37,7 +37,19 @@ class TestProjectProfitabilityCommon(TransactionCase):
|
|||
'revenues': {'data': [], 'total': {'invoiced': 0.0, 'to_invoice': 0.0}},
|
||||
'costs': {'data': [], 'total': {'billed': 0.0, 'to_bill': 0.0}},
|
||||
}
|
||||
|
||||
cls.foreign_currency = cls.env['res.currency'].create({
|
||||
'name': 'Chaos orb',
|
||||
'symbol': '☺',
|
||||
'rounding': 0.001,
|
||||
'position': 'after',
|
||||
'currency_unit_label': 'Chaos',
|
||||
'currency_subunit_label': 'orb',
|
||||
})
|
||||
cls.env['res.currency.rate'].create({
|
||||
'name': '2016-01-01',
|
||||
'rate': '5.0',
|
||||
'currency_id': cls.foreign_currency.id,
|
||||
})
|
||||
|
||||
class TestProfitability(TestProjectProfitabilityCommon):
|
||||
def test_project_profitability(self):
|
||||
|
|
@ -46,9 +58,8 @@ class TestProfitability(TestProjectProfitabilityCommon):
|
|||
In this module, the project profitability should have no data.
|
||||
So the no revenue and cost should be found.
|
||||
"""
|
||||
profitability_items = self.project._get_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
profitability_items,
|
||||
self.project._get_profitability_items(False),
|
||||
self.project_profitability_items_empty,
|
||||
'The profitability data of the project should be return no data and so 0 for each total amount.'
|
||||
)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.fields import Command, Domain
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
|
@ -74,6 +71,19 @@ class TestProjectSharingCommon(TestProjectCommon):
|
|||
view=self.project_sharing_form_view_xml_id
|
||||
)
|
||||
|
||||
def get_project_share_link(self):
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_no_collabo.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
}).action_send_mail()
|
||||
return self.env['mail.message'].search([
|
||||
('partner_ids', 'in', self.user_portal.partner_id.id),
|
||||
])
|
||||
|
||||
|
||||
@tagged('project_sharing')
|
||||
class TestProjectSharing(TestProjectSharingCommon):
|
||||
|
||||
|
|
@ -86,24 +96,162 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
2) Check if no access rights are given to a portal user
|
||||
3) Add access rights to a portal user
|
||||
"""
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_portal.id,
|
||||
'access_mode': 'edit',
|
||||
})
|
||||
self.assertFalse(project_share_wizard.partner_ids, 'No collaborator should be in the wizard.')
|
||||
self.assertFalse(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should not have accessed in project sharing views.')
|
||||
project_share_wizard.write({'partner_ids': [Command.link(self.user_portal.partner_id.id)]})
|
||||
self.project_portal.message_unsubscribe(partner_ids=self.user_portal.partner_id.ids)
|
||||
project_share_form = Form(self.env['project.share.wizard'].with_context(active_model='project.project', active_id=self.project_portal.id))
|
||||
self.assertFalse(project_share_form.collaborator_ids, 'No collaborator should be in the wizard.')
|
||||
with self.assertRaises(AccessError, msg='The public user should not have any access to project sharing feature of the portal project.'):
|
||||
self.project_portal.with_user(self.user_portal)._check_project_sharing_access()
|
||||
with project_share_form.collaborator_ids.new() as collaborator_form:
|
||||
collaborator_form.partner_id = self.user_portal.partner_id
|
||||
collaborator_form.access_mode = 'edit'
|
||||
project_share_wizard = project_share_form.save()
|
||||
project_share_wizard.action_send_mail()
|
||||
self.assertEqual(len(self.project_portal.collaborator_ids), 1, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.')
|
||||
self.assertDictEqual({
|
||||
'partner_id': self.project_portal.collaborator_ids.partner_id,
|
||||
'project_id': self.project_portal.collaborator_ids.project_id,
|
||||
'limited_access': self.project_portal.collaborator_ids.limited_access,
|
||||
}, {
|
||||
'partner_id': self.user_portal.partner_id,
|
||||
'project_id': self.project_portal,
|
||||
'limited_access': False,
|
||||
}, 'The access rights added should be the read access for the portal project for Chell Gladys.')
|
||||
self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should have read access to the portal project with project sharing feature.')
|
||||
project_share_wizard = self.env['project.share.wizard'].with_context(active_model="project.project", active_id=self.project_portal.id).new({})
|
||||
self.assertEqual(len(project_share_wizard.collaborator_ids), 1, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.')
|
||||
self.assertDictEqual({
|
||||
'partner_id': project_share_wizard.collaborator_ids.partner_id,
|
||||
'access_mode': project_share_wizard.collaborator_ids.access_mode,
|
||||
}, {
|
||||
'partner_id': self.user_portal.partner_id,
|
||||
'access_mode': 'edit',
|
||||
})
|
||||
|
||||
def test_project_share_wizard_add_collaborator_with_limited_access(self):
|
||||
ProjectShare = self.env['project.share.wizard'].with_context(active_model="project.project", active_id=self.project_portal.id)
|
||||
self.project_portal.write({
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.partner_1.id}),
|
||||
],
|
||||
})
|
||||
self.project_portal.message_unsubscribe(partner_ids=[self.user_portal.partner_id.id])
|
||||
project_share_form = Form(ProjectShare)
|
||||
self.assertEqual(len(project_share_form.collaborator_ids), 1)
|
||||
with project_share_form.collaborator_ids.new() as collaborator_form:
|
||||
collaborator_form.partner_id = self.user_portal.partner_id
|
||||
collaborator_form.access_mode = 'edit_limited'
|
||||
project_share_wizard = project_share_form.save()
|
||||
project_share_wizard.action_send_mail()
|
||||
self.assertEqual(len(self.project_portal.collaborator_ids), 2, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.')
|
||||
self.assertEqual(self.project_portal.collaborator_ids.partner_id, self.user_portal.partner_id + self.partner_1)
|
||||
for collaborator in self.project_portal.collaborator_ids:
|
||||
collaborator_vals = {
|
||||
'partner_id': collaborator.partner_id,
|
||||
'project_id': collaborator.project_id,
|
||||
'limited_access': collaborator.limited_access,
|
||||
}
|
||||
if collaborator.partner_id == self.user_portal.partner_id:
|
||||
self.assertDictEqual(collaborator_vals, {
|
||||
'partner_id': self.user_portal.partner_id,
|
||||
'project_id': self.project_portal,
|
||||
'limited_access': True,
|
||||
})
|
||||
else:
|
||||
self.assertDictEqual(collaborator_vals, {
|
||||
'partner_id': self.partner_1,
|
||||
'project_id': self.project_portal,
|
||||
'limited_access': False,
|
||||
})
|
||||
self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should have read access to the portal project with project sharing feature.')
|
||||
|
||||
project_share_wizard = ProjectShare.new({})
|
||||
self.assertEqual(len(project_share_wizard.collaborator_ids), 2, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.')
|
||||
for collaborator in project_share_wizard.collaborator_ids:
|
||||
collaborator_vals = {
|
||||
'partner_id': collaborator.partner_id,
|
||||
'access_mode': collaborator.access_mode,
|
||||
}
|
||||
if collaborator.partner_id == self.user_portal.partner_id:
|
||||
self.assertDictEqual(collaborator_vals, {
|
||||
'partner_id': self.user_portal.partner_id,
|
||||
'access_mode': 'edit_limited',
|
||||
})
|
||||
else:
|
||||
self.assertDictEqual(collaborator_vals, {
|
||||
'partner_id': self.partner_1,
|
||||
'access_mode': 'edit',
|
||||
})
|
||||
|
||||
def test_project_share_wizard_remove_collaborators(self):
|
||||
PortalShare = self.env['project.share.wizard'].with_context(active_model="project.project", active_id=self.project_portal.id)
|
||||
self.project_portal.write({
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id}),
|
||||
Command.create({'partner_id': self.partner_1.id, 'limited_access': True}),
|
||||
],
|
||||
})
|
||||
self.project_portal.message_subscribe(partner_ids=[self.partner_2.id])
|
||||
with Form(PortalShare) as project_share_form:
|
||||
self.assertEqual(len(project_share_form.collaborator_ids), 3, "2 external collaborators should be found for that project.")
|
||||
collaborator_vals_per_id = project_share_form.collaborator_ids._field_value._data
|
||||
collaborator_access_mode_per_partner_id = {
|
||||
c['partner_id']: c['access_mode']
|
||||
for c in collaborator_vals_per_id.values()
|
||||
}
|
||||
self.assertIn(self.user_portal.partner_id.id, collaborator_access_mode_per_partner_id)
|
||||
self.assertIn(self.partner_1.id, collaborator_access_mode_per_partner_id)
|
||||
self.assertIn(self.partner_2.id, collaborator_access_mode_per_partner_id)
|
||||
access_mode_expected_per_partner_id = {
|
||||
self.user_portal.partner_id.id: 'edit',
|
||||
self.partner_1.id: 'edit_limited',
|
||||
self.partner_2.id: 'read',
|
||||
}
|
||||
self.assertDictEqual(collaborator_access_mode_per_partner_id, access_mode_expected_per_partner_id)
|
||||
collaborator_ids_to_remove = {c_id for c_id, vals in collaborator_vals_per_id.items() if vals['access_mode'] != 'read'}
|
||||
index = 0
|
||||
for collaborator_id in project_share_form.collaborator_ids.ids:
|
||||
if collaborator_id in collaborator_ids_to_remove:
|
||||
project_share_form.collaborator_ids.remove(index)
|
||||
else:
|
||||
index += 1
|
||||
|
||||
self.assertFalse(self.project_portal.collaborator_ids)
|
||||
self.assertIn(self.partner_2, self.project_portal.message_partner_ids, "The readonly partner should still be a follower.")
|
||||
self.assertNotIn(self.user_portal.partner_id, self.project_portal.message_partner_ids, "The readonly partner should still be a follower.")
|
||||
self.assertNotIn(self.partner_1, self.project_portal.message_partner_ids, "The readonly partner should still be a follower.")
|
||||
|
||||
def test_project_share_wizard_alter_access_mode_collaborators(self):
|
||||
ProjectShare = self.env['project.share.wizard'].with_context(active_model="project.project", active_id=self.project_portal.id)
|
||||
self.project_portal.write({
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id}),
|
||||
Command.create({'partner_id': self.partner_1.id, 'limited_access': True}),
|
||||
],
|
||||
'message_partner_ids': [ # readonly access
|
||||
Command.link(self.partner_2.id),
|
||||
],
|
||||
})
|
||||
with Form(ProjectShare) as project_share_form:
|
||||
access_updated_per_partner_id = {
|
||||
self.user_portal.partner_id.id: 'edit_limited',
|
||||
self.partner_2.id: 'edit',
|
||||
}
|
||||
for index in range(len(project_share_form.collaborator_ids.ids)):
|
||||
with project_share_form.collaborator_ids.edit(index) as collaborator_form:
|
||||
if collaborator_form.partner_id.id in access_updated_per_partner_id:
|
||||
collaborator_form.access_mode = access_updated_per_partner_id[collaborator_form.partner_id.id]
|
||||
|
||||
self.assertEqual(len(self.project_portal.collaborator_ids), 3, "3 collaborators should be found for that project.")
|
||||
self.assertEqual(
|
||||
self.project_portal.collaborator_ids.partner_id,
|
||||
self.user_portal.partner_id + self.partner_1 + self.partner_2,
|
||||
"The collaborators should be the portal user, Valid Lelitre and Valid Poilvache.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.project_portal.collaborator_ids.filtered(lambda c: c.limited_access).partner_id,
|
||||
self.user_portal.partner_id + self.partner_1,
|
||||
"The portal user and Valid Lelitre should have limited access.",
|
||||
)
|
||||
|
||||
def test_project_sharing_access(self):
|
||||
""" Check if the different user types can access to project sharing feature as expected. """
|
||||
|
|
@ -125,7 +273,6 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
3) Give the 'edit' access mode to a portal user in a project and try to create task with this user.
|
||||
3.1) Try to change the project of the new task with this user.
|
||||
"""
|
||||
self.project_portal.allow_subtasks = True
|
||||
Task = self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id, 'default_user_ids': [(4, self.user_portal.id)]})
|
||||
# 1) Give the 'read' access mode to a portal user in a project and try to create task with this user.
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."):
|
||||
|
|
@ -146,6 +293,7 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
self.assertEqual(task.name, 'Test')
|
||||
self.assertEqual(task.project_id, self.project_portal)
|
||||
self.assertFalse(task.portal_user_names)
|
||||
self.assertTrue(task.stage_id)
|
||||
|
||||
# Check creating a sub-task while creating the parent task works as expected.
|
||||
self.assertEqual(task.child_ids.name, 'Test Subtask')
|
||||
|
|
@ -170,32 +318,40 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
task = Task.with_context(default_parent_id=self.task_no_collabo.id).create({'name': 'foo'})
|
||||
|
||||
# Create/Update a forbidden task through child_ids
|
||||
with self.assertRaisesRegex(AccessError, "You cannot write on description"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.create({'name': 'Foo', 'description': 'Foo'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.update(self.task_no_collabo.id, {'name': 'Foo'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.delete(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.unlink(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.link(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.set([self.task_no_collabo.id])]})
|
||||
|
||||
# Same thing but using context defaults
|
||||
with self.assertRaisesRegex(AccessError, "You cannot write on description"):
|
||||
Task.with_context(default_child_ids=[Command.create({'name': 'Foo', 'description': 'Foo'})]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
# However, cache is updated, but nothing is written.
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
Task.with_context(default_child_ids=[Command.update(self.task_no_collabo.id, {'name': 'Foo'})]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.delete(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.unlink(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.link(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.set([self.task_no_collabo.id])]).create({'name': 'foo'})
|
||||
with Task.env.cr.savepoint() as sp:
|
||||
task = Task.with_context(default_child_ids=[Command.delete(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
task.env.invalidate_all()
|
||||
self.assertTrue(self.task_no_collabo.exists(), "Task should still be there, no delete is sent")
|
||||
sp.rollback()
|
||||
with self.env.cr.savepoint() as sp:
|
||||
self.task_no_collabo.parent_id = self.task_no_collabo.create({'name': 'parent collabo'})
|
||||
task = Task.with_context(default_child_ids=[Command.unlink(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
task.env.invalidate_all()
|
||||
self.assertTrue(self.task_no_collabo.parent_id, "Task should still be there, no delete is sent")
|
||||
sp.rollback()
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
task = Task.with_context(default_child_ids=[Command.link(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
task.env.invalidate_all()
|
||||
self.assertFalse(task.child_ids)
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
task = Task.with_context(default_child_ids=[Command.set([self.task_no_collabo.id])]).create({'name': 'foo'})
|
||||
task.env.invalidate_all()
|
||||
self.assertFalse(task.child_ids)
|
||||
|
||||
# Create/update a tag through tag_ids
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to create 'Project Tags'"):
|
||||
|
|
@ -208,15 +364,23 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
# Same thing but using context defaults
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to create 'Project Tags'"):
|
||||
Task.with_context(default_tag_ids=[Command.create({'name': 'Bar'})]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Project Tags'"):
|
||||
Task.with_context(default_tag_ids=[Command.update(self.task_tag.id, {'name': 'Bar'})]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Project Tags'"):
|
||||
with Task.env.cr.savepoint() as sp:
|
||||
task = Task.with_context(default_tag_ids=[Command.update(self.task_tag.id, {'name': 'Bar'})]).create({'name': 'foo'})
|
||||
task.env.invalidate_all()
|
||||
self.assertNotEqual(self.task_tag.name, 'Bar')
|
||||
sp.rollback()
|
||||
with Task.env.cr.savepoint() as sp:
|
||||
Task.with_context(default_tag_ids=[Command.delete(self.task_tag.id)]).create({'name': 'foo'})
|
||||
task.env.invalidate_all()
|
||||
self.assertTrue(self.task_tag.exists())
|
||||
sp.rollback()
|
||||
|
||||
task = Task.create({'name': 'foo', 'tag_ids': [Command.link(self.task_tag.id)]})
|
||||
task = Task.create({'name': 'foo', 'color': 1, 'tag_ids': [Command.link(self.task_tag.id)]})
|
||||
self.assertEqual(task.color, 1)
|
||||
self.assertEqual(task.tag_ids, self.task_tag)
|
||||
|
||||
Task.create({'name': 'foo', 'tag_ids': [Command.set([self.task_tag.id])]})
|
||||
task = Task.create({'name': 'foo', 'color': 4, 'tag_ids': [Command.set([self.task_tag.id])]})
|
||||
self.assertEqual(task.color, 4)
|
||||
self.assertEqual(task.tag_ids, self.task_tag)
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
|
|
@ -231,11 +395,9 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
3.1) Try to change the project of the new task with this user.
|
||||
3.2) Create a sub-task
|
||||
3.3) Create a second sub-task
|
||||
4.1) Restrict to edit with limited access and try to edit a task with and without following it
|
||||
4.2) Restrict to read and check he can no longer edit the tasks, even if he is within the followers
|
||||
"""
|
||||
# 0) Allow to create subtasks in the project tasks
|
||||
# Required for `child_ids` to be visible in the view
|
||||
# {'invisible': [('allow_subtasks', '=', False)]}
|
||||
self.project_cows.allow_subtasks = True
|
||||
# 1) Give the 'read' access mode to a portal user in a project and try to create task with this user.
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."):
|
||||
with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form:
|
||||
|
|
@ -243,15 +405,15 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
task = form.save()
|
||||
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'access_mode': 'edit',
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_cows.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.user_portal.partner_id.id),
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
# the portal user is set as follower for the task_cow. Without it he does not have read access to the task, and thus can not access its view form
|
||||
self.task_cow.message_subscribe(partner_ids=self.user_portal.partner_id.ids)
|
||||
with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id, 'uid': self.user_portal.id}), self.user_portal) as form:
|
||||
form.name = 'Test'
|
||||
task = form.save()
|
||||
|
|
@ -268,7 +430,7 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
with form.child_ids.new() as subtask_form:
|
||||
subtask_form.name = 'Test Subtask'
|
||||
with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
|
||||
subtask_form.display_project_id = self.project_portal
|
||||
subtask_form.project_id = self.project_portal
|
||||
self.assertEqual(task.child_ids.name, 'Test Subtask')
|
||||
self.assertEqual(task.child_ids.project_id, self.project_cows)
|
||||
self.assertFalse(task.child_ids.portal_user_names, 'by default no user should be assigned to a subtask created by the portal user.')
|
||||
|
|
@ -298,17 +460,15 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
task.write({'parent_id': self.task_no_collabo.id})
|
||||
|
||||
# Create/Update a forbidden task through child_ids
|
||||
with self.assertRaisesRegex(AccessError, "You cannot write on description"):
|
||||
task.write({'child_ids': [Command.create({'name': 'Foo', 'description': 'Foo'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
task.write({'child_ids': [Command.update(self.task_no_collabo.id, {'name': 'Foo'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
task.write({'child_ids': [Command.delete(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
task.write({'child_ids': [Command.unlink(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
task.write({'child_ids': [Command.link(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
with self.assertRaisesRegex(AccessError, "top-secret records"):
|
||||
task.write({'child_ids': [Command.set([self.task_no_collabo.id])]})
|
||||
|
||||
# Create/update a tag through tag_ids
|
||||
|
|
@ -332,6 +492,44 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
task.write({'tag_ids': [Command.set([self.task_tag.id])]})
|
||||
self.assertEqual(task.tag_ids, self.task_tag)
|
||||
|
||||
# 4.1) Restrict the collaborator access to edit with limited access, restricting the collaborator to edit task
|
||||
# on which he is in the followers only
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_cows.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id, 'access_mode': 'edit_limited'}),
|
||||
],
|
||||
})
|
||||
self.assertTrue(self.project_cows.collaborator_ids.limited_access)
|
||||
|
||||
# Removing the collaborator from the followers prevents him to edit the task
|
||||
task.sudo().message_partner_ids -= self.user_portal.partner_id
|
||||
with self.assertRaises(AccessError):
|
||||
task.write({'name': 'foo'})
|
||||
|
||||
# Adding the collaborator back to the followers grants him to edit the task
|
||||
task.sudo().message_partner_ids += self.user_portal.partner_id
|
||||
task.write({'name': 'foo'})
|
||||
|
||||
# 4.2) Restrict the access to read and check he can no longer edit the tasks, even if he is within the followers
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_cows.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id, 'access_mode': 'read'}),
|
||||
# Create a second collaborator with edit just so that the project sharing record rules
|
||||
# do not get automatically disabled when removing the last remaining edit collaborator
|
||||
Command.create({'partner_id': self.env['res.partner'].create({'name': 'Alain'}).id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
# Sanity check: Assert the project sharing record rule is still active
|
||||
self.assertTrue(self.env.ref('project.project_task_rule_portal_project_sharing').active)
|
||||
|
||||
# Assert the collaborator can no longer write on the task despite he is still in the followers of the task
|
||||
self.assertIn(self.user_portal.partner_id, task.sudo().message_partner_ids)
|
||||
with self.assertRaises(AccessError):
|
||||
task.write({'name': 'foo'})
|
||||
|
||||
def test_portal_user_cannot_see_all_assignees(self):
|
||||
""" Test when the portal sees a task he cannot see all the assignees.
|
||||
|
|
@ -350,15 +548,15 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
self.assertEqual(len(self.task_cow.user_ids), 2, '2 users should be assigned in this task.')
|
||||
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'access_mode': 'edit',
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_cows.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.user_portal.partner_id.id),
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
# subscribe the portal user to give him read access to the task.
|
||||
self.task_cow.message_subscribe(partner_ids=self.user_portal.partner_id.ids)
|
||||
self.assertFalse(self.task_cow.with_user(self.user_portal).user_ids, 'the portal user should see no assigness in the task.')
|
||||
task_portal_read = self.task_cow.with_user(self.user_portal).read(['portal_user_names'])
|
||||
self.assertEqual(self.task_cow.portal_user_names, task_portal_read[0]['portal_user_names'], 'the portal user should see assignees name in the task via the `portal_user_names` field.')
|
||||
|
|
@ -370,65 +568,52 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
if a email template is set in `rating_template_id` field in the new stage.
|
||||
"""
|
||||
self.project_portal.write({
|
||||
'rating_active': True,
|
||||
'rating_status': 'stage',
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id}),
|
||||
],
|
||||
})
|
||||
self.task_portal.with_user(self.user_portal).write({'stage_id': self.project_portal.type_ids[-1].id})
|
||||
stage = self.project_portal.type_ids[-1]
|
||||
stage.write({
|
||||
'rating_active': True,
|
||||
'rating_status': 'stage',
|
||||
})
|
||||
self.task_portal.with_user(self.user_portal).write({'stage_id': stage.id})
|
||||
|
||||
def test_orm_method_with_true_false_domain(self):
|
||||
""" Test orm method overriden in project for project sharing works with TRUE_LEAF/FALSE_LEAF
|
||||
""" Test orm method overriden in project for project sharing works
|
||||
|
||||
Test Case
|
||||
=========
|
||||
1) Share a project in edit mode for portal user
|
||||
2) Search the portal task contained in the project shared by using a domain with TRUE_LEAF
|
||||
2) Search the portal task contained in the project shared by using a TRUE domain
|
||||
3) Check the task is found with the `search` method
|
||||
4) filter the task with `TRUE_DOMAIN` and check if the task is always returned by `filtered_domain` method
|
||||
5) filter the task with `FALSE_DOMAIN` and check if no task is returned by `filtered_domain` method
|
||||
6) Search the task with `FALSE_LEAF` and check no task is found with `search` method
|
||||
7) Call `read_group` method with `TRUE_LEAF` in the domain and check if the task is found
|
||||
8) Call `read_group` method with `FALSE_LEAF` in the domain and check if no task is found
|
||||
4) Search the task with `FALSE` and check no task is found with `search` method
|
||||
5) Call `read_group` method with `TRUE` in the domain and check if the task is found
|
||||
6) Call `read_group` method with `FALSE` in the domain and check if no task is found
|
||||
"""
|
||||
domain = [('id', '=', self.task_portal.id)]
|
||||
domain = Domain('id', '=', self.task_portal.id)
|
||||
self.project_portal.write({
|
||||
'collaborator_ids': [Command.create({
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
})],
|
||||
})
|
||||
task = self.env['project.task'].with_user(self.user_portal).search(
|
||||
expression.AND([
|
||||
expression.TRUE_DOMAIN,
|
||||
domain,
|
||||
])
|
||||
)
|
||||
task = self.env['project.task'].with_user(self.user_portal).search(domain)
|
||||
self.assertTrue(task, 'The task should be found.')
|
||||
self.assertEqual(task, task.filtered_domain(expression.TRUE_DOMAIN), 'The task found should be kept since the domain is truly')
|
||||
self.assertFalse(task.filtered_domain(expression.FALSE_DOMAIN), 'The task should not be found since the domain is falsy')
|
||||
task = self.env['project.task'].with_user(self.user_portal).search(
|
||||
expression.AND([
|
||||
expression.FALSE_DOMAIN,
|
||||
domain,
|
||||
]),
|
||||
)
|
||||
task = self.env['project.task'].with_user(self.user_portal).search(Domain.FALSE)
|
||||
self.assertFalse(task, 'No task should be found since the domain contained a falsy tuple.')
|
||||
|
||||
task_read_group = self.env['project.task'].read_group(
|
||||
expression.AND([expression.TRUE_DOMAIN, domain]),
|
||||
['id'],
|
||||
[],
|
||||
task_read_group = self.env['project.task'].formatted_read_group(
|
||||
domain,
|
||||
aggregates=['id:min', '__count'],
|
||||
)
|
||||
self.assertEqual(task_read_group[0]['__count'], 1, 'The task should be found with the read_group method containing a truly tuple.')
|
||||
self.assertEqual(task_read_group[0]['id'], self.task_portal.id, 'The task should be found with the read_group method containing a truly tuple.')
|
||||
self.assertEqual(task_read_group[0]['__count'], 1, 'The task should be found with the formatted_read_group method containing a truly tuple.')
|
||||
self.assertEqual(task_read_group[0]['id:min'], self.task_portal.id, 'The task should be found with the formatted_read_group method containing a truly tuple.')
|
||||
|
||||
task_read_group = self.env['project.task'].read_group(
|
||||
expression.AND([expression.FALSE_DOMAIN, domain]),
|
||||
['id'],
|
||||
[],
|
||||
task_read_group = self.env['project.task'].formatted_read_group(
|
||||
Domain.FALSE,
|
||||
aggregates=['__count'],
|
||||
)
|
||||
self.assertFalse(task_read_group[0]['__count'], 'No result should found with the read_group since the domain is falsy.')
|
||||
self.assertFalse(task_read_group[0]['__count'], 'No result should found with the formatted_read_group since the domain is falsy.')
|
||||
|
||||
def test_milestone_read_access_right(self):
|
||||
""" This test ensures that a portal user has read access on the milestone of the project that was shared with him """
|
||||
|
|
@ -456,3 +641,99 @@ class TestProjectSharing(TestProjectSharingCommon):
|
|||
'name': 'Test Project new Milestone',
|
||||
'project_id': self.project_portal.id,
|
||||
})
|
||||
|
||||
def test_add_followers_from_share_edit_wizard(self):
|
||||
"""
|
||||
This test ensures that when a project is shared in edit mode, the partners are correctly set as follower in the project and their respective tasks.
|
||||
"""
|
||||
company_partner = self.env.company.partner_id
|
||||
partners = partner_a, partner_b, partner_d = self.env['res.partner'].create([
|
||||
{'name': "Solanum", 'parent_id': company_partner.id},
|
||||
{'name': "Zana", 'parent_id': company_partner.id},
|
||||
{'name': "Thresh"},
|
||||
])
|
||||
partners |= company_partner
|
||||
project_to_share = self.env['project.project'].create({'name': "project to share"})
|
||||
task_with_partner_1, task_with_partner_2, task_with_parent_partner, task_without_partner = self.env['project.task'].create([{
|
||||
'name': "Task with partner 1",
|
||||
'partner_id': partner_a.id,
|
||||
'project_id': project_to_share.id,
|
||||
}, {
|
||||
'name': "Task with partner 2",
|
||||
'partner_id': partner_b.id,
|
||||
'project_id': project_to_share.id,
|
||||
}, {
|
||||
'name': "Task with company",
|
||||
'partner_id': company_partner.id,
|
||||
'project_id': project_to_share.id,
|
||||
}, {
|
||||
'name': "Task with no partner",
|
||||
'project_id': project_to_share.id,
|
||||
}])
|
||||
project_to_share._add_followers(partners)
|
||||
|
||||
self.assertEqual(partners, project_to_share.message_partner_ids, "All the partner should be set as a new follower of the project")
|
||||
self.assertEqual(partner_a, task_with_partner_1.message_partner_ids, "Only the first partner should be set as a new follower for the task 1")
|
||||
self.assertEqual(partner_b, task_with_partner_2.message_partner_ids, "Only the second partner should be set as a new follower for the task 2")
|
||||
self.assertEqual(partners - partner_d, task_with_parent_partner.message_partner_ids,
|
||||
"The first, second, and the company partner should be set as new followers for the task 3 because the partner of this task is the parent of the other 2")
|
||||
self.assertFalse(task_without_partner.message_partner_ids, "Since this task has no partner, no follower should be added")
|
||||
|
||||
def test_project_manager_remains_follower_after_sharing(self):
|
||||
"""
|
||||
Test that the project manager remains a follower when collaborators are added
|
||||
"""
|
||||
project = self.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'project',
|
||||
'privacy_visibility': 'followers',
|
||||
'user_id': self.user_projectmanager.id,
|
||||
})
|
||||
self.assertIn(self.user_projectmanager.partner_id, project.message_partner_ids, "Project manager should be a follower of the project")
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': project.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id, 'access_mode': 'read'}),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
self.assertIn(self.user_projectmanager.partner_id, project.message_partner_ids, "Project manager should still be a follower after sharing the project")
|
||||
self.assertEqual(len(project.message_follower_ids), 2, "number of followers should be 2")
|
||||
|
||||
def test_portal_user_with_edit_rights_can_close_recurring_task(self):
|
||||
"""
|
||||
Test that a portal user with edit rights can close a recurrent task.
|
||||
|
||||
Test Case:
|
||||
==========
|
||||
1) Create a project with a recurrent task.
|
||||
2) Create a portal user and give them edit rights on the project.
|
||||
3) Ensure the portal user can close the recurrent task.
|
||||
"""
|
||||
portal_user = self.env['res.users'].create({
|
||||
'name': 'Portal User',
|
||||
'login': 'portaluser',
|
||||
'email': 'portaluser@odoo.com',
|
||||
'group_ids': [(6, 0, [self.env.ref('base.group_portal').id])],
|
||||
})
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project with Portal User',
|
||||
})
|
||||
project.task_ids = [Command.create({
|
||||
'name': 'Recurrent Task',
|
||||
'recurring_task': True,
|
||||
'repeat_type': 'forever',
|
||||
})]
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': project.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': portal_user.partner_id.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
task = project.task_ids[0]
|
||||
self.env.invalidate_all()
|
||||
task.with_user(portal_user).write({'state': '1_done'})
|
||||
self.assertEqual(task.state, '1_done', "The portal user with edit rights should be able to mark the task as done.")
|
||||
next_task = task.recurrence_id.task_ids.filtered(lambda t: t != task)
|
||||
self.assertTrue(next_task, "The next occurrence of the recurrent task should be created.")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
|
@ -8,9 +7,9 @@ from lxml import etree
|
|||
from re import search
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tools import mute_logger, config
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged, HttpCase
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
from .test_project_sharing import TestProjectSharingCommon
|
||||
|
||||
|
|
@ -21,38 +20,63 @@ class TestProjectSharingPortalAccess(TestProjectSharingCommon):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
project_share_wizard = cls.env['project.share.wizard'].create({
|
||||
'access_mode': 'edit',
|
||||
cls.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': cls.project_portal.id,
|
||||
'partner_ids': [
|
||||
Command.link(cls.partner_portal.id),
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': cls.partner_portal.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
Task = cls.env['project.task']
|
||||
readable_fields, writeable_fields = Task._portal_accessible_fields()
|
||||
cls.read_protected_fields_task = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in Task._fields.items()
|
||||
if k in Task.SELF_READABLE_FIELDS
|
||||
if k in readable_fields
|
||||
])
|
||||
cls.write_protected_fields_task = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in Task._fields.items()
|
||||
if k in Task.SELF_WRITABLE_FIELDS
|
||||
if k in writeable_fields
|
||||
])
|
||||
cls.readonly_protected_fields_task = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in Task._fields.items()
|
||||
if k in Task.SELF_READABLE_FIELDS and k not in Task.SELF_WRITABLE_FIELDS
|
||||
if k in readable_fields and k not in writeable_fields
|
||||
])
|
||||
cls.other_fields_task = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in Task._fields.items()
|
||||
if k not in Task.SELF_READABLE_FIELDS
|
||||
if k not in readable_fields
|
||||
])
|
||||
|
||||
def test_mention_suggestions(self):
|
||||
data = self.task_portal.with_user(self.user_portal).get_mention_suggestions(search="")
|
||||
suggestion_ids = {partner.get("id") for partner in data.get("res.partner")}
|
||||
self.assertEqual(
|
||||
suggestion_ids,
|
||||
{self.user_projectuser.partner_id.id, self.user_portal.partner_id.id},
|
||||
"Portal user as a project collaborator should have access to mention suggestions",
|
||||
)
|
||||
self.assertEqual(
|
||||
data["res.partner"][0]["mention_token"],
|
||||
self.user_projectuser.partner_id._get_mention_token(),
|
||||
)
|
||||
self.assertEqual(
|
||||
data["res.partner"][1]["mention_token"],
|
||||
self.user_portal.partner_id._get_mention_token(),
|
||||
)
|
||||
# remove portal user from the project collaborators
|
||||
self.project_portal.collaborator_ids.filtered(
|
||||
lambda rec: rec.partner_id == self.user_portal.partner_id
|
||||
).unlink()
|
||||
self.assertEqual(
|
||||
{},
|
||||
self.task_portal.with_user(self.user_portal).get_mention_suggestions(search=""),
|
||||
"Non collaborator portal user should not have access to mention suggestions",
|
||||
)
|
||||
|
||||
def test_readonly_fields(self):
|
||||
""" The fields are not writeable should not be editable by the portal user. """
|
||||
view_infos = self.task_portal.get_view(self.env.ref(self.project_sharing_form_view_xml_id).id)
|
||||
|
|
@ -68,48 +92,101 @@ class TestProjectSharingPortalAccess(TestProjectSharingCommon):
|
|||
form.__setattr__(field, 'coucou')
|
||||
|
||||
def test_read_task_with_portal_user(self):
|
||||
self.task_portal.with_user(self.user_portal).read(self.read_protected_fields_task)
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
self.task_portal.with_user(self.user_portal).read(self.other_fields_task)
|
||||
|
||||
def test_write_with_portal_user(self):
|
||||
for field in self.readonly_protected_fields_task:
|
||||
with self.assertRaises(AccessError):
|
||||
self.task_portal.with_user(self.user_portal).write({field: 'dummy'})
|
||||
task = self.task_portal.with_user(self.user_portal)
|
||||
task.check_access('read')
|
||||
task.read(self.read_protected_fields_task)
|
||||
|
||||
for field in self.other_fields_task:
|
||||
with self.assertRaises(AccessError):
|
||||
self.task_portal.with_user(self.user_portal).write({field: 'dummy'})
|
||||
task.invalidate_recordset()
|
||||
with self.assertRaises(AccessError, msg=f"Field {field} should be inaccessible"):
|
||||
task.read([field])
|
||||
|
||||
def test_write_task_with_portal_user(self):
|
||||
task = self.task_portal.with_user(self.user_portal)
|
||||
task.check_access('write')
|
||||
|
||||
def dummy_value(field_name):
|
||||
field = task._fields[field_name]
|
||||
if field.is_text:
|
||||
value = 'dummy'
|
||||
if field.type == 'html':
|
||||
value = f'<p>{value}</p>'
|
||||
return value
|
||||
if field.relational and field.comodel_name != 'ir.attachment':
|
||||
value = task.env[field.comodel_name].search([], limit=1).id
|
||||
if field.type != 'many2one':
|
||||
value = [value]
|
||||
return value
|
||||
if field.name == 'id':
|
||||
return 42
|
||||
return task.default_get([field_name]).get(field_name, False)
|
||||
|
||||
for field_name in self.write_protected_fields_task:
|
||||
field = task._fields[field_name]
|
||||
if field.comodel_name == 'project.task':
|
||||
other_task = self.env['project.task'].create({'name': 'Parent task', 'project_id': task.project_id.id})
|
||||
value = other_task.id if field.type == 'many2one' else other_task.ids
|
||||
task.write({field_name: value})
|
||||
self.assertEqual(task[field_name], other_task)
|
||||
else:
|
||||
value = dummy_value(field_name)
|
||||
task.write({field_name: value})
|
||||
actual_value = task[field_name]
|
||||
expected_value = field.convert_to_record(value, task)
|
||||
self.assertEqual(actual_value, expected_value, f"Field {field} should be editable.")
|
||||
|
||||
for field in self.readonly_protected_fields_task:
|
||||
with self.assertRaises(AccessError, msg=f"Field {field} should be readonly"):
|
||||
task.write({field: dummy_value(field)})
|
||||
|
||||
for field in self.other_fields_task:
|
||||
with self.assertRaises(AccessError, msg=f"Field {field} should be inaccessible"):
|
||||
task.write({field: dummy_value(field)})
|
||||
|
||||
def test_wizard_confirm(self):
|
||||
partner_portal_no_user = self.env['res.partner'].create({
|
||||
'name': 'NoUser portal',
|
||||
'email': 'no@user.portal',
|
||||
'company_id': False,
|
||||
'user_ids': [],
|
||||
})
|
||||
|
||||
project_share_wizard_no_user = self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_portal.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': partner_portal_no_user.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
self.env["res.config.settings"].create({"auth_signup_uninvited": 'b2b'}).execute()
|
||||
|
||||
project_share_wizard_no_user_action = project_share_wizard_no_user.action_share_record()
|
||||
self.assertEqual(project_share_wizard_no_user_action['type'], 'ir.actions.act_window', 'Sharing a project with partner without user should display a confimation dialog')
|
||||
project_share_wizard_confirmation = self.env['project.share.wizard'].browse(project_share_wizard_no_user_action['res_id'])
|
||||
|
||||
project_share_wizard_confirmation.action_send_mail()
|
||||
mail_partner = self.env['mail.message'].search([('partner_ids', '=', partner_portal_no_user.id)], limit=1)
|
||||
self.assertTrue(mail_partner, 'A mail should have been sent to the non portal user')
|
||||
self.assertIn(f'href="http://localhost:{config["http_port"]}/web/signup', str(mail_partner.body), 'The message link should contain the url to register to the portal')
|
||||
self.assertIn('token=', str(mail_partner.body), 'The message link should contain a personalized token to register to the portal')
|
||||
|
||||
|
||||
class TestProjectSharingChatterAccess(TestProjectSharingCommon, HttpCase):
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_post_chatter_as_portal_user(self):
|
||||
self.project_no_collabo.privacy_visibility = 'portal'
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_no_collabo.id,
|
||||
'access_mode': 'edit',
|
||||
'partner_ids': [Command.set([self.user_portal.partner_id.id])],
|
||||
}).action_send_mail()
|
||||
message = self.env['mail.message'].search([
|
||||
('partner_ids', 'in', self.user_portal.partner_id.id),
|
||||
])
|
||||
|
||||
message = self.get_project_share_link()
|
||||
share_link = str(message.body.split('href="')[1].split('">')[0])
|
||||
match = search(r"access_token=([^&]+)&pid=([^&]+)&hash=([^&]*)", share_link)
|
||||
access_token, pid, _hash = match.groups()
|
||||
|
||||
res = self.url_open(
|
||||
url="/mail/chatter_post",
|
||||
url="/mail/message/post",
|
||||
data=json.dumps({
|
||||
"params": {
|
||||
"res_model": 'project.task',
|
||||
"res_id": self.task_no_collabo.id,
|
||||
"message": '(-b ±√[b²-4ac]) / 2a',
|
||||
"attachment_ids": None,
|
||||
"attachment_tokens": None,
|
||||
"thread_model": self.task_no_collabo._name,
|
||||
"thread_id": self.task_no_collabo.id,
|
||||
"post_data": {'body': '(-b ±√[b²-4ac]) / 2a'},
|
||||
"token": access_token,
|
||||
"pid": pid,
|
||||
"hash": _hash,
|
||||
|
|
|
|||
|
|
@ -3,28 +3,27 @@
|
|||
from odoo import Command
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProjectSharingUi(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
user = cls.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nolog': True}).create({
|
||||
cls.user_portal = cls.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nolog': True}).create({
|
||||
'name': 'Georges',
|
||||
'login': 'georges1',
|
||||
'password': 'georges1',
|
||||
'email': 'georges@project.portal',
|
||||
'signature': 'SignGeorges',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [Command.set([cls.env.ref('base.group_portal').id])],
|
||||
'group_ids': [Command.set([cls.env.ref('base.group_portal').id, cls.env.ref('project.group_project_milestone').id])],
|
||||
})
|
||||
|
||||
cls.partner_portal = cls.env['res.partner'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Georges',
|
||||
'email': 'georges@project.portal',
|
||||
'company_id': False,
|
||||
'user_ids': [user.id],
|
||||
'user_ids': [cls.user_portal.id],
|
||||
})
|
||||
cls.project_portal = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Project Sharing',
|
||||
|
|
@ -35,12 +34,61 @@ class TestProjectSharingUi(HttpCase):
|
|||
Command.create({'name': 'To Do', 'sequence': 1}),
|
||||
Command.create({'name': 'Done', 'sequence': 10})
|
||||
],
|
||||
'allow_milestones': True,
|
||||
})
|
||||
cls.env['res.config.settings'].create({'group_project_milestone': True}).execute()
|
||||
cls.env.user.group_ids |= cls.env.ref('project.group_project_milestone')
|
||||
|
||||
def test_blocked_task_with_project_sharing_string_portal(self):
|
||||
"""
|
||||
Ensure the portal user shows the message 'This task is currently blocked...'.
|
||||
Flow:
|
||||
- Activated Task Dependencies in a portal project
|
||||
- Create a 'New' task stage
|
||||
- Create a project(Test Project)
|
||||
- Ensure the portal user receives the message 'This task is currently blocked..'.
|
||||
- Create task(Test Task)
|
||||
- Create a task with a Blocked task (Test Task)
|
||||
"""
|
||||
|
||||
self.project_portal.write({
|
||||
'allow_task_dependencies': True,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.partner_portal.id}),
|
||||
],
|
||||
})
|
||||
|
||||
project = self.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Project',
|
||||
})
|
||||
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_portal.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.partner_portal.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
|
||||
task = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Task',
|
||||
'project_id': project.id,
|
||||
})
|
||||
|
||||
self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Portal Task',
|
||||
'project_id': self.project_portal.id,
|
||||
'depend_on_ids': task.ids,
|
||||
'stage_id': self.project_portal.type_ids[0].id,
|
||||
})
|
||||
|
||||
self.start_tour("/odoo", 'project_sharing_with_blocked_task_tour', login="georges1")
|
||||
|
||||
def test_01_project_sharing(self):
|
||||
""" Test Project Sharing UI with an internal user """
|
||||
self.start_tour("/web", 'project_sharing_tour', login="admin")
|
||||
self.env.ref('base.user_admin').write({
|
||||
'email': 'mitchell.admin@example.com',
|
||||
})
|
||||
self.start_tour("/odoo", 'project_sharing_tour', login="admin")
|
||||
|
||||
def test_02_project_sharing(self):
|
||||
""" Test project sharing ui with a portal user.
|
||||
|
|
@ -50,15 +98,13 @@ class TestProjectSharingUi(HttpCase):
|
|||
Since a problem to logout Mitchell Admin to log in as Georges user, this test is created
|
||||
to launch a tour with portal user.
|
||||
"""
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'access_mode': 'edit',
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_portal.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.partner_portal.id),
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.partner_portal.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
self.project_portal.write({
|
||||
'task_ids': [Command.create({
|
||||
|
|
@ -67,3 +113,76 @@ class TestProjectSharingUi(HttpCase):
|
|||
})],
|
||||
})
|
||||
self.start_tour("/my/projects", 'portal_project_sharing_tour', login='georges1')
|
||||
|
||||
def test_03_project_sharing(self):
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_portal.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.partner_portal.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
|
||||
self.project_portal.write({
|
||||
'task_ids': [Command.create({
|
||||
'name': "Test Project Sharing",
|
||||
'stage_id': self.project_portal.type_ids.filtered(lambda stage: stage.sequence == 10)[:1].id,
|
||||
})],
|
||||
'allow_milestones': False,
|
||||
})
|
||||
self.start_tour("/my/projects", 'portal_project_sharing_tour_with_disallowed_milestones', login='georges1')
|
||||
|
||||
def test_04_project_sharing_chatter_message_reactions(self):
|
||||
# portal users can load chatter messages containing partner reactions
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_portal.id,
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.partner_portal.id, 'access_mode': 'edit'}),
|
||||
],
|
||||
})
|
||||
user_john = self.env["res.users"].create({
|
||||
'name': 'John',
|
||||
'login': 'john',
|
||||
'password': 'john1234',
|
||||
'email': 'john@example.com',
|
||||
'group_ids': [Command.set([
|
||||
self.env.ref('base.group_user').id,
|
||||
self.env.ref('project.group_project_user').id
|
||||
])]
|
||||
})
|
||||
task = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Task with messages',
|
||||
'project_id': self.project_portal.id,
|
||||
})
|
||||
self.authenticate("georges1", "georges1")
|
||||
message = task.message_post(
|
||||
body='TestingMessage',
|
||||
message_type="comment",
|
||||
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
|
||||
)
|
||||
self.authenticate("john", "john")
|
||||
self.project_portal.message_subscribe(partner_ids=[user_john.partner_id.id])
|
||||
self.make_jsonrpc_request(
|
||||
route="/mail/message/reaction",
|
||||
params={"action": "add", "content": "👀", "message_id": message.id},
|
||||
)
|
||||
self.start_tour("/my/projects", 'test_04_project_sharing_chatter_message_reactions', login='georges1')
|
||||
|
||||
def test_05_project_sharing_chatter_mention_users(self):
|
||||
self.env["project.share.wizard"].create(
|
||||
{
|
||||
"res_model": "project.project",
|
||||
"res_id": self.project_portal.id,
|
||||
"collaborator_ids": [
|
||||
Command.create({"partner_id": self.partner_portal.id, "access_mode": "edit"}),
|
||||
],
|
||||
}
|
||||
)
|
||||
self.env["project.task"].with_context({"mail_create_nolog": True}).create(
|
||||
{
|
||||
"name": "Test Task",
|
||||
"project_id": self.project_portal.id,
|
||||
}
|
||||
)
|
||||
self.start_tour("/my/projects", "portal_project_sharing_chatter_mention_users", login="georges1")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
from .test_multicompany import TestMultiCompanyProject
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestProjectStagesMulticompany(TestMultiCompanyProject):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestProjectStagesMulticompany, cls).setUpClass()
|
||||
|
||||
Users = cls.env['res.users'].with_context({'no_reset_password': True})
|
||||
cls.user_manager_companies = Users.create({
|
||||
'name': 'Manager Companies',
|
||||
'login': 'manager-all',
|
||||
'email': 'manager@companies.com',
|
||||
'company_id': cls.company_a.id,
|
||||
'company_ids': [(4, cls.company_a.id), (4, cls.company_b.id)],
|
||||
'group_ids':
|
||||
[(6, 0, [
|
||||
cls.env.ref('base.group_user').id,
|
||||
cls.env.ref('project.group_project_stages').id,
|
||||
cls.env.ref('project.group_project_manager').id,
|
||||
])]
|
||||
})
|
||||
cls.stage_company_a, cls.stage_company_b, cls.stage_company_none = cls.env['project.project.stage'].create([{
|
||||
'name': 'Stage Company A',
|
||||
'company_id': cls.company_a.id,
|
||||
}, {
|
||||
'name': 'Stage Company B',
|
||||
'company_id': cls.company_b.id,
|
||||
}, {
|
||||
'name': 'Stage Company None',
|
||||
}])
|
||||
cls.project_company_none = cls.env['project.project'].create({
|
||||
'name': 'Project Company None'
|
||||
})
|
||||
|
||||
def test_move_linked_project_stage_other_company(self):
|
||||
""" This test will check that an error is raised if a project belonging to a stage
|
||||
(both linked to company A) is moved to another stage (belonging to company B) """
|
||||
self.project_company_a.stage_id = self.stage_company_a.id
|
||||
with self.assertRaises(UserError):
|
||||
self.project_company_a.stage_id = self.stage_company_b.id
|
||||
|
||||
def test_move_project_stage_other_company(self):
|
||||
""" This test will check that an error is raised a project belonging to a stage (both
|
||||
not linked to any company) is moved to another stage (belonging to company B) """
|
||||
self.project_company_none.stage_id = self.stage_company_none.id
|
||||
with self.assertRaises(UserError):
|
||||
self.project_company_none.stage_id = self.stage_company_b.id,
|
||||
|
||||
def test_move_linked_project_stage_same_company(self):
|
||||
""" This test will check that no error is raised if a project belonging to a stage (with
|
||||
only the project belonging to company B and the stage not linked to any company) is moved
|
||||
to another stage (belonging to company B) """
|
||||
self.project_company_b.stage_id = self.stage_company_none.id
|
||||
self.project_company_b.stage_id = self.stage_company_b.id
|
||||
|
||||
def test_move_project_stage_same_company(self):
|
||||
""" This test will check that no error is raised if a project belonging to a stage (both
|
||||
linked to company A) is moved to another stage (also belonging to company A) """
|
||||
self.project_company_a.stage_id = self.stage_company_a.id
|
||||
self.stage_company_none.company_id = self.company_a.id
|
||||
self.project_company_a.stage_id = self.stage_company_none.id
|
||||
|
||||
def test_change_project_company(self):
|
||||
""" This test will check that a project's stage is changed according to the
|
||||
company it is linked to. When a project (belonging to a stage with both the
|
||||
project and the stage linked to company A) changes company for company B,
|
||||
the stage should change for the lowest stage in sequence that is linked to
|
||||
company B. If there is no stage linked to company B, then the lowest stage
|
||||
in sequence linked to no company will be chosen """
|
||||
project = self.project_company_a.with_user(self.user_manager_companies)
|
||||
project.stage_id = self.stage_company_a.id
|
||||
project.company_id = self.company_b.id
|
||||
|
||||
# Check that project was moved to stage_company_b
|
||||
self.assertFalse(self.project_company_a.stage_id.company_id, "Project Company A should now be in a stage without company")
|
||||
|
||||
def test_project_creation_default_stage(self):
|
||||
"""
|
||||
Check that when creating a project with a company set, the default stage
|
||||
for this project has the same company as the project or no company.
|
||||
If no company is set on the project, the first stage without a company
|
||||
should be chosen.
|
||||
"""
|
||||
# Stage order: company A, company B, no company
|
||||
self.stage_company_a.sequence = 1
|
||||
self.stage_company_b.sequence = 3
|
||||
|
||||
project_company_b = self.env['project.project'].with_user(self.user_manager_companies).create({
|
||||
'name': 'Project company B',
|
||||
'company_id': self.company_b.id,
|
||||
})
|
||||
self.assertEqual(project_company_b.company_id, self.company_b)
|
||||
self.assertEqual(project_company_b.stage_id, self.stage_company_b)
|
||||
|
||||
# Stage order: company A, no company, company B
|
||||
self.stage_company_none.sequence = 2
|
||||
|
||||
project_company_b = self.env['project.project'].with_user(self.user_manager_companies).create({
|
||||
'name': 'Project company B',
|
||||
'company_id': self.company_b.id,
|
||||
})
|
||||
self.assertEqual(project_company_b.company_id, self.company_b)
|
||||
self.assertEqual(project_company_b.stage_id, self.stage_company_none)
|
||||
|
||||
project_no_company = self.env['project.project'].with_user(self.user_manager_companies).create({
|
||||
'name': 'Project no company',
|
||||
})
|
||||
self.assertFalse(project_no_company.company_id)
|
||||
self.assertEqual(project_no_company.stage_id, self.stage_company_none)
|
||||
|
||||
self.env['project.project.stage'].search([]).active = False
|
||||
project_no_company = self.env['project.project'].with_user(self.user_manager_companies).create({
|
||||
'name': 'Project no company',
|
||||
})
|
||||
self.assertFalse(project_no_company.stage_id)
|
||||
|
||||
def test_project_creation_default_stage_in_context(self):
|
||||
"""
|
||||
Project's company should be the same as the default stage's company in the context.
|
||||
"""
|
||||
project = self.env['project.project'].with_user(self.user_manager_companies).with_context(default_stage_id=self.stage_company_b.id).create({
|
||||
'name': 'Project company B',
|
||||
})
|
||||
self.assertEqual(project.company_id, self.company_b)
|
||||
|
||||
def test_create_project_in_stage(self):
|
||||
""" Test create project inside a specific stage """
|
||||
self.env['res.config.settings'].create({'group_project_stages': True}).execute()
|
||||
stage = self.env['project.project.stage'].create({
|
||||
'name': 'Stage 2',
|
||||
'sequence': 100,
|
||||
})
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project in stage 2',
|
||||
'stage_id': stage.id,
|
||||
})
|
||||
self.assertEqual(project.stage_id, stage)
|
||||
|
|
@ -1,22 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from lxml import etree
|
||||
from psycopg2.errors import CheckViolation
|
||||
|
||||
from odoo import Command, _
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectSubtasks(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Enable the company setting
|
||||
cls.env['res.config.settings'].create({
|
||||
'group_subtask_project': True
|
||||
}).execute()
|
||||
|
||||
def test_task_display_project_with_default_form(self):
|
||||
"""
|
||||
Create a task in the default task form should take the project set in the form or the default project in the context
|
||||
|
|
@ -27,14 +23,12 @@ class TestProjectSubtasks(TestProjectCommon):
|
|||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id})) as task_form:
|
||||
task_form.name = 'Test Task 2'
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
def test_task_display_project_with_task_form2(self):
|
||||
"""
|
||||
|
|
@ -46,25 +40,22 @@ class TestProjectSubtasks(TestProjectCommon):
|
|||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id}), view="project.view_task_form2") as task_form:
|
||||
task_form.name = 'Test Task 2'
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
def test_task_display_project_with_quick_create_task_form(self):
|
||||
"""
|
||||
Create a task in the quick create form should take the default project in the context
|
||||
"""
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id}), view="project.quick_create_task_form") as task_form:
|
||||
task_form.name = 'Test Task 2'
|
||||
task_form = Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id}), view="project.quick_create_task_form")
|
||||
task_form.display_name = 'Test Task 2'
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
def test_task_display_project_with_any_task_form(self):
|
||||
"""
|
||||
|
|
@ -72,172 +63,181 @@ class TestProjectSubtasks(TestProjectCommon):
|
|||
"""
|
||||
form_views = self.env['ir.ui.view'].search([('model', '=', 'project.task'), ('type', '=', 'form')])
|
||||
for form_view in form_views:
|
||||
task_form = Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id, 'default_name': 'Test Task 1'}), view=form_view)
|
||||
task_form = Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id, 'default_name': 'Test Task 1', 'default_display_name': 'Test Task 1'}), view=form_view)
|
||||
# Some views have the `name` field invisible
|
||||
# As the goal is simply to test the default project field and not the name, we can skip setting the name
|
||||
# in the view and set it using `default_name` instead
|
||||
# Quick create form use display_name and for the same goal, we can add default_display_name for that form
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned from the default project, form_view name : %s." % form_view.name)
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id, form_view name : %s." % form_view.name)
|
||||
|
||||
def test_subtask_display_project(self):
|
||||
@mute_logger('odoo.sql_db')
|
||||
def test_subtask_project(self):
|
||||
"""
|
||||
1) Create a subtask
|
||||
- Should have the same project as its parent
|
||||
- Shouldn't have a display project set.
|
||||
2) Set display project on subtask
|
||||
- Should have a project set
|
||||
- Shouldn't be displayed
|
||||
2) Set project on subtask
|
||||
- Should not change parent project
|
||||
- Should change the subtask project
|
||||
- Display project should be correct
|
||||
3) Reset the display project to False
|
||||
- Should make the project equal to parent project
|
||||
- Display project should be correct
|
||||
4) Change parent task project
|
||||
- Should make the subtask project follow parent project
|
||||
- Display project should stay false
|
||||
5) Set display project on subtask and change parent task project
|
||||
- Should make the subtask project follow new display project id
|
||||
- Display project should be correct
|
||||
- Project should be correct
|
||||
- Should be displayed
|
||||
3) Reset the project to False
|
||||
- Should raise an error
|
||||
3bis) Reset the parent task project to False
|
||||
- Should raise an error
|
||||
4) Set project on parent to same project as subtask
|
||||
- Project should be correct
|
||||
- Shouldn't change subtask's display
|
||||
5) Set project on subtask and change parent task project
|
||||
- Project should be the one set by the user
|
||||
6) Remove parent task:
|
||||
- The project id should remain unchanged
|
||||
- The display project id should follow the project id
|
||||
7) Remove display project id then parent id:
|
||||
- The project id should be the one from the parent :
|
||||
- Since the display project id was removed, the project id is updated to the parent one
|
||||
- The display project id should follow the project id
|
||||
7) Remove project id then parent id:
|
||||
- Project should be removed
|
||||
- Parent should be removed
|
||||
"""
|
||||
# 1)
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
parent_task = self.task_1.with_context({'tracking_disable': True})
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertFalse(self.task_1.child_ids.display_project_id, "The display project of a sub task should be false to project_id.")
|
||||
# 1)
|
||||
child_task = parent_task.create({
|
||||
'name': 'Test Subtask 1',
|
||||
'parent_id': parent_task.id,
|
||||
'project_id': parent_task.project_id.id,
|
||||
}).with_context({'tracking_disable': True})
|
||||
self.assertEqual(child_task.project_id, self.task_1.project_id, "The project should be inheritted from parent.")
|
||||
self.assertFalse(child_task.display_in_project, "By default, subtasks shouldn't be displayed in project.")
|
||||
|
||||
# 2)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
child_task.project_id = self.project_goats
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs, "Changing the project of a subtask should not change parent project")
|
||||
self.assertEqual(self.task_1.child_ids.display_project_id, self.project_goats, "Display Project of the task should be well assigned")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_goats, "Changing display project id on a subtask should change project id")
|
||||
self.assertEqual(child_task.project_id, self.project_goats, "Display Project of the task should be well assigned")
|
||||
self.assertTrue(child_task.display_in_project, "As the subtask isn't in the same project as its parent, it should be displayed")
|
||||
|
||||
# 3)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
with self.assertRaises(CheckViolation):
|
||||
child_task.project_id = False
|
||||
|
||||
self.assertFalse(self.task_1.child_ids.display_project_id, "Display Project of the task should be well assigned, to False")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_pigs, "Resetting display project to False on a subtask should change project id to parent project id")
|
||||
# 3bis)
|
||||
with self.assertRaises(ValidationError):
|
||||
parent_task.project_id = False
|
||||
|
||||
# 4)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.project_id = self.project_goats
|
||||
|
||||
self.assertEqual(self.task_1.project_id, self.project_goats, "Parent project should change.")
|
||||
self.assertFalse(self.task_1.child_ids.display_project_id, "Display Project of the task should be False")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_goats, "Resetting display project to False on a subtask should follow project of its parent")
|
||||
parent_task.project_id = self.task_1.child_ids.project_id
|
||||
self.assertEqual(self.task_1.project_id, self.project_goats, "Parent project should change")
|
||||
self.assertEqual(child_task.project_id, self.project_goats, "Child project should change")
|
||||
self.assertTrue(child_task.display_in_project, "Changing the project of the task shouldn't change de value of display_in_project of its subtask")
|
||||
|
||||
# 5)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
task_form.project_id = self.project_pigs
|
||||
parent_task.write({
|
||||
'project_id': self.project_pigs.id,
|
||||
'child_ids': [(1, parent_task.child_ids[0].id, {
|
||||
'project_id': self.project_goats.id
|
||||
})],
|
||||
})
|
||||
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs, "Parent project should change back.")
|
||||
self.assertEqual(self.task_1.child_ids.display_project_id, self.project_goats, "Display Project of the task should be well assigned")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_goats, "Changing display project id on a subtask should change project id")
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs, "Parent project should change back")
|
||||
self.assertEqual(child_task.project_id, self.project_goats, "The project of the subtask should have the one set by the user")
|
||||
|
||||
# Debug mode required for `parent_id` to be visible in the view
|
||||
with self.debug_mode():
|
||||
# 6)
|
||||
with Form(self.task_1.child_ids.with_context({'tracking_disable': True})) as subtask_form:
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
orphan_subtask = subtask_form.save()
|
||||
# 6)
|
||||
child_task.parent_id = False
|
||||
self.assertEqual(child_task.project_id, self.project_goats, "The project of the orphan task should stay the same even if it no longer has a parent task")
|
||||
self.assertFalse(child_task.parent_id, "Parent should be false")
|
||||
|
||||
self.assertEqual(orphan_subtask.display_project_id, self.project_goats, "Display Project of the task should be well assigned")
|
||||
self.assertEqual(orphan_subtask.project_id, self.project_goats, "Changing display project id on a subtask should change project id")
|
||||
self.assertFalse(orphan_subtask.parent_id, "Parent should be false")
|
||||
|
||||
# 7)
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
with Form(self.task_1.child_ids.with_context({'tracking_disable': True})) as subtask_form:
|
||||
subtask_form.display_project_id = self.env['project.project']
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
orphan_subtask = subtask_form.save()
|
||||
|
||||
self.assertEqual(orphan_subtask.project_id, self.project_pigs, "Removing parent should not change project")
|
||||
self.assertEqual(orphan_subtask.display_project_id, self.project_pigs, "Removing parent should make the display project set as project.")
|
||||
# 7)
|
||||
other_child_task = parent_task.create({
|
||||
'name': 'Test Subtask 1',
|
||||
'parent_id': parent_task.id,
|
||||
'project_id': self.project_goats.id,
|
||||
})
|
||||
other_child_task.write({
|
||||
'project_id': False,
|
||||
'parent_id': False,
|
||||
})
|
||||
self.assertFalse(other_child_task.project_id, "The project should be removed as expected")
|
||||
self.assertFalse(other_child_task.parent_id, "The parent should be removed as expected")
|
||||
|
||||
def test_subtask_stage(self):
|
||||
"""
|
||||
The stage of the new child must be the default one of the project
|
||||
"""
|
||||
parent_task = self.task_1.with_context({'tracking_disable': True})
|
||||
|
||||
stage_a = self.env['project.task.type'].create({'name': 'a', 'sequence': 1})
|
||||
stage_b = self.env['project.task.type'].create({'name': 'b', 'sequence': 10})
|
||||
self.project_pigs.type_ids |= stage_a
|
||||
self.project_pigs.type_ids |= stage_b
|
||||
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
child_task = parent_task.create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
'parent_id': parent_task.id,
|
||||
'project_id': parent_task.project_id.id,
|
||||
}).with_context({'tracking_disable': True})
|
||||
self.assertEqual(child_task.stage_id, stage_a, "Stage should be set on the subtask since it inheritted the project of its parent.")
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should be the default one of the project.")
|
||||
child_task.project_id = parent_task.project_id
|
||||
self.assertEqual(child_task.stage_id, stage_a, "The stage of the child task should be the default one of the project.")
|
||||
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.stage_id = stage_b
|
||||
parent_task.stage_id = stage_b
|
||||
self.assertEqual(child_task.stage_id, stage_a, "The stage of the child task should remain the same while changing parent task stage.")
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should remain the same while changing parent task stage.")
|
||||
|
||||
test_subtask_2 = self.env['project.task'].create({
|
||||
parent_task.child_ids = False
|
||||
other_child_task = parent_task.create({
|
||||
'name': 'Test Subtask 2',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.remove(test_subtask_1.id)
|
||||
task_form.child_ids.add(test_subtask_2)
|
||||
'parent_id': parent_task.id,
|
||||
'project_id': parent_task.project_id.id,
|
||||
}).with_context({'tracking_disable': True})
|
||||
self.assertEqual(other_child_task.stage_id, stage_a, "The stage of the child task should be the default one of the project even if parent stage id is different.")
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should be the default one of the project even if parent stage id is different.")
|
||||
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id.name, "New", "The stage of the child task should be the default one of the display project id, once set.")
|
||||
other_child_task.project_id = self.project_goats
|
||||
self.assertEqual(other_child_task.stage_id.name, "New", "The stage of the child task should be the default one of the display project id, once set.")
|
||||
|
||||
def test_copy_project_with_subtasks(self):
|
||||
self.project_goats.allow_subtasks = True
|
||||
self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({'name': 'child 1', 'stage_id': self.project_goats.type_ids[0].id}),
|
||||
Command.create({'name': 'child 2', 'display_project_id': self.project_goats.id}),
|
||||
Command.create({'name': 'child 3', 'display_project_id': self.project_pigs.id}),
|
||||
Command.create({'name': 'child 4 with subtask', 'child_ids': [Command.create({'name': 'child 5'}), Command.create({'name': 'child 6 with project', 'display_project_id': self.project_goats.id})]}),
|
||||
Command.create({'name': 'child archived', 'active': False}),
|
||||
Command.create({'name': 'child 1', 'project_id': self.project_goats.id}),
|
||||
Command.create({'name': 'child 2', 'project_id': self.project_pigs.id}),
|
||||
Command.create({
|
||||
'name': 'child 3 with subtask',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({'name': 'granchild 3.1', 'project_id': self.project_goats.id}),
|
||||
]}),
|
||||
Command.create({'name': 'child archived', 'project_id': self.project_goats.id, 'active': False}),
|
||||
],
|
||||
'stage_id': self.project_goats.type_ids[0].id
|
||||
})
|
||||
task_count_with_subtasks_including_archived_in_project_goats = self.project_goats.with_context(active_test=False).task_count_with_subtasks
|
||||
|
||||
task_count_with_subtasks_including_archived = 6
|
||||
task_count_in_project_pigs = self.project_pigs.task_count
|
||||
self.project_goats._compute_task_count() # recompute without archived tasks and subtasks
|
||||
task_count_in_project_goats = self.project_goats.task_count
|
||||
project_goats_duplicated = self.project_goats.copy()
|
||||
self.project_pigs._compute_task_count() # retrigger since a new task should be added in the project after the duplication of Project Goats
|
||||
|
||||
def dfs(task):
|
||||
# ABGH: i used dfs to avoid visiting a task 2 times as it can be a direct task for the project and a subtask for another task like child 6
|
||||
visited[task.id] = True
|
||||
total_count = 1
|
||||
for child_id in task.child_ids:
|
||||
if child_id.id not in visited:
|
||||
total_count += dfs(child_id)
|
||||
return total_count
|
||||
|
||||
visited = {}
|
||||
tasks_copied_count = 0
|
||||
for task in project_goats_duplicated.tasks:
|
||||
if not task.id in visited:
|
||||
tasks_copied_count += dfs(task)
|
||||
|
||||
self.assertEqual(
|
||||
project_goats_duplicated.with_context(active_test=False).task_count_with_subtasks,
|
||||
task_count_with_subtasks_including_archived_in_project_goats - 1,
|
||||
tasks_copied_count,
|
||||
task_count_with_subtasks_including_archived - 1,
|
||||
'The number of duplicated tasks (subtasks included) should be equal to the number of all tasks (with active subtasks included) of both projects, '
|
||||
'that is only the active subtasks are duplicated.')
|
||||
self.assertEqual(self.project_goats.task_count, task_count_in_project_goats, 'The number of tasks should be the same before and after the duplication of this project.')
|
||||
self.assertEqual(self.project_pigs.task_count, task_count_in_project_pigs + 1, 'The project pigs should an additional task after the duplication of the project goats.')
|
||||
self.assertEqual(project_goats_duplicated.tasks[0].child_ids[0].stage_id.id, self.project_goats.type_ids[0].id, 'The stage of subtasks should be copied too.')
|
||||
|
||||
def test_subtask_creation_with_form(self):
|
||||
"""
|
||||
|
|
@ -246,25 +246,388 @@ class TestProjectSubtasks(TestProjectCommon):
|
|||
3) test the creation of sub-sub-tasks
|
||||
4) check the correct nb of sub-tasks is displayed in the 'sub-tasks' stat button and on the parent task kanban card
|
||||
5) sub-tasks should be copied when the parent task is duplicated
|
||||
6) verify if there is a copy in the subtask name.
|
||||
"""
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
|
||||
task_form = Form(self.task_1.with_context({'tracking_disable': True}))
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
task_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 1'
|
||||
child_task_form.project_id = task_form.project_id
|
||||
task = task_form.save()
|
||||
|
||||
child_subtask = self.task_1.child_ids[0]
|
||||
test_subtask_2 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 2',
|
||||
|
||||
with (
|
||||
Form(child_subtask.with_context(tracking_disable=True)) as subtask_form,
|
||||
subtask_form.child_ids.new() as child_subtask_form,
|
||||
):
|
||||
child_subtask_form.name = 'Test Subtask 2'
|
||||
self.assertEqual(child_subtask_form.project_id, subtask_form.project_id)
|
||||
self.assertFalse(child_subtask_form.display_in_project)
|
||||
|
||||
self.assertEqual(task.subtask_count, 1, "Parent task should have 1 children")
|
||||
task_2 = task.copy()
|
||||
self.assertEqual(task_2.subtask_count, 1, "If the parent task is duplicated then the sub task should be copied")
|
||||
self.assertEqual(task_2.child_ids[0].name, "Test Subtask 1 (copy)", "The name of the subtask should contain the word 'copy'.")
|
||||
|
||||
def test_subtask_copy_display_in_project(self):
|
||||
"""
|
||||
Check if `display_in_project` of subtask is not set to `True` during copy
|
||||
"""
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project',
|
||||
})
|
||||
task_A, task_B = self.env['project.task'].create([
|
||||
{
|
||||
'name': 'Task A',
|
||||
'project_id': project.id,
|
||||
},
|
||||
{
|
||||
'name': 'Task B',
|
||||
'project_id': project.id,
|
||||
},
|
||||
])
|
||||
self.env['project.task'].create([
|
||||
{
|
||||
'name': 'Subtask A 1',
|
||||
'parent_id': task_A.id,
|
||||
'project_id': project.id,
|
||||
},
|
||||
{
|
||||
'name': 'Subtask A 2',
|
||||
'parent_id': task_A.id,
|
||||
'project_id': project.id,
|
||||
},
|
||||
{
|
||||
'name': 'Subtask B 1',
|
||||
'parent_id': task_B.id,
|
||||
'project_id': project.id,
|
||||
},
|
||||
{
|
||||
'name': 'Subtask B 2',
|
||||
'parent_id': task_B.id,
|
||||
'project_id': project.id,
|
||||
}
|
||||
])
|
||||
subtask_not_display_in_project = project.task_ids.child_ids.filtered(lambda t: not t.display_in_project)
|
||||
self.assertEqual(len(subtask_not_display_in_project), 4, "No subtask should be displayed in the project")
|
||||
project_copy = project.copy()
|
||||
self.assertEqual(len(project_copy.task_ids.child_ids), 4)
|
||||
subtask_not_display_in_project_copy = project_copy.task_ids.child_ids.filtered(lambda t: not t.display_in_project)
|
||||
self.assertEqual(len(subtask_not_display_in_project_copy), 4, "No subtask should be displayed in the duplicate project")
|
||||
|
||||
def test_subtask_copy_name(self):
|
||||
""" This test ensure that the name of task and project have the '(copy)' added to their name when needed.
|
||||
If a project is copied, the project's name should contain the 'copy' but the project's task should keep the same name as their original.
|
||||
If a task is copied (alone or in a recordset), its name as well as the name of its children should contain the 'copy'.
|
||||
"""
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project',
|
||||
})
|
||||
task_A = self.env['project.task'].create({
|
||||
'name': 'Task A',
|
||||
'project_id': project.id,
|
||||
'child_ids': [Command.create({
|
||||
'name': 'Subtask A 1',
|
||||
'project_id': project.id,
|
||||
'child_ids': [Command.create({
|
||||
'name': 'Sub Subtask A 1',
|
||||
'project_id': project.id,
|
||||
})]
|
||||
}), Command.create({
|
||||
'name': 'Subtask A 2',
|
||||
'project_id': project.id,
|
||||
})]
|
||||
})
|
||||
project_copied = project.copy()
|
||||
self.assertEqual(project_copied.name, 'Project (copy)', 'The name of the project should contains the extra (copy).')
|
||||
parent_task = self.env['project.task'].search([('project_id', '=', project_copied.id), ('parent_id', '=', False)])
|
||||
self.assertEqual(parent_task.name, 'Task A', 'The task is copied from project.copy(). Its name should be the same.')
|
||||
self.assertEqual(parent_task.child_ids[0].name, 'Subtask A 1', 'The task is copied from project.copy(). Its name should be the same.')
|
||||
self.assertEqual(parent_task.child_ids[1].name, 'Subtask A 2', 'The task is copied from project.copy(). Its name should be the same.')
|
||||
self.assertEqual(parent_task.child_ids[0].child_ids.name, 'Sub Subtask A 1', 'The task is copied from project.copy(). Its name should be the same.')
|
||||
copied_task = task_A.copy()
|
||||
self.assertEqual(copied_task.name, 'Task A (copy)', 'The task is copied from task.copy(). Its name should contain the extra (copy).')
|
||||
self.assertEqual(copied_task.child_ids[0].name, 'Subtask A 1 (copy)', 'The task is copied from task.copy(). Its name should contain the extra (copy).')
|
||||
self.assertEqual(copied_task.child_ids[1].name, 'Subtask A 2 (copy)', 'The task is copied from task.copy(). Its name should contain the extra (copy).')
|
||||
self.assertEqual(copied_task.child_ids[0].child_ids.name, 'Sub Subtask A 1 (copy)', 'The task is copied from task.copy(). Its name should contain the extra (copy).')
|
||||
|
||||
def test_subtask_unlinking(self):
|
||||
task_form = Form(self.task_1.with_context({'tracking_disable': True}))
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 1'
|
||||
child_task_form.project_id = task_form.project_id
|
||||
task_form.save()
|
||||
child_subtask = self.task_1.child_ids[0]
|
||||
self.task_1.unlink()
|
||||
|
||||
self.assertFalse(self.task_1.exists())
|
||||
self.assertFalse(child_subtask.exists(), 'Subtask should be removed if the parent task has been deleted')
|
||||
|
||||
def test_get_all_subtasks(self):
|
||||
subsubtasks = self.env['project.task'].create([{
|
||||
'name': 'Subsubtask 1',
|
||||
'project_id': self.project_pigs.id,
|
||||
}, {
|
||||
'name': 'Subsubtask 2',
|
||||
'project_id': self.project_goats.id,
|
||||
}, {
|
||||
'name': 'Subsubtask 3',
|
||||
'project_id': self.project_pigs.id,
|
||||
}])
|
||||
subtasks = self.env['project.task'].create([{
|
||||
'name': 'Subtask 1',
|
||||
'project_id': self.project_pigs.id,
|
||||
'child_ids': subsubtasks[:2],
|
||||
}, {
|
||||
'name': 'Subtask 2',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': subsubtasks[2],
|
||||
}])
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task 1',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': subtasks,
|
||||
})
|
||||
|
||||
with Form(child_subtask.with_context(tracking_disable=True)) as subtask_form:
|
||||
subtask_form.child_ids.add(test_subtask_2)
|
||||
subtask_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
all_subtasks = task._get_all_subtasks()
|
||||
self.assertEqual(all_subtasks, subtasks | subsubtasks)
|
||||
|
||||
self.assertEqual(task.subtask_count, 2, "Parent task should have 2 children")
|
||||
task_2 = task.copy()
|
||||
self.assertEqual(task_2.subtask_count, 2, "If the parent task is duplicated then the sub task should be copied")
|
||||
all_subtasks_by_task_id = task._get_subtask_ids_per_task_id()
|
||||
self.assertEqual(len(all_subtasks_by_task_id), 1, "The result should only contain one item: the common ancestor")
|
||||
for parent_id, subtask_ids in all_subtasks_by_task_id.items():
|
||||
self.assertEqual(parent_id, task.id, "The key should be the common ancestor")
|
||||
self.assertEqual(set(subtask_ids), set(all_subtasks.ids),
|
||||
"All subtasks linked to the common ancestor should be returned by _get_subtask_ids_per_task_id method")
|
||||
|
||||
def test_subtask_copy_followers(self):
|
||||
""" This test will check that a task will propagate its followers to its subtasks """
|
||||
task_form = Form(self.task_1.with_context({'tracking_disable': True}))
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Child Task'
|
||||
child_task_form.project_id = task_form.project_id
|
||||
task = task_form.save()
|
||||
self.assertEqual(task.message_follower_ids.mapped('email'), task.child_ids[0].message_follower_ids.mapped('email'), "The parent and child message_follower_ids should have the same emails")
|
||||
|
||||
def test_toggle_active_task_with_subtasks(self):
|
||||
""" This test will check archiving task should archive it's subtasks and vice versa """
|
||||
parent_task = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({
|
||||
'name': 'child 1',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({
|
||||
'name': 'Child 1 (Subtask 1)',
|
||||
'project_id': self.project_goats.id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Child 1 (Subtask 2)',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [Command.create({
|
||||
'name': 'Subsubtask',
|
||||
'project_id': self.project_goats.id,
|
||||
})],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'child 2',
|
||||
'project_id': self.project_goats.id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'child 3',
|
||||
'project_id': self.project_pigs.id,
|
||||
'child_ids': [
|
||||
Command.create({
|
||||
'name': 'Child 3 (Subtask 1)',
|
||||
'project_id': self.project_pigs.id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Child 3 (Subtask 2)',
|
||||
'project_id': self.project_pigs.id,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'child 4',
|
||||
'project_id': self.project_pigs.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
child_1, child_2, child_3, child_4 = parent_task.child_ids
|
||||
self.assertEqual(9, len(parent_task._get_all_subtasks()), "Should have 9 subtasks")
|
||||
parent_task.action_archive()
|
||||
self.assertFalse(all((parent_task + child_1._get_all_subtasks() + child_2).mapped('active')),
|
||||
"Parent, `child 1` task (with its descendant tasks) and `Child 2` task should be archived")
|
||||
self.assertTrue(all(child_3._get_all_subtasks().mapped('active')), "`child 3` task and its descendant tasks should be unarchived")
|
||||
self.assertEqual(2, len(parent_task.child_ids), "Should have 2 direct non archived subtasks")
|
||||
self.assertEqual(parent_task.child_ids, child_3 + child_4, "Should have 2 direct non archived subtasks")
|
||||
self.assertEqual(4, len(parent_task._get_all_subtasks().filtered('active')), "Should have 4 non archived subtasks")
|
||||
|
||||
def test_display_in_project_unset_parent(self):
|
||||
""" Test _onchange_parent_id when there is no parent task
|
||||
"""
|
||||
Task = self.env['project.task']
|
||||
task = Task.create({
|
||||
'name': 'Task',
|
||||
'parent_id': self.task_1.id,
|
||||
'project_id': self.task_1.project_id.id,
|
||||
})
|
||||
view = self.env.ref('project.view_task_form2')
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="parent_id"][@invisible]'):
|
||||
node.attrib.pop('invisible')
|
||||
view.arch = etree.tostring(tree)
|
||||
with Form(task) as task_form:
|
||||
task_form.parent_id = Task
|
||||
task._compute_display_in_project()
|
||||
self.assertEqual(task.project_id, self.task_1.project_id, "project_id should be affected")
|
||||
self.assertTrue(task.display_in_project, "display_in_project should be True when there is no parent task")
|
||||
|
||||
def test_invisible_subtask_became_visible_when_converted_to_task(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Parent task',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [Command.create({'name': 'Sub-task invisible', 'project_id': self.project_goats.id})],
|
||||
})
|
||||
invisible_subtask = task.child_ids
|
||||
|
||||
self.assertFalse(invisible_subtask.display_in_project)
|
||||
|
||||
with Form(invisible_subtask, view="project.project_task_convert_to_subtask_view_form") as subtask_form:
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
|
||||
self.assertTrue(invisible_subtask.display_in_project)
|
||||
|
||||
def test_convert_tasks_to_subtask(self):
|
||||
"""
|
||||
Check if the parent task is linked with the subtask through the 'Convert to Subtask' wizard.
|
||||
|
||||
Steps:
|
||||
- Open the subtask wizard
|
||||
- Choose the parent task
|
||||
- Check the parent and subtask
|
||||
"""
|
||||
with Form(self.task_1, view="project.project_task_convert_to_subtask_view_form") as subtask:
|
||||
subtask.parent_id = self.task_2
|
||||
self.assertTrue(self.task_2 in self.task_1.parent_id, "Task2 should have Task1 as its parent.")
|
||||
self.assertTrue(self.task_1 in self.task_2.child_ids, "Task1 should have Task2 as its child.")
|
||||
|
||||
def test_action_convert_to_subtask_on_private_task(self):
|
||||
"""
|
||||
Check if a warning is triggered when the user selects a private task as a subtask.
|
||||
|
||||
Steps:
|
||||
- Create a private task
|
||||
- Perform the action convert_to_subtask
|
||||
- Check the returned action result
|
||||
"""
|
||||
private_task = self.env['project.task'].create({
|
||||
'name': 'Private task',
|
||||
'project_id': False,
|
||||
})
|
||||
|
||||
private_task_notification = private_task.action_convert_to_subtask()
|
||||
self.assertDictEqual(private_task_notification, {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'danger',
|
||||
'message': 'Private tasks cannot be converted into sub-tasks. Please set a project on the task to gain access to this feature.',
|
||||
},
|
||||
})
|
||||
|
||||
def test_display_in_project_is_correctly_set_when_parent_task_changes(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Parent task',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({'name': 'Sub-task 1', 'project_id': self.project_goats.id}),
|
||||
Command.create({'name': 'Sub-task 1', 'project_id': self.project_pigs.id}),
|
||||
],
|
||||
})
|
||||
subtask_1, subtask_2 = task.child_ids
|
||||
|
||||
self.assertFalse(subtask_1.display_in_project)
|
||||
self.assertTrue(subtask_2.display_in_project)
|
||||
|
||||
form_view = self.env.ref("project.project_task_convert_to_subtask_view_form")
|
||||
with Form(subtask_1, view=form_view) as subtask_form:
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
|
||||
self.assertTrue(subtask_1.display_in_project)
|
||||
|
||||
with Form(subtask_1, view=form_view) as subtask_form:
|
||||
subtask_form.parent_id = task
|
||||
|
||||
self.assertFalse(subtask_1.display_in_project)
|
||||
|
||||
with Form(subtask_2, view=form_view) as subtask_form:
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
|
||||
self.assertTrue(subtask_2.display_in_project)
|
||||
|
||||
with Form(subtask_2, view=form_view) as subtask_form:
|
||||
subtask_form.parent_id = task
|
||||
|
||||
self.assertTrue(subtask_2.display_in_project)
|
||||
|
||||
def test_subtask_private_project_and_parent_task(self):
|
||||
"""
|
||||
Test that an assigned employee to a subtask can open it even when
|
||||
they don't have access to the parent task or project.
|
||||
|
||||
Test Case:
|
||||
==========
|
||||
1) Create a private project with a parent task and a subtask.
|
||||
2) assign an employee to the subtask.
|
||||
3) Ensure the employee can access the subtask even if they don't have
|
||||
access to the parent task or project.
|
||||
"""
|
||||
private_project = self.env['project.project'].create({
|
||||
'name': 'Private Project',
|
||||
'privacy_visibility': 'followers',
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': private_project.id,
|
||||
})
|
||||
employee = self.env['res.users'].create({
|
||||
'name': 'Employee',
|
||||
'login': 'employee',
|
||||
'email': 'employee@odoo.com',
|
||||
'group_ids': [(6, 0, [self.env.ref('project.group_project_user').id])],
|
||||
})
|
||||
subtask = self.env['project.task'].create({
|
||||
'name': 'Subtask',
|
||||
'parent_id': task.id,
|
||||
'project_id': private_project.id,
|
||||
'user_ids': [(4, employee.id)],
|
||||
})
|
||||
|
||||
# Ensure the employee can read subtask fields that depends on the parent task
|
||||
parent_dependent_fields = [
|
||||
name for name, field in self.env['project.task']._fields.items()
|
||||
if field.compute and any(dep.startswith('parent_id') for dep in field.get_depends(self.env['project.task'])[0])
|
||||
]
|
||||
|
||||
self.env.invalidate_all()
|
||||
subtask_data = subtask.with_user(employee).read(parent_dependent_fields)
|
||||
self.assertTrue(subtask_data, "The employee should be able to read the subtask data.")
|
||||
|
||||
def test_subtasks_inherits_tags_of_parent(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Parent task',
|
||||
'project_id': self.project_goats.id,
|
||||
'tag_ids': [Command.create({'name': 'tag1', 'color': 0}), Command.create({'name': 'tag2', 'color': 1})],
|
||||
})
|
||||
|
||||
subtask1 = self.env['project.task'].with_context({'default_parent_id': task.id}).create({
|
||||
'name': 'Sub-task 1',
|
||||
'project_id': self.project_goats.id,
|
||||
})
|
||||
|
||||
self.assertEqual(subtask1.tag_ids, task.tag_ids, "Subtask should inherit tags from parent task")
|
||||
|
|
|
|||
|
|
@ -55,4 +55,4 @@ class TestProjectTags(HttpCase, TestProjectCommon):
|
|||
])
|
||||
|
||||
def test_01_project_tags(self):
|
||||
self.start_tour("/web", 'project_tags_filter_tour', login="admin")
|
||||
self.start_tour("/odoo", 'project_tags_filter_tour', login="admin")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common_tracking import MailTrackingDurationMixinCase
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
|
||||
@tagged('is_query_count')
|
||||
class TestProjectTaskMailTrackingDuration(MailTrackingDurationMixinCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass('project.task', {'project_id': 'create'})
|
||||
|
||||
def test_project_task_mail_tracking_duration(self):
|
||||
self._test_record_duration_tracking()
|
||||
|
||||
def test_project_task_mail_tracking_duration_batch(self):
|
||||
self._test_record_duration_tracking_batch()
|
||||
|
||||
def test_project_task_queries_batch_mail_tracking_duration(self):
|
||||
self._test_queries_batch_duration_tracking()
|
||||
|
||||
def test_task_mail_tracking_duration_during_onchange_stage(self):
|
||||
"""
|
||||
Checks that the status bar duration is correctly set during an onchange of its stage_id.
|
||||
"""
|
||||
task = self.rec_1
|
||||
task.stage_id = self.stage_1
|
||||
initial_tracking = task.duration_tracking
|
||||
with Form(task) as task_form:
|
||||
task_form.stage_id = self.stage_2
|
||||
final_tracking = task.duration_tracking
|
||||
self.assertEqual(initial_tracking[str(self.stage_1.id)], final_tracking[str(self.stage_1.id)])
|
||||
self.assertEqual(final_tracking[str(self.stage_2.id)], 0)
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestProjectTaskQuickCreate(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user1, cls.user2 = cls.env['res.users'].with_context({'no_reset_password': True}).create([{
|
||||
'name': 'Raouf 1',
|
||||
'login': 'raouf1',
|
||||
'password': 'raouf1aa',
|
||||
'email': 'raouf1@example.com',
|
||||
}, {
|
||||
'name': 'Raouf 2',
|
||||
'login': 'raouf2',
|
||||
'password': 'raouf2aa',
|
||||
'email': 'raouf2@example.com',
|
||||
}])
|
||||
|
||||
def test_create_task_with_valid_expressions(self):
|
||||
# dict format = {display name: (expected name, expected tags count, expected users count, expected priority, expected planned hours)}
|
||||
valid_expressions = {
|
||||
'task A 30H 2.5h #Tag1 #tag2 @Armande @Bast @raouf1 @raouf2 !': ('task A 30H 2.5h', 2, 4, "1", 0),
|
||||
'task A 30H 2.5h #Tag1 #tag2 #tag3 @Armande @Bast @raouf1 ! @raouf2': ('task A 30H 2.5h', 3, 4, "1", 0),
|
||||
'task A ! 30H 2.5h #Tag1 #tag2 #tag3 @Armande @Bast ! @raouf1 #tag4': ('task A 30H 2.5h', 4, 3, "1", 0),
|
||||
'task A': ('task A', 0, 0, "0", 0),
|
||||
'task A !': ('task A', 0, 0, "1", 0),
|
||||
'task A 30H 2.5h #Tag1 #tag2 #tag3 @Armande @Bast @raouf1 @raouf2': ('task A 30H 2.5h', 3, 4, "0", 0),
|
||||
'task A 30H 2.5h #Tag1 @Armande #tag3 @Bast @raouf1 #tag2 @raouf2 #tag4': ('task A 30H 2.5h', 4, 4, "0", 0),
|
||||
'task A 30H #tag1 @raouf1 Nothing !': ('task A 30H #tag1 @raouf1 Nothing', 0, 0, '1', 0),
|
||||
'task A 30H 2.5h #Tag1 #tag2 #tag3 @Armande @Bast @raouf !': ('task A 30H 2.5h @raouf', 3, 2, "1", 0),
|
||||
'task A 30H 2.5h #Tag1 #tag2 #tag3 @Armande @Bastttt @raouf1 @raouf2 !': ('task A 30H 2.5h @Bastttt', 3, 3, "1", 0),
|
||||
'task A 30H 2.5h #TAG1 #tag1 #TAG2': ('task A 30H 2.5h', 2, 0, "0", 0),
|
||||
'task A 30H 2.5h #Tag1 #tag2 @Armande @Bast @raouf1 @raouf2 !!': ('task A 30H 2.5h', 2, 4, "2", 0),
|
||||
'task A !!': ('task A', 0, 0, "2", 0),
|
||||
'task A 30H 2.5h #Tag1 #tag2 #tag3 @Armande @Bastttt @raouf1 @raouf2 !!': ('task A 30H 2.5h @Bastttt', 3, 3, "2", 0),
|
||||
'task A 30H 2.5h #Tag1 #tag2 @Armande @Bast @raouf1 @raouf2 !!!': ('task A 30H 2.5h', 2, 4, "3", 0),
|
||||
'task A !!!': ('task A', 0, 0, "3", 0),
|
||||
'task A 30H 2.5h #Tag1 #tag2 #tag3 @Armande @Bastttt @raouf1 @raouf2 !!!': ('task A 30H 2.5h @Bastttt', 3, 3, "3", 0),
|
||||
}
|
||||
|
||||
for expression, values in valid_expressions.items():
|
||||
task_form = Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id}), view="project.quick_create_task_form")
|
||||
task_form.display_name = expression
|
||||
task = task_form.save()
|
||||
results = (task.name, len(task.tag_ids), len(task.user_ids), task.priority, task.allocated_hours)
|
||||
self.assertEqual(results, values)
|
||||
|
||||
def test_create_task_with_invalid_expressions(self):
|
||||
invalid_expressions = (
|
||||
'#tag1 #tag2 #tag3 @Armande @Bast @raouf1 @raouf2',
|
||||
'@Armande @Bast @raouf1 @raouf2',
|
||||
'!',
|
||||
'task A!',
|
||||
'task A!!',
|
||||
'task A!!!',
|
||||
'!!',
|
||||
'!!!',
|
||||
'!! @Armande',
|
||||
'!!! #tag1',
|
||||
)
|
||||
|
||||
for expression in invalid_expressions:
|
||||
task_form = Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id}), view="project.quick_create_task_form")
|
||||
task_form.display_name = expression
|
||||
task = task_form.save()
|
||||
results = (task.name, len(task.tag_ids), len(task.user_ids), task.priority, task.allocated_hours)
|
||||
self.assertEqual(results, (expression, 0, 0, '0', 0))
|
||||
|
||||
def test_set_stage_on_project_from_task(self):
|
||||
new_stage = self.env['project.task.type'].create({
|
||||
'name': 'New Stage',
|
||||
})
|
||||
self.env['project.task'].create({
|
||||
'name': 'Test Task',
|
||||
'stage_id': new_stage.id,
|
||||
'project_id': self.project_pigs.id,
|
||||
})
|
||||
self.assertEqual(self.project_pigs.type_ids, new_stage, "Task stage is not set in project")
|
||||
|
||||
def test_create_task_with_default_value(self):
|
||||
self.project_pigs.write({
|
||||
'company_id': self.env.company,
|
||||
})
|
||||
project_ids = self.env['project.project'].search([]).ids
|
||||
self.env['ir.default'].discard_values('project.task', 'project_id', project_ids)
|
||||
self.env['ir.default'].set('project.task', 'project_id', self.project_pigs.id)
|
||||
field_specs = {'project_id': {}, 'company_id': {'fields': {}}}
|
||||
task_values = self.env['project.task'].onchange({}, [], field_specs)['value']
|
||||
self.assertEqual(task_values['project_id'], self.project_pigs.id, "The task project_id should be set")
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
|
|
@ -14,137 +13,69 @@ class TestProjectTaskType(TestProjectCommon):
|
|||
|
||||
cls.stage_created = cls.env['project.task.type'].create({
|
||||
'name': 'Stage Already Created',
|
||||
'project_ids': cls.project_goats.ids,
|
||||
})
|
||||
|
||||
def test_create_stage(self):
|
||||
'''
|
||||
Verify that it is not possible to add to a newly created stage a `user_id` and a `project_ids`
|
||||
Verify that 'user_id' is removed when a stage is created with `project_ids` set or set by default to the curent user if not
|
||||
'''
|
||||
with self.assertRaises(UserError):
|
||||
self.env['project.task.type'].create({
|
||||
self.assertFalse(self.env['project.task.type'].create({
|
||||
'name': 'New Stage',
|
||||
'user_id': self.uid,
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
}).user_id,
|
||||
"user_id should be reset if a project is set on the current stage",
|
||||
)
|
||||
self.assertEqual(self.env['project.task.type'].create({
|
||||
'name': 'Other new Stage',
|
||||
}).user_id.id,
|
||||
self.env.uid,
|
||||
"user_id should be set to the current user if no project is set at stage creation",
|
||||
)
|
||||
|
||||
def test_modify_existing_stage(self):
|
||||
'''
|
||||
- case 1: [`user_id`: not set, `project_ids`: not set] | Add `user_id` and `project_ids` => UserError
|
||||
- case 2: [`user_id`: set, `project_ids`: not set] | Add `project_ids` => UserError
|
||||
- case 1: [`user_id`: not set, `project_ids`: set] | Remove `project_ids` => user_id should not be set (no transformation of project stage to personal stage)
|
||||
- case 2: [`user_id`: not set, `project_ids`: not set] | Add `user_id` and `project_ids` => user_id reset
|
||||
- case 3: [`user_id`: not set, `project_ids`: set] | Add `user_id` => UserError
|
||||
- case 4: [`user_id`: set, `project_ids`: not set] | Add `project_ids` => user_id reset
|
||||
'''
|
||||
# case 1
|
||||
with self.assertRaises(UserError):
|
||||
self.stage_created.write({
|
||||
'user_id': self.uid,
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
self.assertTrue(not self.stage_created.user_id and self.stage_created.project_ids)
|
||||
self.stage_created.write({'project_ids': False})
|
||||
self.assertFalse(
|
||||
self.stage_created.user_id,
|
||||
"When project_ids is reset, user_id should not be set (no transformation of project related stage to personal stage)",
|
||||
)
|
||||
|
||||
# case 2
|
||||
self.assertTrue(not self.stage_created.user_id and not self.stage_created.project_ids)
|
||||
self.stage_created.write({
|
||||
'user_id': self.uid,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.stage_created.write({
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
|
||||
# case 3
|
||||
self.stage_created.write({
|
||||
'user_id': False,
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
self.assertFalse(
|
||||
self.stage_created.user_id,
|
||||
"user_id should be reset if a project is set on the current stage",
|
||||
)
|
||||
|
||||
# case 3
|
||||
with self.assertRaises(UserError):
|
||||
self.stage_created.write({
|
||||
'user_id': self.uid,
|
||||
})
|
||||
|
||||
def test_group_by_personal_stage(self):
|
||||
"""
|
||||
Check the consistence of search_read and read_group when one groups project.tasks by personal stages.
|
||||
|
||||
Supose we have a user and his manager. Group all tasks by personal stage in the "list view".
|
||||
A `web_read_group` is performed to classify the tasks and a `web_search_read` is performed to display the lines.
|
||||
We check the consitency of both operations for tasks that are not linked to a personal stage of the current user.
|
||||
"""
|
||||
|
||||
if 'hr.employee' not in self.env:
|
||||
self.skipTest("This test requires to set a manager")
|
||||
project = self.project_goats
|
||||
user = self.user_projectmanager
|
||||
manager_user = self.env['res.users'].create({
|
||||
'name': 'Roger Employee',
|
||||
'login': 'Roger',
|
||||
'email': 'rog.projectmanager@example.com',
|
||||
'groups_id': [Command.set([self.ref('base.group_user'), self.ref('project.group_project_manager')])],
|
||||
# case 4
|
||||
self.stage_created.write({
|
||||
'user_id': self.env.uid,
|
||||
'project_ids': False,
|
||||
})
|
||||
manager = self.env['hr.employee'].create({
|
||||
'user_id': manager_user.id,
|
||||
'image_1920': False,
|
||||
self.assertTrue(self.stage_created.user_id)
|
||||
self.stage_created.write({
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
(user | manager_user).employee_id.write({'parent_id': manager.id})
|
||||
user_personal_stages = self.env['project.task.type'].search([('user_id', '=', user.id)])
|
||||
# we create tasks for the user with different types of assignement
|
||||
self.env['project.task'].with_user(user).create([
|
||||
{
|
||||
'name': f"Task: {stage.id}",
|
||||
'project_id': project.id,
|
||||
'personal_stage_type_id': stage.id,
|
||||
'user_ids': [Command.link(user.id)],
|
||||
}
|
||||
for stage in user_personal_stages],
|
||||
self.assertFalse(
|
||||
self.stage_created.user_id,
|
||||
"user_id should be reset if a project is set on the current stage",
|
||||
)
|
||||
self.env['project.task'].with_user(user).create([
|
||||
{
|
||||
'name': f"Task: {stage.id}",
|
||||
'project_id': project.id,
|
||||
'personal_stage_type_id': stage.id,
|
||||
'user_ids': [Command.link(user.id), Command.link(manager_user.id)],
|
||||
}
|
||||
for stage in user_personal_stages],
|
||||
)
|
||||
# this task is created to create the default personal stages of manager user
|
||||
self.env['project.task'].with_user(manager_user).create({
|
||||
'name': "Manager's task",
|
||||
'project_id': project.id,
|
||||
'user_ids': [Command.link(manager_user.id)],
|
||||
})
|
||||
manager_user_personal_stages = self.env['project.task.type'].search([('user_id', '=', manager_user.id)])
|
||||
self.env['project.task'].with_user(manager_user).create([
|
||||
{
|
||||
'name': f"Task : {stage.id}",
|
||||
'project_id': project.id,
|
||||
'stage_id': stage.id,
|
||||
'user_ids': [Command.link(manager_user.id)],
|
||||
}
|
||||
for stage in manager_user_personal_stages],
|
||||
)
|
||||
|
||||
self.env.uid = user.id
|
||||
base_domain = [("user_ids.employee_parent_id.user_id", "=", manager_user.id)]
|
||||
tasks = self.env['project.task'].with_user(user.id).search(base_domain)
|
||||
tasks_with_personal_stage = tasks.filtered(lambda t: user in t.personal_stage_type_id.user_id)
|
||||
tasks_without_personal_stage = tasks - tasks_with_personal_stage
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"project_id",
|
||||
"milestone_id",
|
||||
"partner_id",
|
||||
"user_ids",
|
||||
"activity_ids",
|
||||
"stage_id",
|
||||
"personal_stage_type_ids",
|
||||
"tag_ids",
|
||||
"priority",
|
||||
"company_id",
|
||||
]
|
||||
groupby = ["personal_stage_type_ids"]
|
||||
user_read_group = self.env['project.task'].with_user(user).read_group(domain=base_domain, fields=fields, groupby=groupby)
|
||||
number_of_tasks_in_groups = sum(gr['personal_stage_type_ids_count'] if gr['personal_stage_type_ids'] and gr['personal_stage_type_ids'][0] in user_personal_stages.ids else 0 for gr in user_read_group)
|
||||
self.assertEqual(len(tasks_with_personal_stage), number_of_tasks_in_groups)
|
||||
tasks_found_for_user = [task['id'] for task in self.env['project.task'].with_user(user.id).search_read(domain=base_domain, fields=fields)]
|
||||
self.assertEqual(tasks.ids, tasks_found_for_user)
|
||||
domain = ["&", ("personal_stage_type_ids", "=", False), ("user_ids.employee_parent_id.user_id", "=", manager_user.id)]
|
||||
tasks_diplayed_without_personal_stage = [task['id'] for task in self.env['project.task'].with_user(user.id).search_read(domain=domain, fields=fields)]
|
||||
self.assertEqual(tasks_without_personal_stage.ids, tasks_diplayed_without_personal_stage)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
from datetime import date
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import freeze_time
|
||||
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
class TestProjectTemplates(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.project_template = cls.env["project.project"].create({
|
||||
"name": "Project Template",
|
||||
"is_template": True,
|
||||
"date_start": date(2025, 6, 1),
|
||||
"date": date(2025, 6, 11),
|
||||
})
|
||||
cls.task_inside_template, cls.task_template_inside_template = cls.env["project.task"].create([{
|
||||
"name": "A Task in Project Template",
|
||||
"project_id": cls.project_template.id,
|
||||
}, {
|
||||
"name": "B Task Template in Project Template",
|
||||
"project_id": cls.project_template.id,
|
||||
"is_template": True,
|
||||
}])
|
||||
|
||||
def test_convert_project_to_template(self):
|
||||
"""
|
||||
Converting a project to a project template should leave the tasks unchanged.
|
||||
"""
|
||||
self.project_template.is_template = False
|
||||
client_action = self.project_template.action_create_template_from_project()
|
||||
created_template = self.env["project.project"].browse(client_action["params"]["project_id"])
|
||||
task_a, task_b = created_template.task_ids.sorted("name")
|
||||
self.assertEqual(task_a.name, self.task_inside_template.name, "The copied task should have the same name as the original.")
|
||||
self.assertFalse(task_a.is_template, "The copied tasks should retain their template status.")
|
||||
self.assertEqual(task_b.name, self.task_template_inside_template.name, "The copied task should have the same name as the original.")
|
||||
self.assertTrue(task_b.is_template, "The copied tasks should retain their template status.")
|
||||
|
||||
def test_create_from_template(self):
|
||||
"""
|
||||
Creating a project through the action should result in a non template copy
|
||||
"""
|
||||
project = self.project_template.action_create_from_template()
|
||||
self.assertEqual(project.name, "Project Template", "The project name should be `Project Template`.")
|
||||
self.assertFalse(project.is_template, "The created Project should be a normal project and not a template.")
|
||||
self.assertFalse(project.partner_id, "The created Project should not have a customer.")
|
||||
|
||||
self.assertEqual(len(project.task_ids), 2, "The tasks of the template should be copied too.")
|
||||
task_a, task_b = project.task_ids.sorted("name")
|
||||
self.assertEqual(task_a.name, self.task_inside_template.name, "The copied task should have the same name as the original.")
|
||||
self.assertFalse(task_a.is_template, "The copied tasks should retain their template status.")
|
||||
self.assertEqual(task_b.name, self.task_template_inside_template.name, "The copied task should have the same name as the original.")
|
||||
self.assertTrue(task_b.is_template, "The copied tasks should retain their template status.")
|
||||
|
||||
def test_copy_template(self):
|
||||
"""
|
||||
A copy of a template should be a template
|
||||
"""
|
||||
copied_template = self.project_template.copy()
|
||||
self.assertEqual(copied_template.name, "Project Template (copy)", "The project name should be `Project Template` (copy).")
|
||||
self.assertTrue(copied_template.is_template, "The copy of the template should also be a template.")
|
||||
self.assertEqual(len(copied_template.task_ids), 2, "The child of the template should be copied too.")
|
||||
task_a, task_b = copied_template.task_ids.sorted("name")
|
||||
self.assertEqual(task_a.name, self.task_inside_template.name, "The copied task should have the same name as the original.")
|
||||
self.assertFalse(task_a.is_template, "The copied tasks should retain their template status.")
|
||||
self.assertEqual(task_b.name, self.task_template_inside_template.name, "The copied task should have the same name as the original.")
|
||||
self.assertTrue(task_b.is_template, "The copied tasks should retain their template status.")
|
||||
|
||||
def test_revert_template(self):
|
||||
"""
|
||||
A revert of a template should not be a template
|
||||
"""
|
||||
self.project_template.action_undo_convert_to_template()
|
||||
self.assertFalse(self.project_template.is_template, "The reverted template should become a normal template.")
|
||||
|
||||
def test_tasks_dispatching_from_template(self):
|
||||
"""
|
||||
The tasks of a project template should be dispatched to the new project according to the role-to-users mapping defined
|
||||
on the project template wizard.
|
||||
"""
|
||||
role1, role2, role3, role4, role5 = self.env['project.role'].create([
|
||||
{'name': 'Developer'},
|
||||
{'name': 'Designer'},
|
||||
{'name': 'Project Manager'},
|
||||
{'name': 'Tester'},
|
||||
{'name': 'Product Owner'},
|
||||
])
|
||||
project_template = self.env['project.project'].create({
|
||||
'name': 'Project template',
|
||||
'is_template': True,
|
||||
'task_ids': [
|
||||
Command.create({
|
||||
'name': 'Task 1',
|
||||
'role_ids': [role1.id, role3.id],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Task 2',
|
||||
'role_ids': [role5.id, role4.id],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Task 3',
|
||||
'role_ids': [role2.id, role5.id],
|
||||
'child_ids': [Command.create({
|
||||
'name': 'Sub Task 1',
|
||||
}), Command.create({
|
||||
'name': 'Sub Task 2',
|
||||
})],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Task 4',
|
||||
'role_ids': [role3.id],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Task 5',
|
||||
'role_ids': [role5.id],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Task 6',
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Task 7',
|
||||
'role_ids': [role2.id, role3.id],
|
||||
'user_ids': [self.user_projectuser.id, self.user_projectmanager.id],
|
||||
}),
|
||||
],
|
||||
})
|
||||
user1, user2 = self.env['res.users'].create([
|
||||
{
|
||||
'name': 'Test User 1',
|
||||
'login': 'test1',
|
||||
'password': 'testuser1',
|
||||
'email': 'test1.test@example.com',
|
||||
},
|
||||
{
|
||||
'name': 'Test User 2',
|
||||
'login': 'test2',
|
||||
'password': 'testuser2',
|
||||
'email': 'test2.test@example.com',
|
||||
}
|
||||
])
|
||||
wizard = self.env['project.template.create.wizard'].create({
|
||||
'template_id': project_template.id,
|
||||
'name': 'New Project from Template',
|
||||
'role_to_users_ids': [
|
||||
Command.create({
|
||||
'role_id': role1.id,
|
||||
'user_ids': [self.user_projectuser.id, self.user_projectmanager.id],
|
||||
}),
|
||||
Command.create({
|
||||
'role_id': role2.id,
|
||||
'user_ids': [user1.id],
|
||||
}),
|
||||
Command.create({
|
||||
'role_id': role3.id,
|
||||
'user_ids': [user2.id],
|
||||
}),
|
||||
Command.create({
|
||||
'role_id': role4.id,
|
||||
'user_ids': [self.user_projectuser.id],
|
||||
}),
|
||||
],
|
||||
})
|
||||
new_project = wizard._create_project_from_template()
|
||||
|
||||
self.assertEqual(
|
||||
new_project.task_ids.filtered(lambda t: t.name == 'Task 1').user_ids,
|
||||
self.user_projectuser + self.user_projectmanager + user2,
|
||||
'Task 1 should be assigned to the users mapped to `role1` and `role3`.',
|
||||
)
|
||||
self.assertEqual(
|
||||
new_project.task_ids.filtered(lambda t: t.name == 'Task 2').user_ids,
|
||||
self.user_projectuser,
|
||||
'Task 2 should be assigned to the users mapped to `role4`. As `role5` is not in the mapping.',
|
||||
)
|
||||
self.assertEqual(
|
||||
new_project.task_ids.filtered(lambda t: t.name == 'Task 3').user_ids,
|
||||
user1,
|
||||
'Task 3 should be assigned to the users mapped to `role2`. As `role5` is not in the mapping.',
|
||||
)
|
||||
self.assertEqual(
|
||||
new_project.task_ids.filtered(lambda t: t.name == 'Task 4').user_ids,
|
||||
user2,
|
||||
'Task 4 should be assigned to the users mapped to `role3`.'
|
||||
)
|
||||
self.assertFalse(
|
||||
new_project.task_ids.filtered(lambda t: t.name == 'Task 5').user_ids,
|
||||
'Task 5 should not be assigned to any user as `role5` is not in the mapping.',
|
||||
)
|
||||
self.assertFalse(
|
||||
new_project.task_ids.filtered(lambda t: t.name == 'Task 6').user_ids,
|
||||
'Task 6 should not be assigned to any user as it has no role.',
|
||||
)
|
||||
self.assertEqual(
|
||||
new_project.task_ids.filtered(lambda t: t.name == 'Task 7').user_ids,
|
||||
self.user_projectuser + self.user_projectmanager + user1 + user2,
|
||||
'Task 7 should be assigned to the users mapped to `role2` and `role3`, plus the users who were already assigned to the task.'
|
||||
)
|
||||
|
||||
@freeze_time("2025-06-15")
|
||||
def test_create_from_template_no_dates(self):
|
||||
today = date.today()
|
||||
|
||||
new_project = self.project_template.action_create_from_template({
|
||||
"name": "New Project",
|
||||
"is_template": False,
|
||||
})
|
||||
|
||||
expected_delta = self.project_template.date - self.project_template.date_start
|
||||
self.assertEqual(new_project.date_start, today, "Start date should default to today")
|
||||
self.assertEqual(new_project.date, today + expected_delta, "End date should be start + template delta")
|
||||
|
||||
def test_create_from_template_start_only(self):
|
||||
new_start = date(2025, 7, 1)
|
||||
|
||||
new_project = self.project_template.action_create_from_template({
|
||||
"name": "New Project",
|
||||
"is_template": False,
|
||||
"date_start": new_start,
|
||||
})
|
||||
|
||||
expected_delta = self.project_template.date - self.project_template.date_start
|
||||
self.assertEqual(new_project.date_start, new_start, "Start date should be the one provided")
|
||||
self.assertEqual(new_project.date, new_start + expected_delta, "End date should be start + template delta")
|
||||
|
||||
def test_create_from_template_with_dates(self):
|
||||
new_start = date(2025, 7, 1)
|
||||
new_end = date(2025, 7, 15)
|
||||
|
||||
new_project = self.project_template.action_create_from_template({
|
||||
"name": "New Project",
|
||||
"is_template": False,
|
||||
"date_start": new_start,
|
||||
"date": new_end,
|
||||
})
|
||||
|
||||
self.assertEqual(new_project.date_start, new_start, "Start date should be the one provided")
|
||||
self.assertEqual(new_project.date, new_end, "End date should be the one provided")
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestProjectTemplatesTour(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.project_template = cls.env["project.project"].create({
|
||||
"name": "Project Template",
|
||||
"is_template": True,
|
||||
})
|
||||
cls.task_inside_template = cls.env["project.task"].create({
|
||||
"name": "Task in Project Template",
|
||||
"project_id": cls.project_template.id,
|
||||
})
|
||||
|
||||
def test_project_templates_tour(self):
|
||||
user_admin = self.env.ref('base.user_admin')
|
||||
user_admin.write({
|
||||
'email': 'mitchell.admin@example.com',
|
||||
})
|
||||
self.start_tour("/odoo", "project_templates_tour", login="admin")
|
||||
|
|
@ -9,7 +9,42 @@ class TestUi(odoo.tests.HttpCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env['res.config.settings'].create({'group_project_milestone': True}).execute()
|
||||
cls.env.ref('base.group_user').sudo().implied_ids |= cls.env.ref('project.group_project_milestone')
|
||||
|
||||
def test_01_project_tour(self):
|
||||
self.start_tour("/web", 'project_tour', login="admin")
|
||||
self.start_tour("/odoo", 'project_tour', login="admin")
|
||||
|
||||
def test_project_task_history(self):
|
||||
"""This tour will check that the history works properly."""
|
||||
stage = self.env['project.task.type'].create({'name': 'To Do'})
|
||||
_dummy, project2 = self.env['project.project'].create([{
|
||||
'name': 'Without tasks project',
|
||||
'type_ids': stage.ids,
|
||||
}, {
|
||||
'name': 'Test History Project',
|
||||
'type_ids': stage.ids,
|
||||
}])
|
||||
|
||||
self.env['project.task'].create({
|
||||
'name': 'Test History Task',
|
||||
'stage_id': stage.id,
|
||||
'project_id': project2.id,
|
||||
})
|
||||
|
||||
self.start_tour('/odoo?debug=1', 'project_task_history_tour', login='admin')
|
||||
|
||||
def test_project_task_last_history_steps(self):
|
||||
"""This tour will check that the history works properly."""
|
||||
stage = self.env['project.task.type'].create({'name': 'To Do'})
|
||||
project = self.env['project.project'].create([{
|
||||
'name': 'Test History Project',
|
||||
'type_ids': stage.ids,
|
||||
}])
|
||||
|
||||
self.env['project.task'].create({
|
||||
'name': 'Test History Task',
|
||||
'stage_id': stage.id,
|
||||
'project_id': project.id,
|
||||
})
|
||||
|
||||
self.start_tour('/odoo', 'project_task_last_history_steps_tour', login='admin')
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
|
|
@ -13,9 +12,8 @@ class TestProjectUpdate(TestProjectCommon):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
cls.env.user.group_ids |= cls.env.ref('project.group_project_milestone')
|
||||
cls.project_pigs.allow_milestones = True
|
||||
|
||||
def test_project_update_form(self):
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
|
|
@ -53,7 +51,7 @@ class TestProjectUpdate(TestProjectCommon):
|
|||
|
||||
template_values = self.env['project.update']._get_template_values(self.project_pigs)
|
||||
|
||||
self.assertTrue(template_values['milestones']['show_section'], 'The milestone section should not be visible since the feature is disabled')
|
||||
self.assertTrue(template_values['milestones']['show_section'], 'The milestone section should be visible since the feature is enabled')
|
||||
self.assertEqual(len(template_values['milestones']['list']), 2, "Milestone list length should be equal to 2")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 3, "Milestone created length tasks should be equal to 3")
|
||||
|
||||
|
|
@ -65,17 +63,6 @@ class TestProjectUpdate(TestProjectCommon):
|
|||
self.assertEqual(len(template_values['milestones']['list']), 0, "Milestone list length should be equal to 0 because the Milestones feature is disabled.")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 0, "Milestone created length tasks should be equal to 0 because the Milestones feature is disabled.")
|
||||
|
||||
self.project_pigs.write({'allow_milestones': True})
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
|
||||
template_values = self.env['project.update']._get_template_values(self.project_pigs)
|
||||
|
||||
self.assertFalse(template_values['milestones']['show_section'], 'The milestone section should not be visible since the feature is disabled')
|
||||
self.assertEqual(len(template_values['milestones']['list']), 0, "Milestone list length should be equal to 0 because the Milestones feature is disabled.")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 0, "Milestone created length tasks should be equal to 0 because the Milestones feature is disabled.")
|
||||
|
||||
def test_project_update_panel(self):
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 1"
|
||||
|
|
@ -106,9 +93,34 @@ class TestProjectUpdate(TestProjectCommon):
|
|||
self.assertNotIn('milestones', panel_data, 'Since the "Milestones" feature is disabled in this project, the "Milestones" section is not loaded.')
|
||||
|
||||
# Disable globally the Milestones feature and check the Milestones section is not loaded.
|
||||
self.project_pigs.write({'allow_milestones': True})
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
self.env.user.group_ids -= self.env.ref('project.group_project_milestone')
|
||||
panel_data = self.project_pigs.get_panel_data()
|
||||
self.assertNotIn('milestones', panel_data, 'Since the "Milestones" feature is globally disabled, the "Milestones" section is not loaded.')
|
||||
|
||||
def test_project_update_reflects_task_changes(self):
|
||||
"""
|
||||
Check if the project update reflects according to the task changes or not.
|
||||
Steps:
|
||||
1) Create a project update
|
||||
2) Check the task count, closetask, and closed task percentag
|
||||
3) Move Task1 to the Done stage
|
||||
4) Repeat steps 1 and 2
|
||||
5) Move Task2 to the Canceled stage
|
||||
6) Create a new task
|
||||
7) Repeat steps 1 and 2
|
||||
"""
|
||||
def create_project_update_view():
|
||||
update_form = Form(self.env['project.update'].with_context({'default_project_id': self.project_pigs.id}))
|
||||
update_form.name = "Test"
|
||||
project_update = update_form.save()
|
||||
return [project_update.task_count, project_update.closed_task_count, project_update.closed_task_percentage]
|
||||
|
||||
project_update_data_list = create_project_update_view()
|
||||
self.assertListEqual(project_update_data_list, [self.project_pigs.task_count, 0, 0])
|
||||
self.task_1.state = '1_done'
|
||||
project_update_data_list = create_project_update_view()
|
||||
self.assertListEqual(project_update_data_list, [self.project_pigs.task_count, 1, 50])
|
||||
self.task_2.state = '1_canceled'
|
||||
self.task_2.copy()
|
||||
project_update_data_list = create_project_update_view()
|
||||
self.assertListEqual(project_update_data_list, [self.project_pigs.task_count, 2, 67])
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ from odoo.tests import HttpCase, tagged
|
|||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProjectUpdateUi(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Enable the "Milestones" feature to be able to create milestones on this tour.
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
|
||||
def test_01_project_tour(self):
|
||||
self.start_tour("/web", 'project_update_tour', login="admin")
|
||||
# Enable milestones to avoid a different behavior when running the tour with or without demo data.
|
||||
# Indeed, when we check Milestones on the Settings tab of a newly created project,
|
||||
# we ensure milestones are globally enabled. If the feature was disabled, it causes a full page reload.
|
||||
# The tour step should then have a expectUnloadPage depending on whether milestones are already enabled.
|
||||
# As it is too complicated to determine this value from the tour itself, we avoid this page reload completely.
|
||||
self.env.ref('base.group_user').implied_ids |= self.env.ref('project.group_project_milestone')
|
||||
|
||||
self.start_tour("/odoo", 'project_update_tour', login="admin")
|
||||
self.start_tour("/odoo", 'project_tour', login="admin")
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged, Form, TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestResConfigSettings(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.analytic_plan_1, cls.analytic_plan_2 = cls.env['account.analytic.plan'].create([
|
||||
{
|
||||
'name': 'Plan 1',
|
||||
'default_applicability': 'optional',
|
||||
'company_id': False,
|
||||
}, {
|
||||
'name': 'Plan 2',
|
||||
'default_applicability': 'optional',
|
||||
'company_id': False,
|
||||
},
|
||||
])
|
||||
cls.company_1, cls.company_2 = cls.env['res.company'].create([
|
||||
{'name': 'Test Company 1'},
|
||||
{'name': 'Test Company 2'},
|
||||
])
|
||||
(cls.analytic_plan_1 + cls.analytic_plan_2).write({
|
||||
'company_id': cls.company_1.id,
|
||||
})
|
||||
|
||||
def test_set_default_analytic_plan(self):
|
||||
"""
|
||||
Test that we can set the default analytic plan in the settings per company.
|
||||
When there are no analytic plans for the company, a new one named "Default" should be created.
|
||||
"""
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
settings_company_1 = self.env['res.config.settings'].with_company(self.company_1).create({})
|
||||
with Form(settings_company_1) as form:
|
||||
self.assertEqual(form.analytic_plan_id, self.analytic_plan_1)
|
||||
form.analytic_plan_id = self.analytic_plan_2
|
||||
form.save()
|
||||
self.assertEqual(settings_company_1.analytic_plan_id, self.analytic_plan_2)
|
||||
settings_company_2 = self.env['res.config.settings'].with_company(self.company_2).create({})
|
||||
with Form(settings_company_2) as form:
|
||||
self.assertNotEqual(form.analytic_plan_id, self.analytic_plan_1)
|
||||
self.assertNotEqual(form.analytic_plan_id, self.analytic_plan_2)
|
||||
self.assertEqual(form.analytic_plan_id.name, "Default")
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.fields import Command, Datetime
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ class TestTaskDependencies(TestProjectCommon):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.env.user.group_ids |= cls.env.ref('project.group_project_task_dependencies')
|
||||
cls.project_pigs.write({
|
||||
'allow_task_dependencies': True,
|
||||
})
|
||||
|
|
@ -80,89 +80,37 @@ class TestTaskDependencies(TestProjectCommon):
|
|||
self.assertEqual(len(self.task_2.depend_on_ids), 1, "The task 2 should have one dependency.")
|
||||
|
||||
# 4) Add task1 as dependency in task3 and check a validation error is raised
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
with self.assertRaises(ValidationError):
|
||||
self.task_3.write({
|
||||
'depend_on_ids': [Command.link(self.task_1.id)],
|
||||
})
|
||||
self.assertEqual(len(self.task_3.depend_on_ids), 0, "The dependency should not be added in the task 3 because of a cyclic dependency.")
|
||||
|
||||
# 5) Add task1 as dependency in task2 and check a validation error is raised
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
with self.assertRaises(ValidationError):
|
||||
self.task_2.write({
|
||||
'depend_on_ids': [Command.link(self.task_1.id)],
|
||||
})
|
||||
self.assertEqual(len(self.task_2.depend_on_ids), 1, "The number of dependencies should no change in the task 2 because of a cyclic dependency.")
|
||||
|
||||
def test_tracking_dependencies(self):
|
||||
# Enable the company setting
|
||||
self.env['res.config.settings'].create({
|
||||
'group_project_task_dependencies': True
|
||||
}).execute()
|
||||
# `depend_on_ids` is tracked
|
||||
self.task_1.with_context(mail_notrack=True).write({
|
||||
'depend_on_ids': [Command.link(self.task_2.id)]
|
||||
})
|
||||
self.cr.precommit.clear()
|
||||
# Check that changing a dependency tracked field in task_2 logs a message in task_1.
|
||||
self.task_2.write({'date_deadline': date(1983, 3, 1)}) # + 1 message in task_1 and task_2
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 1,
|
||||
'Changing the deadline on task 2 should have logged a message in task 1.')
|
||||
|
||||
# Check that changing a dependency tracked field in task_1 does not log a message in task_2.
|
||||
self.task_1.date_deadline = date(2020, 1, 2) # + 1 message in task_1
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_2.message_ids), 1,
|
||||
'Changing the deadline on task 1 should not have logged a message in task 2.')
|
||||
|
||||
# Check that changing a field that is not tracked at all on task 2 does not impact task 1.
|
||||
self.task_2.color = 100 # no new message
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 2,
|
||||
'Changing the color on task 2 should not have logged a message in task 1 since it is not tracked.')
|
||||
|
||||
# Check that changing multiple fields does not log more than one message.
|
||||
self.task_2.write({
|
||||
'date_deadline': date(2020, 1, 1),
|
||||
'kanban_state': 'blocked',
|
||||
}) # + 1 message in task_1 and task_2
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 3,
|
||||
'Changing multiple fields on task 2 should only log one message in task 1.')
|
||||
|
||||
def test_task_dependencies_settings_change(self):
|
||||
|
||||
def set_task_dependencies_setting(enabled):
|
||||
features_config = self.env["res.config.settings"].create({'group_project_task_dependencies': enabled})
|
||||
features_config.execute()
|
||||
|
||||
self.project_pigs.write({
|
||||
'allow_task_dependencies': False,
|
||||
})
|
||||
|
||||
# As the Project General Setting group_project_task_dependencies needs to be toggled in order
|
||||
# to be applied on the existing projects we need to force it so that it does not depends on anything
|
||||
# (like demo data for instance)
|
||||
set_task_dependencies_setting(False)
|
||||
set_task_dependencies_setting(True)
|
||||
self.assertTrue(self.project_pigs.allow_task_dependencies, "Projects allow_task_dependencies should follow group_project_task_dependencies setting changes")
|
||||
|
||||
# set group_project_task_dependencies(True)
|
||||
self.project_chickens = self.env['project.project'].create({
|
||||
'name': 'My Chicken Project'
|
||||
})
|
||||
self.assertTrue(self.project_chickens.allow_task_dependencies, "New Projects allow_task_dependencies should default to group_project_task_dependencies")
|
||||
|
||||
set_task_dependencies_setting(False)
|
||||
self.assertFalse(self.project_pigs.allow_task_dependencies, "Projects allow_task_dependencies should follow group_project_task_dependencies setting changes")
|
||||
self.assertFalse(self.project_chickens.allow_task_dependencies, "New Projects allow_task_dependencies should default to False")
|
||||
|
||||
# set group_project_task_dependencies(False)
|
||||
self.env.user.group_ids -= self.env.ref('project.group_project_task_dependencies')
|
||||
self.project_ducks = self.env['project.project'].create({
|
||||
'name': 'My Ducks Project'
|
||||
})
|
||||
self.assertFalse(self.project_ducks.allow_task_dependencies, "New Projects allow_task_dependencies should default to group_project_task_dependencies")
|
||||
self.assertFalse(self.project_ducks.allow_task_dependencies, "New Projects allow_task_dependencies should still default to False")
|
||||
|
||||
def test_duplicate_project_with_task_dependencies(self):
|
||||
self.project_pigs.allow_task_dependencies = True
|
||||
self.task_1.depend_on_ids = self.task_2
|
||||
self.task_1.date_deadline = Datetime.now()
|
||||
pigs_copy = self.project_pigs.copy()
|
||||
|
||||
task1_copy = pigs_copy.task_ids.filtered(lambda t: t.name == 'Pigs UserTask')
|
||||
|
|
@ -182,16 +130,21 @@ class TestTaskDependencies(TestProjectCommon):
|
|||
self.assertEqual(task1_copy_copy.depend_on_ids.ids, [self.task_1.id],
|
||||
"Copy should not alter the relation if the other task is in a different project")
|
||||
|
||||
self.project_pigs.allow_task_dependencies = False
|
||||
project_pigs_no_dep = self.project_pigs.copy()
|
||||
self.assertFalse(project_pigs_no_dep.allow_task_dependencies, 'The copied project should have the dependencies feature disabled')
|
||||
self.assertFalse(project_pigs_no_dep.task_ids.depend_on_ids, 'The copied task should not have any dependencies')
|
||||
self.assertFalse(project_pigs_no_dep.task_ids.dependent_ids, 'The copied task should not have any dependencies')
|
||||
|
||||
def test_duplicate_project_with_subtask_dependencies(self):
|
||||
self.project_goats.allow_task_dependencies = True
|
||||
self.project_goats.allow_subtasks = True
|
||||
parent_task = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({'name': 'Node 1'}),
|
||||
Command.create({'name': 'SuperNode 2', 'child_ids': [Command.create({'name': 'Node 2'})]}),
|
||||
Command.create({'name': 'Node 3'}),
|
||||
Command.create({'name': 'Node 1', 'project_id': self.project_goats.id}),
|
||||
Command.create({'name': 'SuperNode 2', 'project_id': self.project_goats.id, 'child_ids': [Command.create({'name': 'Node 2', 'project_id': self.project_goats.id})]}),
|
||||
Command.create({'name': 'Node 3', 'project_id': self.project_goats.id}),
|
||||
],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import odoo.tests
|
||||
from odoo.tests.common import HttpCase, new_test_user
|
||||
from odoo.tools.json import scriptsafe as json_safe
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestTaskLinkPreviewName(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.admin = new_test_user(cls.env, login='admin_user', groups='base.group_user,base.group_system')
|
||||
|
||||
cls.project_internal_link_display = cls.env['project.project'].create({
|
||||
'name': 'project',
|
||||
'display_name': 'project_display_name',
|
||||
'description': 'project_description',
|
||||
})
|
||||
cls.task_internal_link_customized = cls.env['project.task'].create({
|
||||
'name': 'task1',
|
||||
'display_name': 'task1_display_name',
|
||||
'link_preview_name': 'test1 | test parent',
|
||||
'project_id': cls.project_internal_link_display.id,
|
||||
'description': 'task1_description',
|
||||
'user_ids': [(6, 0, [cls.admin.id])],
|
||||
})
|
||||
|
||||
def test_01_task_link_preview_name(self):
|
||||
self.authenticate(self.admin.login, self.admin.login)
|
||||
# retrieve metadata of an record with customerized link_preview_name
|
||||
response_with_preview_name = self.url_open(
|
||||
'/html_editor/link_preview_internal',
|
||||
data=json_safe.dumps({
|
||||
"params": {
|
||||
"preview_url": f"/odoo/all-tasks/{self.task_internal_link_customized.id}",
|
||||
}
|
||||
}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
self.assertEqual(200, response_with_preview_name.status_code)
|
||||
self.assertTrue('link_preview_name' in response_with_preview_name.text)
|
||||
|
||||
def test_my_tasks_path(self):
|
||||
self.authenticate(self.admin.login, self.admin.login)
|
||||
response_with_preview_name = self.url_open(
|
||||
'/html_editor/link_preview_internal',
|
||||
data=json_safe.dumps({
|
||||
"params": {
|
||||
"preview_url": f"/odoo/my-tasks/{self.task_internal_link_customized.id}",
|
||||
}
|
||||
}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
self.assertEqual(200, response_with_preview_name.status_code)
|
||||
self.assertTrue('link_preview_name' in response_with_preview_name.text)
|
||||
227
odoo-bringout-oca-ocb-project/project/tests/test_task_state.py
Normal file
227
odoo-bringout-oca-ocb-project/project/tests/test_task_state.py
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestTaskState(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.project_goats.write({
|
||||
'allow_task_dependencies': True,
|
||||
})
|
||||
(cls.task_1 + cls.task_2).write({
|
||||
'project_id': cls.project_goats.id,
|
||||
})
|
||||
|
||||
def test_base_state(self):
|
||||
""" Test the task base state features
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) check that task_1 and task_2 are in_progress by default
|
||||
2) add task_2 as a dependency for task_1, check that task_1 state has gone to waiting_normal.
|
||||
3) force task_1 state to done, the state of task_1 should become done
|
||||
4) switch task_1 state back to in progress, its state should automatically switch back to waiting normal because of the task_2 dependency
|
||||
5) change task_2 state to canceled, check that task_1 state has gone back to in_progress.
|
||||
"""
|
||||
|
||||
# 1) check that task_1 and task2 are in_progress by default
|
||||
self.assertEqual(self.task_1.state, '01_in_progress', "The task_1 should be in progress by default")
|
||||
self.assertEqual(self.task_2.state, '01_in_progress', "The task_2 should be in progress by default")
|
||||
|
||||
# 2) add task2 as a dependency for task 1, check that task_1 state has gone to waiting_normal.
|
||||
self.task_1.write({
|
||||
'depend_on_ids': [Command.link(self.task_2.id)],
|
||||
})
|
||||
self.assertEqual(self.task_1.state, '04_waiting_normal', "The task_1 should be in waiting_normal after depending on another open task")
|
||||
|
||||
|
||||
# 3) force task_1 state to done, the state of task_1 should become done
|
||||
self.task_1.write({
|
||||
'state': '1_done',
|
||||
})
|
||||
self.assertEqual(self.task_1.state, '1_done', "The task_1 should be in done even if it has a depending task not closed")
|
||||
|
||||
# 4) switch task_1 state back to in progress, its state should automatically switch back to waiting normal because of the task2 dependency
|
||||
|
||||
self.task_1.write({
|
||||
'state': '01_in_progress',
|
||||
})
|
||||
self.assertEqual(self.task_1.state, '04_waiting_normal', "task_1 state should automatically switch back to waiting_normal because of the task2 dependency")
|
||||
|
||||
# 5) change task_2 state to done, check that task_1 state has gone back to in_progress.
|
||||
|
||||
self.task_2.write({
|
||||
'state': '1_canceled',
|
||||
})
|
||||
self.assertEqual(self.task_1.state, '01_in_progress', "task_1 state should automatically switch back to in_progress when its dependency closes")
|
||||
|
||||
def test_change_stage_or_project(self):
|
||||
"""
|
||||
Test special cases where the task is moved from a stage to another or a project to another
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) change task_1 to an open state and task_2 to a closed state
|
||||
2) change task_1 and task_2 stage, task_1 should go back to in_progress, task_2 should stay in its closing state
|
||||
3) change task_1 and task_2 project, they should both go back to in_progress
|
||||
"""
|
||||
|
||||
# 1) change task_1 to an open state and task_2 to a closed state
|
||||
|
||||
stage_won = self.env['project.task.type'].search([('name', '=', 'Won')])
|
||||
project_pigs = self.env['project.project'].search([('name', '=', 'Pigs')])
|
||||
|
||||
self.task_1.write({
|
||||
'state': '02_changes_requested',
|
||||
})
|
||||
self.task_2.write({
|
||||
'state': '1_canceled',
|
||||
})
|
||||
# 2) change task_1 and task_2 from stage, task_1 should go back to in_progress, task_2 should stay in its closing state
|
||||
(self.task_1 + self.task_2).write({
|
||||
'stage_id': stage_won.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.state, '01_in_progress', "task_1 state should automatically switch back to in_progress when its stage changes")
|
||||
self.assertEqual(self.task_2.state, '1_canceled', "task_2 state should stay in its closed state")
|
||||
|
||||
# 3) change task_1 and task_2 project, they should both go back to in_progress
|
||||
|
||||
# we make change the task_1 state back to an open state
|
||||
self.task_1.write({
|
||||
'state': '02_changes_requested',
|
||||
})
|
||||
|
||||
(self.task_1 + self.task_2).write({
|
||||
'project_id': project_pigs.id
|
||||
})
|
||||
self.task_1._onchange_project_id()
|
||||
self.assertEqual(self.task_1.state, '01_in_progress', "task_1 state should automatically switch back to in_progress when its project changes")
|
||||
self.assertEqual(self.task_2.state, '01_in_progress', "task_2 state should automatically switch back to in_progress when its project changes")
|
||||
|
||||
def test_duplicate_dependent_task(self):
|
||||
self.task_1.write({
|
||||
'depend_on_ids': [Command.link(self.task_2.id)],
|
||||
})
|
||||
self.assertEqual(self.task_1.state, '04_waiting_normal', "The task_1 should be in waiting_normal after depending on another open task")
|
||||
|
||||
self.task_1_copy = self.task_1.copy()
|
||||
self.assertEqual(self.task_1.state, '04_waiting_normal', "The task_1_copy should keep his dependence and stay in waiting_normal")
|
||||
|
||||
self.task_2.write({
|
||||
'state': '03_approved',
|
||||
})
|
||||
self.task_2_copy = self.task_2.copy()
|
||||
self.assertEqual(self.task_2_copy.state, '01_in_progress', "The task_2_copy should go back to in_progress")
|
||||
|
||||
self.task_2.write({
|
||||
'state': '1_done',
|
||||
})
|
||||
self.assertEqual(self.task_1.state, '04_waiting_normal', "The task_1 should have both tasks as dependencies and so should stay in waiting when one of the two is completed")
|
||||
self.assertEqual(self.task_1_copy.state, '04_waiting_normal', "The task_1_copy should have both tasks as dependencies and so should stay in waiting when one of the two is completed")
|
||||
|
||||
self.task_2_copy.write({
|
||||
'state': '1_done',
|
||||
})
|
||||
|
||||
self.assertEqual(self.task_1.state, '01_in_progress', "The task_1 should have both tasks as dependencies and so should stay go to 'done' when both dependencies are completed")
|
||||
self.assertEqual(self.task_1_copy.state, '01_in_progress', "The task_1_copy should have both tasks as dependencies and so should stay go to 'done' when both dependencies are completed")
|
||||
|
||||
def test_duplicate_task_state_retention_with_closed_dependencies(self):
|
||||
self.project_pigs.allow_task_dependencies = True
|
||||
self.task_1.depend_on_ids = self.task_2
|
||||
self.task_2.write({'state': '1_done'})
|
||||
self.task_1.write({'state': '03_approved'})
|
||||
|
||||
task_1_copy = self.task_1.copy()
|
||||
|
||||
self.assertEqual(self.task_1.state, '03_approved', "The task_1 should retain its state after being copied.")
|
||||
self.assertEqual(task_1_copy.state, '01_in_progress', "The task_1_copy should have a state of 'in progress'.")
|
||||
|
||||
def test_duplicate_task_state_retention_with_open_dependencies(self):
|
||||
self.project_pigs.allow_task_dependencies = True
|
||||
self.task_1.depend_on_ids = self.task_2
|
||||
self.task_2.write({'state': '01_in_progress'})
|
||||
|
||||
task_1_copy = self.task_1.copy()
|
||||
|
||||
self.assertEqual(self.task_1.state, '04_waiting_normal')
|
||||
self.assertEqual(task_1_copy.state, '04_waiting_normal')
|
||||
|
||||
def test_task_created_in_waiting_stage_gets_in_progress_state(self):
|
||||
"""
|
||||
Test that when a new task is created in the "Waiting" state (by grouping by state in Kanban view), it gets the state "In Progress" by default.
|
||||
"""
|
||||
project_pigs = self.env['project.project'].search([('name', '=', 'Pigs')])
|
||||
task = self.env['project.task'].with_context({
|
||||
'default_state': '04_waiting_normal',
|
||||
}).create({
|
||||
'name': 'Task initially waiting state',
|
||||
'project_id': project_pigs.id,
|
||||
})
|
||||
|
||||
self.assertEqual(task.state, '01_in_progress', "The task should be in progress")
|
||||
|
||||
def test_changing_parent_do_not_reset_task_state(self):
|
||||
self.task_2.state = '04_waiting_normal'
|
||||
self.task_2.parent_id = self.task_1
|
||||
self.assertEqual(
|
||||
self.task_2.state,
|
||||
'04_waiting_normal',
|
||||
"Changing the task's parent should not reset the task's state.",
|
||||
)
|
||||
|
||||
def test_state_dont_reset_when_enabling_task_dependencies(self):
|
||||
self.project_goats.allow_task_dependencies = False
|
||||
self.env.user.group_ids -= self.env.ref('project.group_project_task_dependencies')
|
||||
self.task_1.state = "03_approved"
|
||||
self.task_2.state = "02_changes_requested"
|
||||
self.project_goats.allow_task_dependencies = True
|
||||
self.env.user.group_ids += self.env.ref('project.group_project_task_dependencies')
|
||||
self.assertEqual(self.task_1.state, "03_approved")
|
||||
self.assertEqual(self.task_2.state, "02_changes_requested")
|
||||
|
||||
def test_recompute_state_when_task_dependencies_feature_changes(self):
|
||||
""" Test task state is correctly computed when the task dependencies feature changes
|
||||
|
||||
Test Case:
|
||||
---------
|
||||
1. Enable the task dependencies feature globally.
|
||||
2. Add Task 2 in the dependencies of task 1.
|
||||
3. Check task 1 as the right state ("Waiting" state expected).
|
||||
4. Disable the task dependencies feature on the project linked to the both tasks.
|
||||
5. Check the state of task 1 is correctly reset.
|
||||
6. Enable again the task dependencies feature on the project.
|
||||
7. Check task 1 state is `Waiting` state as before.
|
||||
8. Mark as done task 2
|
||||
9. Task 1 state should now be reset since all the tasks in dependencies are done
|
||||
(in that case only task 2 is in the dependencies).
|
||||
10. Change the state of task 1 to set it to `Approved` state.
|
||||
11. Disable the task dependencies feature on the project
|
||||
12. Check the state of task 1 did not change
|
||||
13. Enable again the task dependencies on the project.
|
||||
14. Check the state of task 1 did not change.
|
||||
"""
|
||||
self.assertTrue(self.project_goats.allow_task_dependencies)
|
||||
self.assertTrue(self.env.user.has_group('project.group_project_task_dependencies'))
|
||||
self.task_1.depend_on_ids = self.task_2
|
||||
self.assertEqual(self.task_1.state, '04_waiting_normal')
|
||||
self.project_goats.allow_task_dependencies = False
|
||||
self.assertEqual(self.task_1.state, '01_in_progress')
|
||||
self.project_goats.allow_task_dependencies = True
|
||||
self.assertEqual(self.task_1.state, '04_waiting_normal')
|
||||
self.task_2.state = '1_done'
|
||||
self.assertEqual(self.task_1.state, '01_in_progress')
|
||||
self.task_1.state = '03_approved'
|
||||
self.project_goats.allow_task_dependencies = False
|
||||
self.assertEqual(self.task_1.state, '03_approved')
|
||||
self.project_goats.allow_task_dependencies = True
|
||||
self.assertEqual(self.task_1.state, '03_approved')
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
class TestTaskTemplates(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.project_with_templates = cls.env["project.project"].create({
|
||||
"name": "Project with Task Template",
|
||||
})
|
||||
cls.template_task = cls.env["project.task"].create({
|
||||
"name": "Template",
|
||||
"project_id": cls.project_with_templates.id,
|
||||
"is_template": True,
|
||||
"description": "Template description",
|
||||
"partner_id": cls.partner_1.id,
|
||||
})
|
||||
cls.child_task = cls.env["project.task"].create({
|
||||
"name": "Child Task",
|
||||
"parent_id": cls.template_task.id,
|
||||
"description": "Child description",
|
||||
"partner_id": cls.partner_2.id,
|
||||
})
|
||||
|
||||
def test_create_from_template(self):
|
||||
"""
|
||||
Creating a task through the action should result in a non template copy, with no partner_id
|
||||
"""
|
||||
task_id = self.template_task.action_create_from_template()
|
||||
task = self.env["project.task"].browse(task_id)
|
||||
self.assertFalse(task.is_template, "The created task should be a normal task and not a template.")
|
||||
self.assertFalse(task.partner_id, "The created task should not have a partner.")
|
||||
|
||||
self.assertEqual(len(task.child_ids), 1, "The child of the template should be copied too.")
|
||||
child_task = task.child_ids
|
||||
self.assertFalse(child_task.is_template, "The child task should still not be a template.")
|
||||
self.assertFalse(child_task.partner_id, "The child task should also not have a partner.")
|
||||
|
||||
# With a partner set on the project, new tasks should get the partner too, even if created from a template
|
||||
self.project_with_templates.partner_id = self.partner_3
|
||||
|
||||
task_id = self.template_task.action_create_from_template()
|
||||
task = self.env["project.task"].browse(task_id)
|
||||
self.assertEqual(task.partner_id, self.partner_3, "The created task should have the same partner as the project.")
|
||||
child_task = task.child_ids
|
||||
self.assertEqual(child_task.partner_id, self.partner_3, "The child of the created task should have the same partner as the project.")
|
||||
|
||||
def test_copy_template(self):
|
||||
"""
|
||||
A copy of a template should be a template
|
||||
"""
|
||||
copied_template = self.template_task.copy()
|
||||
self.assertTrue(copied_template.is_template, "The copy of the template should also be a template.")
|
||||
self.assertEqual(len(copied_template.child_ids), 1, "The child of the template should be copied too.")
|
||||
copied_template_child_task = copied_template.child_ids
|
||||
self.assertFalse(copied_template_child_task.is_template, "The child of the copy should still not be a template.")
|
||||
|
||||
def test_copy_project_with_templates(self):
|
||||
"""
|
||||
Copying a project should also copy its task templates
|
||||
"""
|
||||
copied_project = self.project_with_templates.copy()
|
||||
task = self.env["project.task"].search([("project_id", "=", copied_project.id)], order="id asc", limit=1)
|
||||
self.assertTrue(task, "The copied project should contain a copy of the template.")
|
||||
self.assertTrue(task.is_template, "The copied template should still be a template.")
|
||||
|
||||
def test_has_template_ancestor(self):
|
||||
self.assertTrue(self.template_task.has_template_ancestor, "The template is a template.")
|
||||
self.assertTrue(self.child_task.has_template_ancestor, "The child of the template has a template ancestor.")
|
||||
|
||||
task = self.env["project.task"].create({
|
||||
"name": "Task",
|
||||
"project_id": self.project_with_templates.id,
|
||||
})
|
||||
self.assertFalse(task.has_template_ancestor, "The task does not have ancestors and is not a template.")
|
||||
|
||||
child = self.env["project.task"].create({
|
||||
"name": "Child",
|
||||
"parent_id": task.id,
|
||||
})
|
||||
self.assertFalse(child.has_template_ancestor, "The task has ancestors, but none of them are templates.")
|
||||
|
||||
self.assertCountEqual(
|
||||
self.env["project.task"].search(
|
||||
[('project_id', '=', self.project_with_templates.id), ('has_template_ancestor', '=', True)],
|
||||
),
|
||||
self.template_task | self.child_task,
|
||||
"The search should find the template and its child",
|
||||
)
|
||||
self.assertCountEqual(
|
||||
self.env["project.task"].search(
|
||||
[('project_id', '=', self.project_with_templates.id), ('has_template_ancestor', '=', False)],
|
||||
),
|
||||
task | child,
|
||||
"The search should find the non template task and its child",
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
from odoo import Command
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestTaskTemplatesTour(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.project_with_templates = cls.env["project.project"].create({
|
||||
"name": "Project with Task Template",
|
||||
"type_ids": [Command.create({
|
||||
"name": "New",
|
||||
})],
|
||||
})
|
||||
cls.template_task = cls.env["project.task"].create({
|
||||
"name": "Template",
|
||||
"project_id": cls.project_with_templates.id,
|
||||
"is_template": True,
|
||||
"description": "Template description",
|
||||
})
|
||||
|
||||
def test_task_templates_tour(self):
|
||||
self.start_tour("/odoo", "project_task_templates_tour", login="admin")
|
||||
Loading…
Add table
Add a link
Reference in a new issue