diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/project/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/project/tests')
| -rw-r--r-- | addons/project/tests/__init__.py | 9 | ||||
| -rw-r--r-- | addons/project/tests/test_access_rights.py | 228 | ||||
| -rw-r--r-- | addons/project/tests/test_multicompany.py | 292 | ||||
| -rw-r--r-- | addons/project/tests/test_project_base.py | 106 | ||||
| -rw-r--r-- | addons/project/tests/test_project_config.py | 61 | ||||
| -rw-r--r-- | addons/project/tests/test_project_flow.py | 276 | ||||
| -rw-r--r-- | addons/project/tests/test_project_recurrence.py | 460 | ||||
| -rw-r--r-- | addons/project/tests/test_project_ui.py | 10 |
8 files changed, 1442 insertions, 0 deletions
diff --git a/addons/project/tests/__init__.py b/addons/project/tests/__init__.py new file mode 100644 index 00000000..d5c89ac7 --- /dev/null +++ b/addons/project/tests/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from . import test_access_rights +from . import test_project_base +from . import test_project_config +from . import test_project_flow +from . import test_project_recurrence +from . import test_project_ui +from . import test_multicompany diff --git a/addons/project/tests/test_access_rights.py b/addons/project/tests/test_access_rights.py new file mode 100644 index 00000000..a912f357 --- /dev/null +++ b/addons/project/tests/test_access_rights.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.project.tests.test_project_base import TestProjectCommon +from odoo.exceptions import AccessError, ValidationError +from odoo.tests.common import users + + +class TestAccessRights(TestProjectCommon): + + def setUp(self): + super().setUp() + self.task = self.create_task('Make the world a better place') + self.user = mail_new_test_user(self.env, 'Internal user', groups='base.group_user') + self.portal = mail_new_test_user(self.env, 'Portal user', groups='base.group_portal') + + def create_task(self, name, *, with_user=None, **kwargs): + values = dict(name=name, project_id=self.project_pigs.id, **kwargs) + return self.env['project.task'].with_user(with_user or self.env.user).create(values) + + +class TestCRUDVisibilityFollowers(TestAccessRights): + + def setUp(self): + super().setUp() + self.project_pigs.privacy_visibility = 'followers' + + @users('Internal user', 'Portal user') + def test_project_no_write(self): + with self.assertRaises(AccessError, msg="%s should not be able to write on the project" % self.env.user.name): + self.project_pigs.with_user(self.env.user).name = "Take over the world" + + self.project_pigs.allowed_user_ids = self.env.user + with self.assertRaises(AccessError, msg="%s should not be able to write on the project" % self.env.user.name): + self.project_pigs.with_user(self.env.user).name = "Take over the world" + + @users('Internal user', 'Portal user') + def test_project_no_unlink(self): + self.project_pigs.task_ids.unlink() + with self.assertRaises(AccessError, msg="%s should not be able to unlink the project" % self.env.user.name): + self.project_pigs.with_user(self.env.user).unlink() + + self.project_pigs.allowed_user_ids = self.env.user + self.project_pigs.task_ids.unlink() + with self.assertRaises(AccessError, msg="%s should not be able to unlink the project" % self.env.user.name): + self.project_pigs.with_user(self.env.user).unlink() + + @users('Internal user', 'Portal user') + def test_project_no_read(self): + self.project_pigs.invalidate_cache() + with self.assertRaises(AccessError, msg="%s should not be able to read the project" % self.env.user.name): + self.project_pigs.with_user(self.env.user).name + + @users('Portal user') + def test_project_allowed_portal_no_read(self): + self.project_pigs.allowed_user_ids = self.env.user + self.project_pigs.invalidate_cache() + with self.assertRaises(AccessError, msg="%s should not be able to read the project" % self.env.user.name): + self.project_pigs.with_user(self.env.user).name + + @users('Internal user') + def test_project_allowed_internal_read(self): + self.project_pigs.allowed_user_ids = self.env.user + self.project_pigs.invalidate_cache() + self.project_pigs.with_user(self.env.user).name + + @users('Internal user', 'Portal user') + def test_task_no_read(self): + self.task.invalidate_cache() + with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name): + self.task.with_user(self.env.user).name + + @users('Portal user') + def test_task_allowed_portal_no_read(self): + self.project_pigs.allowed_user_ids = self.env.user + self.task.invalidate_cache() + with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name): + self.task.with_user(self.env.user).name + + @users('Internal user') + def test_task_allowed_internal_read(self): + self.project_pigs.allowed_user_ids = self.env.user + self.task.invalidate_cache() + self.task.with_user(self.env.user).name + + @users('Internal user', 'Portal user') + def test_task_no_write(self): + with self.assertRaises(AccessError, msg="%s should not be able to write on the task" % self.env.user.name): + self.task.with_user(self.env.user).name = "Paint the world in black & white" + + self.project_pigs.allowed_user_ids = self.env.user + with self.assertRaises(AccessError, msg="%s should not be able to write on the task" % self.env.user.name): + self.task.with_user(self.env.user).name = "Paint the world in black & white" + + @users('Internal user', 'Portal user') + def test_task_no_create(self): + with self.assertRaises(AccessError, msg="%s should not be able to create a task" % self.env.user.name): + self.create_task("Archive the world, it's not needed anymore") + + self.project_pigs.allowed_user_ids = self.env.user + with self.assertRaises(AccessError, msg="%s should not be able to create a task" % self.env.user.name): + self.create_task("Archive the world, it's not needed anymore") + + @users('Internal user', 'Portal user') + def test_task_no_unlink(self): + with self.assertRaises(AccessError, msg="%s should not be able to unlink the task" % self.env.user.name): + self.task.with_user(self.env.user).unlink() + + self.project_pigs.allowed_user_ids = self.env.user + with self.assertRaises(AccessError, msg="%s should not be able to unlink the task" % self.env.user.name): + self.task.with_user(self.env.user).unlink() + + +class TestCRUDVisibilityPortal(TestAccessRights): + + def setUp(self): + super().setUp() + self.project_pigs.privacy_visibility = 'portal' + + @users('Portal user') + def test_task_portal_no_read(self): + self.task.invalidate_cache() + with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name): + self.task.with_user(self.env.user).name + + @users('Portal user') + def test_task_allowed_portal_read(self): + self.project_pigs.allowed_user_ids = self.env.user + self.task.invalidate_cache() + self.task.with_user(self.env.user).name + + @users('Internal user') + def test_task_internal_read(self): + self.task.with_user(self.env.user).name + + +class TestCRUDVisibilityEmployees(TestAccessRights): + + def setUp(self): + super().setUp() + self.project_pigs.privacy_visibility = 'employees' + + @users('Portal user') + def test_task_portal_no_read(self): + self.task.invalidate_cache() + with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name): + self.task.with_user(self.env.user).name + + self.project_pigs.allowed_user_ids = self.env.user + self.task.invalidate_cache() + with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name): + self.task.with_user(self.env.user).name + + @users('Internal user') + def test_task_allowed_portal_read(self): + self.task.invalidate_cache() + self.task.with_user(self.env.user).name + + +class TestAllowedUsers(TestAccessRights): + + def setUp(self): + super().setUp() + self.project_pigs.privacy_visibility = 'followers' + + def test_project_permission_added(self): + self.project_pigs.allowed_user_ids = self.user + self.assertIn(self.user, self.task.allowed_user_ids) + + def test_project_default_permission(self): + self.project_pigs.allowed_user_ids = self.user + task = self.create_task("Review the end of the world") + self.assertIn(self.user, task.allowed_user_ids) + + def test_project_default_customer_permission(self): + self.project_pigs.privacy_visibility = 'portal' + self.project_pigs.partner_id = self.portal.partner_id + self.assertIn(self.portal, self.task.allowed_user_ids) + self.assertIn(self.portal, self.project_pigs.allowed_user_ids) + + def test_project_permission_removed(self): + self.project_pigs.allowed_user_ids = self.user + self.project_pigs.allowed_user_ids -= self.user + self.assertNotIn(self.user, self.task.allowed_user_ids) + + def test_project_specific_permission(self): + self.project_pigs.allowed_user_ids = self.user + john = mail_new_test_user(self.env, login='John') + self.task.allowed_user_ids |= john + self.project_pigs.allowed_user_ids -= self.user + self.assertIn(john, self.task.allowed_user_ids, "John should still be allowed to read the task") + + def test_project_specific_remove_mutliple_tasks(self): + self.project_pigs.allowed_user_ids = self.user + john = mail_new_test_user(self.env, login='John') + task = self.create_task('task') + self.task.allowed_user_ids |= john + self.project_pigs.allowed_user_ids -= self.user + self.assertIn(john, self.task.allowed_user_ids) + self.assertNotIn(john, task.allowed_user_ids) + self.assertNotIn(self.user, task.allowed_user_ids) + self.assertNotIn(self.user, self.task.allowed_user_ids) + + def test_no_portal_allowed(self): + with self.assertRaises(ValidationError, msg="It should not allow to add portal users"): + self.task.allowed_user_ids = self.portal + + def test_visibility_changed(self): + self.project_pigs.privacy_visibility = 'portal' + self.task.allowed_user_ids |= self.portal + self.assertNotIn(self.user, self.task.allowed_user_ids, "Internal user should have been removed from allowed users") + self.project_pigs.privacy_visibility = 'employees' + self.assertNotIn(self.portal, self.task.allowed_user_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.assertNotIn(self.user, self.project_pigs.allowed_user_ids) + self.task.allowed_user_ids = self.user + self.project_pigs.invalidate_cache() + self.task.invalidate_cache() + 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.assertNotIn(self.user, self.project_pigs.allowed_user_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!" diff --git a/addons/project/tests/test_multicompany.py b/addons/project/tests/test_multicompany.py new file mode 100644 index 00000000..5fc6b018 --- /dev/null +++ b/addons/project/tests/test_multicompany.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- + +from contextlib import contextmanager + +from odoo.tests.common import SavepointCase, Form +from odoo.exceptions import AccessError, UserError + + +class TestMultiCompanyCommon(SavepointCase): + + @classmethod + def setUpMultiCompany(cls): + + # create companies + cls.company_a = cls.env['res.company'].create({ + 'name': 'Company A' + }) + cls.company_b = cls.env['res.company'].create({ + 'name': 'Company B' + }) + + # shared customers + cls.partner_1 = cls.env['res.partner'].create({ + 'name': 'Valid Lelitre', + 'email': 'valid.lelitre@agrolait.com', + 'company_id': False, + }) + cls.partner_2 = cls.env['res.partner'].create({ + 'name': 'Valid Poilvache', + 'email': 'valid.other@gmail.com', + 'company_id': False, + }) + + # users to use through the various tests + user_group_employee = cls.env.ref('base.group_user') + Users = cls.env['res.users'].with_context({'no_reset_password': True}) + + cls.user_employee_company_a = Users.create({ + 'name': 'Employee Company A', + 'login': 'employee-a', + '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])] + }) + cls.user_manager_company_a = Users.create({ + 'name': 'Manager Company A', + 'login': 'manager-a', + '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])] + }) + cls.user_employee_company_b = Users.create({ + 'name': 'Employee Company B', + 'login': 'employee-b', + '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])] + }) + cls.user_manager_company_b = Users.create({ + 'name': 'Manager Company B', + 'login': 'manager-b', + '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])] + }) + + @contextmanager + def sudo(self, login): + old_uid = self.uid + try: + user = self.env['res.users'].sudo().search([('login', '=', login)]) + # switch user + self.uid = user.id + self.env = self.env(user=self.uid) + yield + finally: + # back + self.uid = old_uid + self.env = self.env(user=self.uid) + + @contextmanager + def allow_companies(self, company_ids): + """ The current user will be allowed in each given companies (like he can sees all of them in the company switcher and they are all checked) """ + old_allow_company_ids = self.env.user.company_ids.ids + current_user = self.env.user + try: + current_user.write({'company_ids': company_ids}) + context = dict(self.env.context, allowed_company_ids=company_ids) + self.env = self.env(user=current_user, context=context) + yield + finally: + # back + current_user.write({'company_ids': old_allow_company_ids}) + context = dict(self.env.context, allowed_company_ids=old_allow_company_ids) + self.env = self.env(user=current_user, context=context) + + @contextmanager + def switch_company(self, company): + """ Change the company in which the current user is logged """ + old_companies = self.env.context.get('allowed_company_ids', []) + try: + # switch company in context + new_companies = list(old_companies) + if company.id not in new_companies: + new_companies = [company.id] + new_companies + else: + new_companies.insert(0, new_companies.pop(new_companies.index(company.id))) + context = dict(self.env.context, allowed_company_ids=new_companies) + self.env = self.env(context=context) + yield + finally: + # back + context = dict(self.env.context, allowed_company_ids=old_companies) + self.env = self.env(context=context) + + +class TestMultiCompanyProject(TestMultiCompanyCommon): + + @classmethod + def setUpClass(cls): + super(TestMultiCompanyProject, cls).setUpClass() + + cls.setUpMultiCompany() + + user_group_project_user = cls.env.ref('project.group_project_user') + user_group_project_manager = cls.env.ref('project.group_project_manager') + + # setup users + cls.user_employee_company_a.write({ + 'groups_id': [(4, user_group_project_user.id)] + }) + cls.user_manager_company_a.write({ + 'groups_id': [(4, user_group_project_manager.id)] + }) + cls.user_employee_company_b.write({ + 'groups_id': [(4, user_group_project_user.id)] + }) + cls.user_manager_company_b.write({ + 'groups_id': [(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({ + 'name': 'Project Company A', + 'alias_name': 'project+companya', + 'partner_id': cls.partner_1.id, + 'company_id': cls.company_a.id, + 'type_ids': [ + (0, 0, { + 'name': 'New', + 'sequence': 1, + }), + (0, 0, { + 'name': 'Won', + 'sequence': 10, + }) + ] + }) + cls.project_company_b = Project.create({ + 'name': 'Project Company B', + 'alias_name': 'project+companyb', + 'partner_id': cls.partner_1.id, + 'company_id': cls.company_b.id, + 'type_ids': [ + (0, 0, { + 'name': 'New', + 'sequence': 1, + }), + (0, 0, { + 'name': 'Won', + 'sequence': 10, + }) + ] + }) + # already-existing tasks in company A and B + Task = cls.env['project.task'].with_context({'mail_create_nolog': True, 'tracking_disable': True}) + cls.task_1 = Task.create({ + 'name': 'Task 1 in Project A', + 'user_id': cls.user_employee_company_a.id, + 'project_id': cls.project_company_a.id + }) + cls.task_2 = Task.create({ + 'name': 'Task 2 in Project B', + 'user_id': cls.user_employee_company_b.id, + 'project_id': cls.project_company_b.id + }) + + def test_create_project(self): + """ Check project creation in multiple companies """ + with self.sudo('manager-a'): + project = self.env['project.project'].with_context({'tracking_disable': True}).create({ + '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") + + 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"): + project = self.env['project.project'].with_context({'tracking_disable': True}).create({ + 'name': 'Project Company B', + 'partner_id': self.partner_1.id, + 'company_id': self.company_b.id + }) + + # when allowed in other company, can create a project in another company (different from the one in which you are logged) + with self.allow_companies([self.company_a.id, self.company_b.id]): + project = self.env['project.project'].with_context({'tracking_disable': True}).create({ + 'name': 'Project Company B', + 'partner_id': self.partner_1.id, + 'company_id': self.company_b.id + }) + + def test_generate_analytic_account(self): + """ Check the analytic account generation, company propagation """ + with self.sudo('manager-b'): + 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") + + def test_create_task(self): + with self.sudo('employee-a'): + # create task, set project; the onchange will set the correct company + with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form: + task_form.name = 'Test Task in company A' + task_form.project_id = self.project_company_a + task = task_form.save() + + 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_move_task(self): + with self.sudo('employee-a'): + with self.allow_companies([self.company_a.id, self.company_b.id]): + with Form(self.task_1) as task_form: + task_form.project_id = self.project_company_b + task = task_form.save() + + self.assertEqual(task.company_id, self.company_b, "The company of the task should be the one from its project.") + + with Form(self.task_1) as task_form: + task_form.project_id = self.project_company_a + task = task_form.save() + + 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})) as task_form: + task_form.name = 'Test Subtask in company B' + task_form.parent_id = self.task_1 + task_form.project_id = self.project_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.") + + # set parent on existing orphan task; the onchange will set the correct company and subtask project + self.task_2.write({'project_id': False}) + 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() + + 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, 'subtask_project_id': self.project_company_b.id}) + + with self.sudo('employee-a'): + with self.allow_companies([self.company_a.id, self.company_b.id]): + with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form: + task_form.name = 'Test Subtask in company B' + task_form.parent_id = self.task_1 + + task = task_form.save() + + self.assertEqual(task.project_id, self.task_1.project_id.subtask_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.subtask_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" diff --git a/addons/project/tests/test_project_base.py b/addons/project/tests/test_project_base.py new file mode 100644 index 00000000..72350b1a --- /dev/null +++ b/addons/project/tests/test_project_base.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +from odoo.tests.common import SavepointCase +from odoo.exceptions import UserError + +class TestProjectCommon(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestProjectCommon, cls).setUpClass() + + 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') + + cls.partner_1 = cls.env['res.partner'].create({ + 'name': 'Valid Lelitre', + 'email': 'valid.lelitre@agrolait.com'}) + cls.partner_2 = cls.env['res.partner'].create({ + 'name': 'Valid Poilvache', + 'email': 'valid.other@gmail.com'}) + cls.partner_3 = cls.env['res.partner'].create({ + 'name': 'Valid Poilboeuf', + 'email': 'valid.poilboeuf@gmail.com'}) + + # Test users to use through the various tests + Users = cls.env['res.users'].with_context({'no_reset_password': True}) + cls.user_public = Users.create({ + 'name': 'Bert Tartignole', + 'login': 'bert', + 'email': 'b.t@example.com', + 'signature': 'SignBert', + 'notification_type': 'email', + 'groups_id': [(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])]}) + cls.user_projectuser = Users.create({ + 'name': 'Armande ProjectUser', + 'login': 'Armande', + 'email': 'armande.projectuser@example.com', + 'groups_id': [(6, 0, [user_group_employee.id, user_group_project_user.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])]}) + + # Test 'Pigs' project + cls.project_pigs = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({ + 'name': 'Pigs', + 'privacy_visibility': 'employees', + 'alias_name': 'project+pigs', + 'partner_id': cls.partner_1.id}) + # Already-existing tasks in Pigs + cls.task_1 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({ + 'name': 'Pigs UserTask', + 'user_id': cls.user_projectuser.id, + 'project_id': cls.project_pigs.id}) + cls.task_2 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({ + 'name': 'Pigs ManagerTask', + 'user_id': cls.user_projectmanager.id, + 'project_id': cls.project_pigs.id}) + + # Test 'Goats' project, same as 'Pigs', but with 2 stages + cls.project_goats = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({ + 'name': 'Goats', + 'privacy_visibility': 'followers', + 'alias_name': 'project+goats', + 'partner_id': cls.partner_1.id, + 'type_ids': [ + (0, 0, { + 'name': 'New', + 'sequence': 1, + }), + (0, 0, { + 'name': 'Won', + 'sequence': 10, + })] + }) + + 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'].with_context(mail_channel_noautofollow=True).message_process(model, mail) + return self.env[target_model].search([(target_field, '=', subject)]) + + def test_delete_project_with_tasks(self): + """User should never be able to delete a project with tasks""" + + with self.assertRaises(UserError): + self.project_pigs.unlink() + + # click on the archive button + self.project_pigs.write({'active': False}) + + with self.assertRaises(UserError): + self.project_pigs.unlink() diff --git a/addons/project/tests/test_project_config.py b/addons/project/tests/test_project_config.py new file mode 100644 index 00000000..e84c648a --- /dev/null +++ b/addons/project/tests/test_project_config.py @@ -0,0 +1,61 @@ +# -*- 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.""" + + @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"), + ) + + # 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). + """ + features_config = cls.Settings.create( + {feature[0]: is_enabled for feature in cls.features}) + features_config.execute() + + def test_existing_projects_enable_features(self): + """Check that *existing* projects have features enabled when + the user enables them in the module configuration. + """ + 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") + + 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. + """ + 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") diff --git a/addons/project/tests/test_project_flow.py b/addons/project/tests/test_project_flow.py new file mode 100644 index 00000000..b5d5e277 --- /dev/null +++ b/addons/project/tests/test_project_flow.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 + +from .test_project_base import TestProjectCommon +from odoo.tools import mute_logger +from odoo.modules.module import get_resource_path + + +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): + + 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.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[0].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[0].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[0].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.id, self.project_pigs.id, '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.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[0].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[0].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[0].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.id, self.project_goats.id, 'project_task: incorrect project') + self.assertEqual(task.stage_id.sequence, 1, "project_task: should have a stage with sequence=1") + + def test_subtask_process(self): + """ + Check subtask mecanism and change it from project. + + For this test, 2 projects are used: + - the 'pigs' project which has a partner_id + - the 'goats' project where the partner_id is removed at the beginning of the tests and then restored. + + 2 parent tasks are also used to be able to switch the parent task of a sub-task: + - 'parent_task' linked to the partner_2 + - 'another_parent_task' linked to the partner_3 + """ + + Task = self.env['project.task'].with_context({'tracking_disable': True}) + + parent_task = Task.create({ + 'name': 'Mother Task', + 'user_id': self.user_projectuser.id, + 'project_id': self.project_pigs.id, + 'partner_id': self.partner_2.id, + 'planned_hours': 12, + }) + + another_parent_task = Task.create({ + 'name': 'Another Mother Task', + 'user_id': self.user_projectuser.id, + 'project_id': self.project_pigs.id, + 'partner_id': self.partner_3.id, + 'planned_hours': 0, + }) + + # remove the partner_id of the 'goats' project + goats_partner_id = self.project_goats.partner_id + + self.project_goats.write({ + 'partner_id': False + }) + + # the child task 1 is linked to a project without partner_id (goats project) + child_task_1 = Task.create({ + 'name': 'Task Child with project', + 'parent_id': parent_task.id, + 'project_id': self.project_goats.id, + 'planned_hours': 3, + }) + + # the child task 2 is linked to a project with a partner_id (pigs project) + child_task_2 = Task.create({ + 'name': 'Task Child without project', + 'parent_id': parent_task.id, + 'project_id': self.project_pigs.id, + 'planned_hours': 5, + }) + + self.assertEqual( + child_task_1.partner_id, child_task_1.parent_id.partner_id, + "When no project partner_id has been set, a subtask should have the same partner as its parent") + + self.assertEqual( + child_task_2.partner_id, child_task_2.project_id.partner_id, + "When a project partner_id has been set, a subtask should have the same partner as its project") + + self.assertEqual( + parent_task.subtask_count, 2, + "Parent task should have 2 children") + + self.assertEqual( + parent_task.subtask_planned_hours, 8, + "Planned hours of subtask should impact parent task") + + # change the parent of a subtask without a project partner_id + child_task_1.write({ + 'parent_id': another_parent_task.id + }) + + self.assertEqual( + child_task_1.partner_id, parent_task.partner_id, + "When changing the parent task of a subtask with no project partner_id, the partner_id should remain the same.") + + # change the parent of a subtask with a project partner_id + child_task_2.write({ + 'parent_id': another_parent_task.id + }) + + self.assertEqual( + child_task_2.partner_id, child_task_2.project_id.partner_id, + "When changing the parent task of a subtask with a project, the partner_id should remain the same.") + + # set a project with partner_id to a subtask without project partner_id + child_task_1.write({ + 'project_id': self.project_pigs.id + }) + + self.assertEqual( + child_task_1.partner_id, self.project_pigs.partner_id, + "When the project changes, the subtask should have the same partner id as the new project.") + + # restore the partner_id of the 'goats' project + self.project_goats.write({ + 'partner_id': goats_partner_id + }) + + # set a project with partner_id to a subtask with a project partner_id + child_task_2.write({ + 'project_id': self.project_goats.id + }) + + self.assertEqual( + child_task_2.partner_id, self.project_goats.partner_id, + "When the project changes, the subtask should have the same partner id as the new project.") + + def test_rating(self): + """Check if rating works correctly even when task is changed from project A to project B""" + Task = self.env['project.task'].with_context({'tracking_disable': True}) + first_task = Task.create({ + 'name': 'first task', + 'user_id': self.user_projectuser.id, + 'project_id': self.project_pigs.id, + 'partner_id': self.partner_2.id, + }) + + self.assertEqual(first_task.rating_count, 0, "Task should have no rating associated with it") + + rating_good = self.env['rating.rating'].create({ + 'res_model_id': self.env['ir.model']._get('project.task').id, + 'res_id': first_task.id, + 'parent_res_model_id': self.env['ir.model']._get('project.project').id, + 'parent_res_id': self.project_pigs.id, + 'rated_partner_id': self.partner_2.id, + 'partner_id': self.partner_2.id, + 'rating': 5, + 'consumed': False, + }) + + rating_bad = self.env['rating.rating'].create({ + 'res_model_id': self.env['ir.model']._get('project.task').id, + 'res_id': first_task.id, + 'parent_res_model_id': self.env['ir.model']._get('project.project').id, + 'parent_res_id': self.project_pigs.id, + 'rated_partner_id': self.partner_2.id, + 'partner_id': self.partner_2.id, + 'rating': 3, + 'consumed': True, + }) + + # We need to invalidate cache since it is not done automatically by the ORM + # Our One2Many is linked to a res_id (int) for which the orm doesn't create an inverse + first_task.invalidate_cache() + + self.assertEqual(rating_good.rating_text, 'satisfied') + self.assertEqual(rating_bad.rating_text, 'not_satisfied') + self.assertEqual(first_task.rating_count, 1, "Task should have only one rating associated, since one is not consumed") + self.assertEqual(rating_good.parent_res_id, self.project_pigs.id) + + self.assertEqual(self.project_goats.rating_percentage_satisfaction, -1) + self.assertEqual(self.project_pigs.rating_percentage_satisfaction, 0) # There is a rating but not a "great" on, just an "okay". + + # Consuming rating_good + first_task.rating_apply(5, rating_good.access_token) + + # We need to invalidate cache since it is not done automatically by the ORM + # Our One2Many is linked to a res_id (int) for which the orm doesn't create an inverse + first_task.invalidate_cache() + + self.assertEqual(first_task.rating_count, 2, "Task should have two ratings associated with it") + self.assertEqual(rating_good.parent_res_id, self.project_pigs.id) + self.assertEqual(self.project_goats.rating_percentage_satisfaction, -1) + self.assertEqual(self.project_pigs.rating_percentage_satisfaction, 50) + + # We change the task from project_pigs to project_goats, ratings should be associated with the new project + first_task.project_id = self.project_goats.id + + # We need to invalidate cache since it is not done automatically by the ORM + # Our One2Many is linked to a res_id (int) for which the orm doesn't create an inverse + first_task.invalidate_cache() + + self.assertEqual(rating_good.parent_res_id, self.project_goats.id) + self.assertEqual(self.project_goats.rating_percentage_satisfaction, 50) + self.assertEqual(self.project_pigs.rating_percentage_satisfaction, -1) diff --git a/addons/project/tests/test_project_recurrence.py b/addons/project/tests/test_project_recurrence.py new file mode 100644 index 00000000..63090189 --- /dev/null +++ b/addons/project/tests/test_project_recurrence.py @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- + + +from odoo.tests.common import SavepointCase, Form +from odoo.exceptions import ValidationError +from odoo import fields + +from datetime import date, datetime +from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU +from freezegun import freeze_time + + +class TestProjectrecurrence(SavepointCase): + @classmethod + def setUpClass(cls): + super(TestProjectrecurrence, cls).setUpClass() + + cls.env.user.groups_id += cls.env.ref('project.group_project_recurring_tasks') + + cls.stage_a = cls.env['project.task.type'].create({'name': 'a'}) + cls.stage_b = cls.env['project.task.type'].create({'name': 'b'}) + cls.project_recurring = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({ + 'name': 'Recurring', + 'allow_recurring_tasks': True, + 'type_ids': [ + (4, cls.stage_a.id), + (4, cls.stage_b.id), + ] + }) + + def set_task_create_date(self, task_id, create_date): + self.env.cr.execute("UPDATE project_task SET create_date=%s WHERE id=%s", (create_date, task_id)) + + def test_recurrence_simple(self): + with freeze_time("2020-02-01"): + with Form(self.env['project.task']) as form: + form.name = 'test recurring task' + form.project_id = self.project_recurring + + form.recurring_task = True + form.repeat_interval = 5 + form.repeat_unit = 'month' + form.repeat_type = 'after' + form.repeat_number = 10 + form.repeat_on_month = 'date' + form.repeat_day = '31' + task = form.save() + self.assertTrue(bool(task.recurrence_id), 'should create a recurrence') + + task.write(dict(repeat_interval=2, repeat_number=11)) + self.assertEqual(task.recurrence_id.repeat_interval, 2, 'recurrence should be updated') + self.assertEqual(task.recurrence_id.repeat_number, 11, 'recurrence should be updated') + self.assertEqual(task.recurrence_id.recurrence_left, 11) + self.assertEqual(task.recurrence_id.next_recurrence_date, date(2020, 2, 29)) + + task.recurring_task = False + self.assertFalse(bool(task.recurrence_id), 'the recurrence should be deleted') + + def test_recurrence_cron_repeat_after(self): + domain = [('project_id', '=', self.project_recurring.id)] + with freeze_time("2020-01-01"): + form = Form(self.env['project.task']) + form.name = 'test recurring task' + form.description = 'my super recurring task bla bla bla' + form.project_id = self.project_recurring + form.date_deadline = datetime(2020, 2, 1) + + form.recurring_task = True + form.repeat_interval = 1 + form.repeat_unit = 'month' + form.repeat_type = 'after' + form.repeat_number = 2 + form.repeat_on_month = 'date' + form.repeat_day = '15' + task = form.save() + task.planned_hours = 2 + + self.assertEqual(task.recurrence_id.next_recurrence_date, date(2020, 1, 15)) + self.assertEqual(self.env['project.task'].search_count(domain), 1) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 1, 'no extra task should be created') + self.assertEqual(task.recurrence_id.recurrence_left, 2) + + with freeze_time("2020-01-15"): + self.assertEqual(self.env['project.task'].search_count(domain), 1) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 2) + self.assertEqual(task.recurrence_id.recurrence_left, 1) + + with freeze_time("2020-02-15"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 3) + self.assertEqual(task.recurrence_id.recurrence_left, 0) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 3) + self.assertEqual(task.recurrence_id.recurrence_left, 0) + + + tasks = self.env['project.task'].search(domain) + self.assertEqual(len(tasks), 3) + + self.assertTrue(bool(tasks[2].date_deadline)) + self.assertFalse(tasks[1].date_deadline, "Deadline should not be copied") + + for f in self.env['project.task.recurrence']._get_recurring_fields(): + self.assertTrue(tasks[0][f] == tasks[1][f] == tasks[2][f], "Field %s should have been copied" % f) + + def test_recurrence_cron_repeat_until(self): + domain = [('project_id', '=', self.project_recurring.id)] + with freeze_time("2020-01-01"): + form = Form(self.env['project.task']) + form.name = 'test recurring task' + form.description = 'my super recurring task bla bla bla' + form.project_id = self.project_recurring + form.date_deadline = datetime(2020, 2, 1) + + form.recurring_task = True + form.repeat_interval = 1 + form.repeat_unit = 'month' + form.repeat_type = 'until' + form.repeat_until = date(2020, 2, 20) + form.repeat_on_month = 'date' + form.repeat_day = '15' + task = form.save() + task.planned_hours = 2 + + self.assertEqual(task.recurrence_id.next_recurrence_date, date(2020, 1, 15)) + self.assertEqual(self.env['project.task'].search_count(domain), 1) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 1, 'no extra task should be created') + + with freeze_time("2020-01-15"): + self.assertEqual(self.env['project.task'].search_count(domain), 1) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 2) + + with freeze_time("2020-02-15"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 3) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 3) + + tasks = self.env['project.task'].search(domain) + self.assertEqual(len(tasks), 3) + + self.assertTrue(bool(tasks[2].date_deadline)) + self.assertFalse(tasks[1].date_deadline, "Deadline should not be copied") + + for f in self.env['project.task.recurrence']._get_recurring_fields(): + self.assertTrue(tasks[0][f] == tasks[1][f] == tasks[2][f], "Field %s should have been copied" % f) + + def test_recurrence_cron_repeat_forever(self): + domain = [('project_id', '=', self.project_recurring.id)] + with freeze_time("2020-01-01"): + form = Form(self.env['project.task']) + form.name = 'test recurring task' + form.description = 'my super recurring task bla bla bla' + form.project_id = self.project_recurring + form.date_deadline = datetime(2020, 2, 1) + + form.recurring_task = True + form.repeat_interval = 1 + form.repeat_unit = 'month' + form.repeat_type = 'forever' + form.repeat_on_month = 'date' + form.repeat_day = '15' + task = form.save() + task.planned_hours = 2 + + self.assertEqual(task.recurrence_id.next_recurrence_date, date(2020, 1, 15)) + self.assertEqual(self.env['project.task'].search_count(domain), 1) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 1, 'no extra task should be created') + + with freeze_time("2020-01-15"): + self.assertEqual(self.env['project.task'].search_count(domain), 1) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 2) + + with freeze_time("2020-02-15"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 3) + + with freeze_time("2020-02-16"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 3) + + with freeze_time("2020-02-17"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 3) + + with freeze_time("2020-02-17"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 3) + + with freeze_time("2020-03-15"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 4) + + tasks = self.env['project.task'].search(domain) + self.assertEqual(len(tasks), 4) + + self.assertTrue(bool(tasks[3].date_deadline)) + self.assertFalse(tasks[1].date_deadline, "Deadline should not be copied") + + for f in self.env['project.task.recurrence']._get_recurring_fields(): + self.assertTrue( + tasks[0][f] == tasks[1][f] == tasks[2][f] == tasks[3][f], + "Field %s should have been copied" % f) + + def test_recurrence_update_task(self): + with freeze_time("2020-01-01"): + task = self.env['project.task'].create({ + 'name': 'test recurring task', + 'project_id': self.project_recurring.id, + 'recurring_task': True, + 'repeat_interval': 1, + 'repeat_unit': 'week', + 'repeat_type': 'after', + 'repeat_number': 2, + 'mon': True, + }) + + with freeze_time("2020-01-06"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + + with freeze_time("2020-01-13"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + + task_c, task_b, task_a = self.env['project.task'].search([('project_id', '=', self.project_recurring.id)]) + + self.set_task_create_date(task_a.id, datetime(2020, 1, 1)) + self.set_task_create_date(task_b.id, datetime(2020, 1, 6)) + self.set_task_create_date(task_c.id, datetime(2020, 1, 13)) + (task_a+task_b+task_c).invalidate_cache() + + task_c.write({ + 'name': 'my super updated task', + 'recurrence_update': 'all', + }) + + self.assertEqual(task_a.name, 'my super updated task') + self.assertEqual(task_b.name, 'my super updated task') + self.assertEqual(task_c.name, 'my super updated task') + + task_a.write({ + 'name': 'don\'t you dare change my title', + 'recurrence_update': 'this', + }) + + self.assertEqual(task_a.name, 'don\'t you dare change my title') + self.assertEqual(task_b.name, 'my super updated task') + self.assertEqual(task_c.name, 'my super updated task') + + task_b.write({ + 'description': 'hello!', + 'recurrence_update': 'subsequent', + }) + + self.assertEqual(task_a.description, False) + self.assertEqual(task_b.description, '<p>hello!</p>') + self.assertEqual(task_c.description, '<p>hello!</p>') + + def test_recurrence_fields_visibility(self): + form = Form(self.env['project.task']) + + form.name = 'test recurring task' + form.project_id = self.project_recurring + form.recurring_task = True + + form.repeat_unit = 'week' + self.assertTrue(form.repeat_show_dow) + self.assertFalse(form.repeat_show_day) + self.assertFalse(form.repeat_show_week) + self.assertFalse(form.repeat_show_month) + + form.repeat_unit = 'month' + form.repeat_on_month = 'date' + self.assertFalse(form.repeat_show_dow) + self.assertTrue(form.repeat_show_day) + self.assertFalse(form.repeat_show_week) + self.assertFalse(form.repeat_show_month) + + form.repeat_unit = 'month' + form.repeat_on_month = 'day' + self.assertFalse(form.repeat_show_dow) + self.assertFalse(form.repeat_show_day) + self.assertTrue(form.repeat_show_week) + self.assertFalse(form.repeat_show_month) + + form.repeat_unit = 'year' + form.repeat_on_year = 'date' + self.assertFalse(form.repeat_show_dow) + self.assertTrue(form.repeat_show_day) + self.assertFalse(form.repeat_show_week) + self.assertTrue(form.repeat_show_month) + + form.repeat_unit = 'year' + form.repeat_on_year = 'day' + self.assertFalse(form.repeat_show_dow) + self.assertFalse(form.repeat_show_day) + self.assertTrue(form.repeat_show_week) + self.assertTrue(form.repeat_show_month) + + form.recurring_task = False + self.assertFalse(form.repeat_show_dow) + self.assertFalse(form.repeat_show_day) + self.assertFalse(form.repeat_show_week) + self.assertFalse(form.repeat_show_month) + + def test_recurrence_week_day(self): + form = Form(self.env['project.task']) + + form.name = 'test recurring task' + form.project_id = self.project_recurring + form.recurring_task = True + form.repeat_unit = 'week' + + form.mon = False + form.tue = False + form.wed = False + form.thu = False + form.fri = False + form.sat = False + form.sun = False + + with self.assertRaises(ValidationError), self.cr.savepoint(): + form.save() + + def test_recurrence_next_dates_week(self): + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2020, 1, 1), + repeat_interval=1, + repeat_unit='week', + repeat_type=False, + repeat_until=False, + repeat_on_month=False, + repeat_on_year=False, + weekdays=False, + repeat_day=False, + repeat_week=False, + repeat_month=False, + count=5) + + self.assertEqual(dates[0], datetime(2020, 1, 6, 0, 0)) + self.assertEqual(dates[1], datetime(2020, 1, 13, 0, 0)) + self.assertEqual(dates[2], datetime(2020, 1, 20, 0, 0)) + self.assertEqual(dates[3], datetime(2020, 1, 27, 0, 0)) + self.assertEqual(dates[4], datetime(2020, 2, 3, 0, 0)) + + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2020, 1, 1), + repeat_interval=3, + repeat_unit='week', + repeat_type='until', + repeat_until=date(2020, 2, 1), + repeat_on_month=False, + repeat_on_year=False, + weekdays=[MO, FR], + repeat_day=False, + repeat_week=False, + repeat_month=False, + count=100) + + self.assertEqual(len(dates), 3) + self.assertEqual(dates[0], datetime(2020, 1, 3, 0, 0)) + self.assertEqual(dates[1], datetime(2020, 1, 20, 0, 0)) + self.assertEqual(dates[2], datetime(2020, 1, 24, 0, 0)) + + def test_recurrence_next_dates_month(self): + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2020, 1, 15), + repeat_interval=1, + repeat_unit='month', + repeat_type=False, # Forever + repeat_until=False, + repeat_on_month='date', + repeat_on_year=False, + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month=False, + count=12) + + # should take the last day of each month + self.assertEqual(dates[0], date(2020, 1, 31)) + self.assertEqual(dates[1], date(2020, 2, 29)) + self.assertEqual(dates[2], date(2020, 3, 31)) + self.assertEqual(dates[3], date(2020, 4, 30)) + self.assertEqual(dates[4], date(2020, 5, 31)) + self.assertEqual(dates[5], date(2020, 6, 30)) + self.assertEqual(dates[6], date(2020, 7, 31)) + self.assertEqual(dates[7], date(2020, 8, 31)) + self.assertEqual(dates[8], date(2020, 9, 30)) + self.assertEqual(dates[9], date(2020, 10, 31)) + self.assertEqual(dates[10], date(2020, 11, 30)) + self.assertEqual(dates[11], date(2020, 12, 31)) + + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2020, 2, 20), + repeat_interval=3, + repeat_unit='month', + repeat_type=False, # Forever + repeat_until=False, + repeat_on_month='date', + repeat_on_year=False, + weekdays=False, + repeat_day=29, + repeat_week=False, + repeat_month=False, + count=5) + + self.assertEqual(dates[0], date(2020, 2, 29)) + self.assertEqual(dates[1], date(2020, 5, 29)) + self.assertEqual(dates[2], date(2020, 8, 29)) + self.assertEqual(dates[3], date(2020, 11, 29)) + self.assertEqual(dates[4], date(2021, 2, 28)) + + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2020, 1, 10), + repeat_interval=1, + repeat_unit='month', + repeat_type='until', + repeat_until=datetime(2020, 5, 31), + repeat_on_month='day', + repeat_on_year=False, + weekdays=[SA(4), ], # 4th Saturday + repeat_day=29, + repeat_week=False, + repeat_month=False, + count=6) + + self.assertEqual(len(dates), 5) + self.assertEqual(dates[0], datetime(2020, 1, 25)) + self.assertEqual(dates[1], datetime(2020, 2, 22)) + self.assertEqual(dates[2], datetime(2020, 3, 28)) + self.assertEqual(dates[3], datetime(2020, 4, 25)) + self.assertEqual(dates[4], datetime(2020, 5, 23)) + + def test_recurrence_next_dates_year(self): + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2020, 12, 1), + repeat_interval=1, + repeat_unit='year', + repeat_type='until', + repeat_until=datetime(2026, 1, 1), + repeat_on_month=False, + repeat_on_year='date', + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month='november', + count=10) + + self.assertEqual(len(dates), 5) + self.assertEqual(dates[0], datetime(2021, 11, 30)) + self.assertEqual(dates[1], datetime(2022, 11, 30)) + self.assertEqual(dates[2], datetime(2023, 11, 30)) + self.assertEqual(dates[3], datetime(2024, 11, 30)) + self.assertEqual(dates[4], datetime(2025, 11, 30)) diff --git a/addons/project/tests/test_project_ui.py b/addons/project/tests/test_project_ui.py new file mode 100644 index 00000000..9fd5ee2c --- /dev/null +++ b/addons/project/tests/test_project_ui.py @@ -0,0 +1,10 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo.tests + + +@odoo.tests.tagged('post_install', '-at_install') +class TestUi(odoo.tests.HttpCase): + + def test_01_project_tour(self): + self.start_tour("/web", 'project_tour', login="admin") |
