summaryrefslogtreecommitdiff
path: root/addons/project/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/project/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/project/tests')
-rw-r--r--addons/project/tests/__init__.py9
-rw-r--r--addons/project/tests/test_access_rights.py228
-rw-r--r--addons/project/tests/test_multicompany.py292
-rw-r--r--addons/project/tests/test_project_base.py106
-rw-r--r--addons/project/tests/test_project_config.py61
-rw-r--r--addons/project/tests/test_project_flow.py276
-rw-r--r--addons/project/tests/test_project_recurrence.py460
-rw-r--r--addons/project/tests/test_project_ui.py10
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")