mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-19 08:02:01 +02:00
Initial commit: Project packages
This commit is contained in:
commit
89613c97b0
753 changed files with 496325 additions and 0 deletions
28
odoo-bringout-oca-ocb-project/project/tests/__init__.py
Normal file
28
odoo-bringout-oca-ocb-project/project/tests/__init__.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_access_rights
|
||||
from . import test_burndown_chart
|
||||
from . import test_project_base
|
||||
from . import test_project_config
|
||||
from . import test_project_flow
|
||||
from . import test_project_milestone
|
||||
from . import test_project_profitability
|
||||
from . import test_project_recurrence
|
||||
from . import test_project_sharing
|
||||
from . import test_project_sharing_portal_access
|
||||
from . import test_project_sharing_ui
|
||||
from . import test_project_subtasks
|
||||
from . import test_project_tags_filter
|
||||
from . import test_project_task_type
|
||||
from . import test_project_ui
|
||||
from . import test_project_update_access_rights
|
||||
from . import test_project_update_flow
|
||||
from . import test_project_update_ui
|
||||
from . import test_portal
|
||||
from . import test_multicompany
|
||||
from . import test_personal_stages
|
||||
from . import test_res_config_settings
|
||||
from . import test_task_dependencies
|
||||
from . import test_task_follow
|
||||
from . import test_task_tracking
|
||||
from . import test_project_report
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
# -*- 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 import Command
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
from odoo.tests.common import users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
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):
|
||||
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.privacy_visibility = 'portal'
|
||||
self.project_pigs.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
self.project_pigs.privacy_visibility = 'followers'
|
||||
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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
self.project_pigs.flush_model()
|
||||
self.project_pigs.invalidate_model()
|
||||
self.project_pigs.with_user(self.env.user).name
|
||||
|
||||
@users('Internal user', 'Portal user')
|
||||
def test_task_no_read(self):
|
||||
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.privacy_visibility = 'portal'
|
||||
self.project_pigs.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
self.project_pigs.privacy_visibility = 'followers'
|
||||
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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
self.task.flush_model()
|
||||
self.task.invalidate_model()
|
||||
self.task.with_user(self.env.user).name
|
||||
|
||||
@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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
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'
|
||||
self.env.flush_all()
|
||||
|
||||
@users('Portal user')
|
||||
def test_task_portal_no_read(self):
|
||||
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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
self.task.flush_model()
|
||||
self.task.invalidate_model()
|
||||
self.task.with_user(self.env.user).name
|
||||
|
||||
@users('Internal user')
|
||||
def test_task_internal_read(self):
|
||||
self.task.flush_model()
|
||||
self.task.invalidate_model()
|
||||
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):
|
||||
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.message_subscribe(partner_ids=[self.env.user.partner_id.id])
|
||||
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.flush_model()
|
||||
self.task.invalidate_model()
|
||||
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.message_subscribe(partner_ids=[self.user.partner_id.id])
|
||||
self.assertIn(self.user.partner_id, self.project_pigs.message_partner_ids)
|
||||
# Subscribing to a project should not cause subscription to existing tasks in the project.
|
||||
self.assertNotIn(self.user.partner_id, self.task.message_partner_ids)
|
||||
|
||||
def test_project_default_permission(self):
|
||||
self.project_pigs.message_subscribe(partner_ids=[self.user.partner_id.id])
|
||||
created_task = self.create_task("Review the end of the world")
|
||||
# Subscribing to a project should cause subscription to new tasks in the project.
|
||||
self.assertIn(self.user.partner_id, created_task.message_partner_ids)
|
||||
|
||||
def test_project_default_customer_permission(self):
|
||||
self.project_pigs.privacy_visibility = 'portal'
|
||||
self.project_pigs.message_subscribe(partner_ids=[self.portal.partner_id.id])
|
||||
# Subscribing a default customer to a project should not cause its subscription to existing tasks in the project.
|
||||
self.assertNotIn(self.portal.partner_id, self.task.message_partner_ids)
|
||||
self.assertIn(self.portal.partner_id, self.project_pigs.message_partner_ids)
|
||||
|
||||
def test_project_permission_removed(self):
|
||||
self.project_pigs.message_subscribe(partner_ids=[self.user.partner_id.id])
|
||||
self.project_pigs.message_unsubscribe(partner_ids=[self.user.partner_id.id])
|
||||
# Unsubscribing to a project should not cause unsubscription of existing tasks in the project.
|
||||
self.assertNotIn(self.user.partner_id, self.project_pigs.message_partner_ids)
|
||||
|
||||
def test_project_specific_permission(self):
|
||||
self.project_pigs.message_subscribe(partner_ids=[self.user.partner_id.id])
|
||||
john = mail_new_test_user(self.env, 'John')
|
||||
self.project_pigs.message_subscribe(partner_ids=[john.partner_id.id])
|
||||
self.project_pigs.message_unsubscribe(partner_ids=[self.user.partner_id.id])
|
||||
# User specific subscribing to a project should not cause its subscription to existing tasks in the project.
|
||||
self.assertNotIn(john.partner_id, self.task.message_partner_ids, "John should not be allowed to read the task")
|
||||
task = self.create_task("New task")
|
||||
self.assertIn(john.partner_id, task.message_partner_ids, "John should allowed to read the task")
|
||||
|
||||
def test_project_specific_remove_mutliple_tasks(self):
|
||||
self.project_pigs.message_subscribe(partner_ids=[self.user.partner_id.id])
|
||||
john = mail_new_test_user(self.env, 'John')
|
||||
task = self.create_task('task')
|
||||
self.task.message_subscribe(partner_ids=[john.partner_id.id])
|
||||
self.project_pigs.message_unsubscribe(partner_ids=[self.user.partner_id.id])
|
||||
self.assertIn(john.partner_id, self.task.message_partner_ids)
|
||||
self.assertNotIn(john.partner_id, task.message_partner_ids)
|
||||
# Unsubscribing to a project should not cause unsubscription of existing tasks in the project.
|
||||
self.assertIn(self.user.partner_id, task.message_partner_ids)
|
||||
self.assertNotIn(self.user.partner_id, self.task.message_partner_ids)
|
||||
|
||||
def test_visibility_changed(self):
|
||||
self.project_pigs.privacy_visibility = 'portal'
|
||||
self.task.message_subscribe(partner_ids=[self.portal.partner_id.id])
|
||||
self.assertNotIn(self.user.partner_id, self.task.message_partner_ids, "Internal user should have been removed from allowed users")
|
||||
self.project_pigs.write({'privacy_visibility': 'employees'})
|
||||
self.assertNotIn(self.portal.partner_id, self.task.message_partner_ids, "Portal user should have been removed from allowed users")
|
||||
|
||||
def test_write_task(self):
|
||||
self.user.groups_id |= self.env.ref('project.group_project_user')
|
||||
self.assertNotIn(self.user.partner_id, self.project_pigs.message_partner_ids)
|
||||
self.task.message_subscribe(partner_ids=[self.user.partner_id.id])
|
||||
self.project_pigs.invalidate_model()
|
||||
self.task.invalidate_model()
|
||||
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.partner_id, self.project_pigs.message_partner_ids)
|
||||
with self.assertRaises(AccessError, msg="User should not be able to edit project"):
|
||||
self.project_pigs.with_user(self.user).name = "I can't edit a task!"
|
||||
|
||||
class TestProjectPortalCommon(TestProjectCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestProjectPortalCommon, self).setUp()
|
||||
self.user_noone = self.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True}).create({
|
||||
'name': 'Noemie NoOne',
|
||||
'login': 'noemie',
|
||||
'email': 'n.n@example.com',
|
||||
'signature': '--\nNoemie',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [(6, 0, [])]})
|
||||
|
||||
self.task_3 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test3', 'user_ids': self.user_portal, 'project_id': self.project_pigs.id})
|
||||
self.task_4 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test4', 'user_ids': self.user_public, 'project_id': self.project_pigs.id})
|
||||
self.task_5 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test5', 'user_ids': False, 'project_id': self.project_pigs.id})
|
||||
self.task_6 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test5', 'user_ids': False, 'project_id': self.project_pigs.id})
|
||||
|
||||
class TestPortalProject(TestProjectPortalCommon):
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_employee_project_access_rights(self):
|
||||
pigs = self.project_pigs
|
||||
|
||||
pigs.write({'privacy_visibility': 'employees'})
|
||||
# Do: Alfred reads project -> ok (employee ok employee)
|
||||
pigs.with_user(self.user_projectuser).read(['user_id'])
|
||||
# Test: all project tasks visible
|
||||
tasks = self.env['project.task'].with_user(self.user_projectuser).search([('project_id', '=', pigs.id)])
|
||||
test_task_ids = set([self.task_1.id, self.task_2.id, self.task_3.id, self.task_4.id, self.task_5.id, self.task_6.id])
|
||||
self.assertEqual(set(tasks.ids), test_task_ids,
|
||||
'access rights: project user cannot see all tasks of an employees project')
|
||||
# Do: Bert reads project -> crash, no group
|
||||
self.assertRaises(AccessError, pigs.with_user(self.user_noone).read, ['user_id'])
|
||||
# Do: Donovan reads project -> ko (public ko employee)
|
||||
self.assertRaises(AccessError, pigs.with_user(self.user_public).read, ['user_id'])
|
||||
# Do: project user is employee and can create a task
|
||||
tmp_task = self.env['project.task'].with_user(self.user_projectuser).with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Pigs task',
|
||||
'project_id': pigs.id})
|
||||
tmp_task.with_user(self.user_projectuser).unlink()
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_favorite_project_access_rights(self):
|
||||
pigs = self.project_pigs.with_user(self.user_projectuser)
|
||||
|
||||
# we can't write on project name
|
||||
self.assertRaises(AccessError, pigs.write, {'name': 'False Pigs'})
|
||||
# we can write on is_favorite
|
||||
pigs.write({'is_favorite': True})
|
||||
|
||||
@mute_logger('odoo.addons.base.ir.ir_model')
|
||||
def test_followers_project_access_rights(self):
|
||||
pigs = self.project_pigs
|
||||
pigs.write({'privacy_visibility': 'followers'})
|
||||
# Do: Alfred reads project -> ko (employee ko followers)
|
||||
self.assertRaises(AccessError, pigs.with_user(self.user_projectuser).read, ['user_id'])
|
||||
# Test: no project task visible
|
||||
tasks = self.env['project.task'].with_user(self.user_projectuser).search([('project_id', '=', pigs.id)])
|
||||
self.assertEqual(tasks, self.task_1,
|
||||
'access rights: employee user should not see tasks of a not-followed followers project, only assigned')
|
||||
|
||||
# Do: Bert reads project -> crash, no group
|
||||
self.assertRaises(AccessError, pigs.with_user(self.user_noone).read, ['user_id'])
|
||||
|
||||
# Do: Donovan reads project -> ko (public ko employee)
|
||||
self.assertRaises(AccessError, pigs.with_user(self.user_public).read, ['user_id'])
|
||||
|
||||
pigs.message_subscribe(partner_ids=[self.user_projectuser.partner_id.id])
|
||||
|
||||
# Do: Alfred reads project -> ok (follower ok followers)
|
||||
donkey = pigs.with_user(self.user_projectuser)
|
||||
donkey.invalidate_model()
|
||||
donkey.read(['user_id'])
|
||||
|
||||
# Do: Donovan reads project -> ko (public ko follower even if follower)
|
||||
self.assertRaises(AccessError, pigs.with_user(self.user_public).read, ['user_id'])
|
||||
# Do: project user is follower of the project and can create a task
|
||||
self.env['project.task'].with_user(self.user_projectuser).with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Pigs task', 'project_id': pigs.id
|
||||
})
|
||||
# not follower user should not be able to create a task
|
||||
pigs.with_user(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_projectuser.partner_id.id])
|
||||
self.assertRaises(AccessError, self.env['project.task'].with_user(self.user_projectuser).with_context({
|
||||
'mail_create_nolog': True}).create, {'name': 'Pigs task', 'project_id': pigs.id})
|
||||
|
||||
# Do: project user can create a task without project
|
||||
self.assertRaises(AccessError, self.env['project.task'].with_user(self.user_projectuser).with_context({
|
||||
'mail_create_nolog': True}).create, {'name': 'Pigs task', 'project_id': pigs.id})
|
||||
|
||||
|
||||
class TestAccessRightsPrivateTask(TestAccessRights):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.private_task = cls.env['project.task'].create({'name': 'OdooBot Private Task'})
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.project_user = mail_new_test_user(self.env, 'Project user', groups='project.group_project_user')
|
||||
|
||||
def create_private_task(self, name, with_user=None, **kwargs):
|
||||
user = with_user or self.env.user
|
||||
values = {'name': name, 'user_ids': [Command.set(user.ids)], **kwargs}
|
||||
return self.env['project.task'].with_user(user).create(values)
|
||||
|
||||
@users('Internal user', 'Portal user')
|
||||
def test_internal_cannot_crud_private_task(self):
|
||||
with self.assertRaises(AccessError):
|
||||
self.create_private_task('Private task')
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).write({'name': 'Test write'})
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).unlink()
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).read(['name'])
|
||||
|
||||
@users('Project user')
|
||||
def test_project_user_crud_own_private_task(self):
|
||||
private_task = self.create_private_task('Private task')
|
||||
|
||||
private_task.with_user(self.env.user).write({'name': 'Test write'})
|
||||
vals = private_task.with_user(self.env.user).read(['name'])
|
||||
self.assertEqual(vals[0]['id'], private_task.id)
|
||||
self.assertEqual(vals[0]['name'], private_task.name)
|
||||
|
||||
@users('Project user')
|
||||
def test_project_user_can_create_private_task_for_another_user(self):
|
||||
self.create_private_task('Private task', user_ids=[Command.set(self.user_projectuser.ids)])
|
||||
|
||||
@users('Project user')
|
||||
def test_project_current_user_is_added_in_private_task_assignees(self):
|
||||
task_values = {'name': 'Private task'}
|
||||
my_private_task = self.env['project.task'].create(task_values)
|
||||
self.assertEqual(my_private_task.user_ids, self.env.user, 'When no assignee is set on a private task, the task should be assigned to the current user.')
|
||||
user_projectuser_private_task = self.env['project.task'].create({**task_values, 'user_ids': [Command.set(self.user_projectuser.ids)]})
|
||||
self.assertTrue(self.env.user in user_projectuser_private_task.user_ids, 'When creating a private task for another user, the current user should be added to the assignees.')
|
||||
|
||||
@users('Project user')
|
||||
def test_project_current_user_is_added_in_task_assignees_when_project_id_is_set(self):
|
||||
task_values = {'name': 'Private task', 'project_id': self.project_pigs.id, 'user_ids': [Command.set(self.user_projectuser.ids)]}
|
||||
user_projectuser_task = self.env['project.task'].create(task_values)
|
||||
self.assertFalse(self.env.user in user_projectuser_task.user_ids, "When creating a task that has a project for another user, the current user should not be added to the assignees.")
|
||||
|
||||
@users('Project user')
|
||||
def test_project_current_user_is_set_as_assignee_in_task_when_project_id_is_set_with_no_assignees(self):
|
||||
task = self.env['project.task'].create({'name': 'Private task', 'project_id': self.project_pigs.id})
|
||||
self.assertEqual(task.user_ids, self.env.user, "When creating a task that has a project without assignees, the task will be assigned to the current user if no default_project_id is provided in the context (which is handled in _default_personal_stage_type_id).")
|
||||
|
||||
@users('Project user')
|
||||
def test_project_current_user_is_not_added_in_private_task_assignees_when_default_project_id_is_in_the_context(self):
|
||||
task_values = {'name': 'Private task'}
|
||||
context = {'default_project_id': self.project_pigs.id}
|
||||
ProjectTask_with_default_project_id = self.env['project.task'].with_context(context)
|
||||
task = ProjectTask_with_default_project_id.create(task_values)
|
||||
self.assertNotEqual(task.user_ids, self.env.user, "When creating a task without assignees and providing default_project_id in the context, the task should not be assigned to the current user.")
|
||||
user_projectuser_task = ProjectTask_with_default_project_id.create({**task_values, 'user_ids': [Command.set(self.user_projectuser.ids)]})
|
||||
self.assertFalse(self.env.user in user_projectuser_task.user_ids, "When creating a task for another user and providing default_project_id in the context, the current user should not be added to the assignees.")
|
||||
|
||||
@users('Project user')
|
||||
def test_project_user_cannot_write_private_task_of_another_user(self):
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).write({'name': 'Test write'})
|
||||
|
||||
@users('Project user')
|
||||
def test_project_user_cannot_read_private_task_of_another_user(self):
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).read(['name'])
|
||||
|
||||
@users('Project user')
|
||||
def test_project_user_cannot_unlink_private_task_of_another_user(self):
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).unlink()
|
||||
|
||||
def test_of_setting_root_user_on_private_task(self):
|
||||
test_task = self.env['project.task'].create({
|
||||
'name':'Test Private Task',
|
||||
'user_ids': [Command.link(self.user_projectuser.id)]
|
||||
})
|
||||
self.assertNotEqual(test_task.user_ids, self.env.user, "Created private task should not have odoobot as asignee")
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from freezegun import freeze_time
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import Command
|
||||
from odoo.osv.expression import AND, OR
|
||||
from odoo.tests.common import tagged, HttpCase
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
class TestBurndownChartCommon(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def set_create_date(cls, table, res_id, create_date):
|
||||
cls.env.cr.execute("UPDATE {} SET create_date=%s WHERE id=%s".format(table), (create_date, res_id))
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.current_year = datetime.now().year
|
||||
create_date = datetime(cls.current_year - 1, 1, 1)
|
||||
kanban_state_vals = {
|
||||
"legend_blocked": 'Blocked',
|
||||
"legend_done": 'Ready',
|
||||
"legend_normal": 'In Progress'
|
||||
}
|
||||
Stage = cls.env['project.task.type']
|
||||
cls.todo_stage = Stage.create({
|
||||
'sequence': 1,
|
||||
'name': 'TODO',
|
||||
**kanban_state_vals,
|
||||
})
|
||||
cls.set_create_date('project_task_type', cls.todo_stage.id, create_date)
|
||||
cls.in_progress_stage = Stage.create({
|
||||
'sequence': 10,
|
||||
'name': 'In Progress',
|
||||
**kanban_state_vals,
|
||||
})
|
||||
cls.set_create_date('project_task_type', cls.in_progress_stage.id, create_date)
|
||||
cls.testing_stage = Stage.create({
|
||||
'sequence': 20,
|
||||
'name': 'Testing',
|
||||
**kanban_state_vals,
|
||||
})
|
||||
cls.set_create_date('project_task_type', cls.testing_stage.id, create_date)
|
||||
cls.done_stage = Stage.create({
|
||||
'sequence': 30,
|
||||
'name': 'Done',
|
||||
**kanban_state_vals,
|
||||
})
|
||||
cls.set_create_date('project_task_type', cls.done_stage.id, create_date)
|
||||
cls.stages = cls.todo_stage + cls.in_progress_stage + cls.testing_stage + cls.done_stage
|
||||
cls.project = cls.env['project.project'].create({
|
||||
'name': 'Burndown Chart Test',
|
||||
'privacy_visibility': 'employees',
|
||||
'alias_name': 'project+burndown_chart',
|
||||
'type_ids': [Command.link(stage_id) for stage_id in cls.stages.ids],
|
||||
})
|
||||
cls.set_create_date('project_project', cls.project.id, create_date)
|
||||
cls.project.invalidate_model()
|
||||
cls.milestone = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Milestone',
|
||||
'project_id': cls.project_pigs.id,
|
||||
})
|
||||
cls.task_a = cls.env['project.task'].create({
|
||||
'name': 'Task A',
|
||||
'priority': 0,
|
||||
'project_id': cls.project.id,
|
||||
'stage_id': cls.todo_stage.id,
|
||||
})
|
||||
cls.set_create_date('project_task', cls.task_a.id, create_date)
|
||||
cls.task_b = cls.task_a.copy({
|
||||
'name': 'Task B',
|
||||
'user_ids': [Command.set([cls.user_projectuser.id, cls.user_projectmanager.id])],
|
||||
})
|
||||
cls.set_create_date('project_task', cls.task_b.id, create_date)
|
||||
cls.task_c = cls.task_a.copy({
|
||||
'name': 'Task C',
|
||||
'partner_id': cls.partner_1.id,
|
||||
'user_ids': [Command.link(cls.user_projectuser.id)],
|
||||
})
|
||||
cls.set_create_date('project_task', cls.task_c.id, create_date)
|
||||
cls.task_d = cls.task_a.copy({
|
||||
'name': 'Task D',
|
||||
'milestone_id': cls.milestone.id,
|
||||
'user_ids': [Command.link(cls.user_projectmanager.id)],
|
||||
})
|
||||
cls.set_create_date('project_task', cls.task_d.id, create_date)
|
||||
cls.task_e = cls.task_a.copy({
|
||||
'name': 'Task E',
|
||||
'partner_id': cls.partner_1.id,
|
||||
})
|
||||
cls.set_create_date('project_task', cls.task_e.id, create_date)
|
||||
|
||||
# Create a new task to check if a task without changing its stage is taken into account
|
||||
task_f = cls.env['project.task'].create({
|
||||
'name': 'Task F',
|
||||
'priority': 0,
|
||||
'project_id': cls.project.id,
|
||||
'milestone_id': cls.milestone.id,
|
||||
'stage_id': cls.todo_stage.id,
|
||||
})
|
||||
cls.set_create_date('project_task', task_f.id, datetime(cls.current_year - 1, 12, 20))
|
||||
|
||||
cls.project_2 = cls.env['project.project'].create({
|
||||
'name': 'Burndown Chart Test 2 mySearchTag',
|
||||
'privacy_visibility': 'employees',
|
||||
'alias_name': 'project+burndown_chart+2',
|
||||
'type_ids': [Command.link(stage_id) for stage_id in cls.stages.ids],
|
||||
})
|
||||
cls.set_create_date('project_project', cls.project_2.id, create_date)
|
||||
cls.project.invalidate_model()
|
||||
cls.task_g = cls.env['project.task'].create({
|
||||
'name': 'Task G',
|
||||
'priority': 0,
|
||||
'project_id': cls.project_2.id,
|
||||
'stage_id': cls.todo_stage.id,
|
||||
'user_ids': [Command.link(cls.user_projectuser.id)],
|
||||
})
|
||||
cls.set_create_date('project_task', cls.task_g.id, create_date)
|
||||
cls.task_h = cls.task_g.copy({
|
||||
'name': 'Task H',
|
||||
'user_ids': [Command.link(cls.user_projectmanager.id)],
|
||||
})
|
||||
cls.set_create_date('project_task', cls.task_h.id, create_date)
|
||||
|
||||
# Precommit to have the records in db and allow to rollback at the end of test
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-02-10' % (cls.current_year - 1)):
|
||||
(cls.task_a + cls.task_b).write({'stage_id': cls.in_progress_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-02-20' % (cls.current_year - 1)):
|
||||
cls.task_c.write({'stage_id': cls.in_progress_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-03-15' % (cls.current_year - 1)):
|
||||
(cls.task_d + cls.task_e).write({'stage_id': cls.in_progress_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-04-10' % (cls.current_year - 1)):
|
||||
(cls.task_a + cls.task_b).write({'stage_id': cls.testing_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-05-12' % (cls.current_year - 1)):
|
||||
cls.task_c.write({'stage_id': cls.testing_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-06-25' % (cls.current_year - 1)):
|
||||
cls.task_d.write({'stage_id': cls.testing_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-07-25' % (cls.current_year - 1)):
|
||||
cls.task_e.write({'stage_id': cls.testing_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-08-01' % (cls.current_year - 1)):
|
||||
cls.task_a.write({'stage_id': cls.done_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-09-10' % (cls.current_year - 1)):
|
||||
cls.task_b.write({'stage_id': cls.done_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-10-05' % (cls.current_year - 1)):
|
||||
cls.task_c.write({'stage_id': cls.done_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-11-25' % (cls.current_year - 1)):
|
||||
cls.task_d.write({'stage_id': cls.done_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
with freeze_time('%s-12-12' % (cls.current_year - 1)):
|
||||
cls.task_e.write({'stage_id': cls.done_stage.id})
|
||||
cls.env.cr.flush()
|
||||
|
||||
|
||||
class TestBurndownChart(TestBurndownChartCommon):
|
||||
|
||||
def map_read_group_result(self, read_group_result):
|
||||
return {(res['date:month'], res['stage_id'][0]): res['__count'] for res in read_group_result if res['stage_id'][1]}
|
||||
|
||||
def check_read_group_results(self, domain, expected_results_dict):
|
||||
stages_dict = {stage.id: stage.name for stage in self.stages}
|
||||
read_group_result = self.env['project.task.burndown.chart.report'].read_group(
|
||||
domain, ['date', 'stage_id'], ['date:month', 'stage_id'], lazy=False)
|
||||
read_group_result_dict = self.map_read_group_result(read_group_result)
|
||||
for (month, stage_id), __count in read_group_result_dict.items():
|
||||
expected_count = expected_results_dict.get((month, stage_id), 100000)
|
||||
self.assertEqual(
|
||||
__count,
|
||||
expected_count,
|
||||
'In %s, the number of tasks should be equal to %s in %s stage.' % (month, expected_count, stages_dict.get(stage_id, 'Unknown'))
|
||||
)
|
||||
|
||||
def test_burndown_chart(self):
|
||||
burndown_chart_domain = [('display_project_id', '!=', False)]
|
||||
project_domain = [('project_id', '=', self.project.id)]
|
||||
|
||||
# Check that we get the expected results for the complete data of `self.project`.
|
||||
project_expected_dict = {
|
||||
('January %s' % (self.current_year - 1), self.todo_stage.id): 5,
|
||||
('January %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('January %s' % (self.current_year - 1), self.testing_stage.id): 0,
|
||||
('January %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('February %s' % (self.current_year - 1), self.todo_stage.id): 2,
|
||||
('February %s' % (self.current_year - 1), self.in_progress_stage.id): 3,
|
||||
('February %s' % (self.current_year - 1), self.testing_stage.id): 0,
|
||||
('February %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('March %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('March %s' % (self.current_year - 1), self.in_progress_stage.id): 5,
|
||||
('March %s' % (self.current_year - 1), self.testing_stage.id): 0,
|
||||
('March %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('April %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('April %s' % (self.current_year - 1), self.in_progress_stage.id): 3,
|
||||
('April %s' % (self.current_year - 1), self.testing_stage.id): 2,
|
||||
('April %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('May %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('May %s' % (self.current_year - 1), self.in_progress_stage.id): 2,
|
||||
('May %s' % (self.current_year - 1), self.testing_stage.id): 3,
|
||||
('May %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('June %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('June %s' % (self.current_year - 1), self.in_progress_stage.id): 1,
|
||||
('June %s' % (self.current_year - 1), self.testing_stage.id): 4,
|
||||
('June %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('July %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('July %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('July %s' % (self.current_year - 1), self.testing_stage.id): 5,
|
||||
('July %s' % (self.current_year - 1), self.done_stage.id): 0,
|
||||
('August %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('August %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('August %s' % (self.current_year - 1), self.testing_stage.id): 4,
|
||||
('August %s' % (self.current_year - 1), self.done_stage.id): 1,
|
||||
('September %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('September %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('September %s' % (self.current_year - 1), self.testing_stage.id): 3,
|
||||
('September %s' % (self.current_year - 1), self.done_stage.id): 2,
|
||||
('October %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('October %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('October %s' % (self.current_year - 1), self.testing_stage.id): 2,
|
||||
('October %s' % (self.current_year - 1), self.done_stage.id): 3,
|
||||
('November %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('November %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('November %s' % (self.current_year - 1), self.testing_stage.id): 1,
|
||||
('November %s' % (self.current_year - 1), self.done_stage.id): 4,
|
||||
('December %s' % (self.current_year - 1), self.todo_stage.id): 0,
|
||||
('December %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
|
||||
('December %s' % (self.current_year - 1), self.done_stage.id): 5,
|
||||
('December %s' % (self.current_year - 1), self.todo_stage.id): 1,
|
||||
('January %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('January %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('January %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('January %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('February %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('February %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('February %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('February %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('March %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('March %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('March %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('March %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('April %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('April %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('April %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('April %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('May %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('May %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('May %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('May %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('June %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('June %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('June %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('June %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('July %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('July %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('July %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('July %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('August %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('August %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('August %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('August %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('September %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('September %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('September %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('September %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('October %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('October %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('October %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('October %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('November %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('November %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('November %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('November %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
('December %s' % (self.current_year), self.todo_stage.id): 0,
|
||||
('December %s' % (self.current_year), self.in_progress_stage.id): 0,
|
||||
('December %s' % (self.current_year), self.done_stage.id): 5,
|
||||
('December %s' % (self.current_year), self.todo_stage.id): 1,
|
||||
}
|
||||
self.check_read_group_results(AND([burndown_chart_domain, project_domain]), project_expected_dict)
|
||||
|
||||
# Check that we get the expected results for the complete data of `self.project` & `self.project_2` using an
|
||||
# `ilike` in the domain.
|
||||
all_projects_domain_with_ilike = OR([project_domain, [('project_id', 'ilike', 'mySearchTag')]])
|
||||
project_expected_dict = {key: val if key[1] != self.todo_stage.id else val + 2 for key, val in project_expected_dict.items()}
|
||||
self.check_read_group_results(AND([burndown_chart_domain, all_projects_domain_with_ilike]), project_expected_dict)
|
||||
|
||||
date_from, date_to = ('%s-01-01' % (self.current_year - 1), '%s-02-01' % (self.current_year - 1))
|
||||
date_and_user_domain = [('date', '>=', date_from), ('date', '<', date_to), ('user_ids', 'ilike', 'ProjectUser')]
|
||||
complex_domain_expected_dict = {
|
||||
('January %s' % (self.current_year - 1), self.todo_stage.id): 3,
|
||||
('February %s' % (self.current_year - 1), self.todo_stage.id): 1,
|
||||
('February %s' % (self.current_year - 1), self.in_progress_stage.id): 2,
|
||||
}
|
||||
complex_domain = AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain])
|
||||
self.check_read_group_results(complex_domain, complex_domain_expected_dict)
|
||||
|
||||
date_and_user_domain = [('date', '>=', date_from), ('date', '<', date_to), ('user_ids', 'ilike', 'ProjectManager')]
|
||||
milestone_domain = [('milestone_id', 'ilike', 'Test')]
|
||||
complex_domain = AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain, milestone_domain])
|
||||
complex_domain_expected_dict = {
|
||||
('January %s' % (self.current_year - 1), self.todo_stage.id): 1,
|
||||
('February %s' % (self.current_year - 1), self.todo_stage.id): 1,
|
||||
}
|
||||
self.check_read_group_results(complex_domain, complex_domain_expected_dict)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestBurndownChartTour(HttpCase, TestBurndownChartCommon):
|
||||
|
||||
def test_burndown_chart_tour(self):
|
||||
# Test customizing personal stages as a project user
|
||||
self.start_tour('/web', 'burndown_chart_tour', login="admin")
|
||||
316
odoo-bringout-oca-ocb-project/project/tests/test_multicompany.py
Normal file
316
odoo-bringout-oca-ocb-project/project/tests/test_multicompany.py
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from contextlib import contextmanager
|
||||
from lxml import etree
|
||||
|
||||
from odoo.tests.common import TransactionCase, Form
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
|
||||
class TestMultiCompanyCommon(TransactionCase):
|
||||
|
||||
@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_ids': cls.user_employee_company_a,
|
||||
'project_id': cls.project_company_a.id
|
||||
})
|
||||
cls.task_2 = Task.create({
|
||||
'name': 'Task 2 in Project B',
|
||||
'user_ids': cls.user_employee_company_b,
|
||||
'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, 'default_parent_id': self.task_1.id, 'default_project_id': self.project_company_b.id})) as task_form:
|
||||
task_form.name = 'Test Subtask in company B'
|
||||
task = task_form.save()
|
||||
self.assertEqual(task.company_id, self.project_company_b.company_id, "The company of the subtask should be the one from its project, and not from its parent.")
|
||||
|
||||
# set parent on existing orphan task; the onchange will set the correct company and subtask project
|
||||
self.task_2.write({'project_id': False})
|
||||
# For `parent_id` to be visible in the view, you need
|
||||
# 1. The debug mode
|
||||
# 2. `allow_subtasks` to be true
|
||||
# <field name="parent_id" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
# `allow_subtasks` is a related to `allow_subtasks` on the project
|
||||
# as the point of the test is to test the behavior of the task `_compute_project_id` when there is no project,
|
||||
# `allow_subtasks` is by default invisible, and you shouldn't therefore be able to change it.
|
||||
# So, to make it visible, temporary modify the view to make it visible even when `allow_subtasks` is `False`.
|
||||
view = self.env.ref('project.view_task_form2').sudo()
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="parent_id"][@attrs]'):
|
||||
node.attrib.pop('attrs')
|
||||
view.arch = etree.tostring(tree)
|
||||
with self.debug_mode():
|
||||
with Form(self.task_2) as task_form:
|
||||
task_form.name = 'Test Task 2 becomes child of Task 1 (other company)'
|
||||
task_form.parent_id = self.task_1
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.company_id, task.project_id.company_id, "The company of the orphan subtask should be the one from its project.")
|
||||
|
||||
def test_cross_subtask_project(self):
|
||||
# set up default subtask project
|
||||
self.project_company_a.write({'allow_subtasks': True})
|
||||
|
||||
# For `parent_id` to be visible in the view, you need
|
||||
# 1. The debug mode
|
||||
# 2. `allow_subtasks` to be true
|
||||
# <field name="parent_id" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
# `allow_subtasks` is a related to `allow_subtasks` on the project
|
||||
# as the point of the test is to test the behavior of the task `_compute_project_id` when there is no project,
|
||||
# `allow_subtasks` is by default invisible, and you shouldn't therefore be able to change it.
|
||||
# So, to make it visible, temporary modify the view to make it visible even when `allow_subtasks` is `False`.
|
||||
view = self.env.ref('project.view_task_form2').sudo()
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="parent_id"][@attrs]'):
|
||||
node.attrib.pop('attrs')
|
||||
view.arch = etree.tostring(tree)
|
||||
|
||||
with self.sudo('employee-a'):
|
||||
with self.allow_companies([self.company_a.id, self.company_b.id]):
|
||||
with self.debug_mode():
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.name = 'Test Subtask in company B'
|
||||
task_form.parent_id = self.task_1
|
||||
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.task_1.project_id, "The default project of a subtask should be the default subtask project of the project from the mother task")
|
||||
self.assertEqual(task.company_id, task.project_id.company_id, "The company of the orphan subtask should be the one from its project.")
|
||||
self.assertEqual(self.task_1.child_ids.ids, [task.id])
|
||||
|
||||
with self.sudo('employee-a'):
|
||||
with self.assertRaises(AccessError):
|
||||
with Form(task) as task_form:
|
||||
task_form.name = "Testing changing name in a company I can not read/write"
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import tagged, HttpCase
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
@tagged('-at_install', 'post_install', 'personal_stages')
|
||||
class TestPersonalStages(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user_stages = cls.env['project.task.type'].search([('user_id', '=', cls.user_projectuser.id)])
|
||||
cls.manager_stages = cls.env['project.task.type'].search([('user_id', '=', cls.user_projectmanager.id)])
|
||||
|
||||
def test_personal_stage_base(self):
|
||||
# Project User is assigned to task_1 he should be able to see a personal stage
|
||||
self.task_1.with_user(self.user_projectuser)._compute_personal_stage_id()
|
||||
self.assertTrue(self.task_1.with_user(self.user_projectuser).personal_stage_type_id,
|
||||
'Project User is assigned to task 1, he should have a personal stage assigned.')
|
||||
|
||||
self.task_1.with_user(self.user_projectmanager)._compute_personal_stage_id()
|
||||
self.assertFalse(self.env['project.task'].browse(self.task_1.id).with_user(self.user_projectmanager).personal_stage_type_id,
|
||||
'Project Manager is not assigned to task 1, he should not have a personal stage assigned.')
|
||||
|
||||
# Now assign a second user to our task_1
|
||||
self.task_1.user_ids += self.user_projectmanager
|
||||
self.assertTrue(self.task_1.with_user(self.user_projectmanager).personal_stage_type_id,
|
||||
'Project Manager has now been assigned to task 1 and should have a personal stage assigned.')
|
||||
|
||||
self.task_1.with_user(self.user_projectmanager)._compute_personal_stage_id()
|
||||
task_1_manager_stage = self.task_1.with_user(self.user_projectmanager).personal_stage_type_id
|
||||
|
||||
self.task_1.with_user(self.user_projectuser)._compute_personal_stage_id()
|
||||
self.task_1.with_user(self.user_projectuser).personal_stage_type_id = self.user_stages[1]
|
||||
self.assertEqual(self.task_1.with_user(self.user_projectuser).personal_stage_type_id, self.user_stages[1],
|
||||
'Assigning another personal stage to the task should have changed it for user 1.')
|
||||
|
||||
self.task_1.with_user(self.user_projectmanager)._compute_personal_stage_id()
|
||||
self.assertEqual(self.task_1.with_user(self.user_projectmanager).personal_stage_type_id, task_1_manager_stage,
|
||||
'Modifying the personal stage of Project User should not have affected the personal stage of Project Manager.')
|
||||
|
||||
self.task_2.with_user(self.user_projectmanager).personal_stage_type_id = self.manager_stages[1]
|
||||
self.assertEqual(self.task_1.with_user(self.user_projectmanager).personal_stage_type_id, task_1_manager_stage,
|
||||
'Modifying the personal stage on task 2 for Project Manager should not have affected the stage on task 1.')
|
||||
|
||||
def test_personal_stage_search(self):
|
||||
self.task_2.user_ids += self.user_projectuser
|
||||
# Make sure both personal stages are different
|
||||
self.task_1.with_user(self.user_projectuser).personal_stage_type_id = self.user_stages[0]
|
||||
self.task_2.with_user(self.user_projectuser).personal_stage_type_id = self.user_stages[1]
|
||||
tasks = self.env['project.task'].with_user(self.user_projectuser).search([('personal_stage_type_id', '=', self.user_stages[0].id)])
|
||||
self.assertTrue(tasks, 'The search result should not be empty.')
|
||||
for task in tasks:
|
||||
self.assertEqual(task.personal_stage_type_id, self.user_stages[0],
|
||||
'The search should only have returned task that are in the inbox personal stage.')
|
||||
|
||||
def test_personal_stage_read_group(self):
|
||||
self.task_1.user_ids += self.user_projectmanager
|
||||
self.task_1.with_user(self.user_projectmanager).personal_stage_type_id = self.manager_stages[1]
|
||||
#Makes sure the personal stage for project manager is saved in the database
|
||||
self.env.flush_all()
|
||||
read_group_user = self.env['project.task'].with_user(self.user_projectuser).read_group(
|
||||
[('user_ids', '=', self.user_projectuser.id)], fields=['sequence:avg'], groupby=['personal_stage_type_ids'])
|
||||
# Check that the result is at least a bit coherent
|
||||
self.assertEqual(len(self.user_stages), len(read_group_user),
|
||||
'read_group should return %d groups' % len(self.user_stages))
|
||||
# User has only one task assigned the sum of all counts should be 1
|
||||
total = 0
|
||||
for group in read_group_user:
|
||||
total += group['personal_stage_type_ids_count']
|
||||
self.assertEqual(1, total,
|
||||
'read_group should not have returned more tasks than the user is assigned to.')
|
||||
read_group_manager = self.env['project.task'].with_user(self.user_projectmanager).read_group(
|
||||
[('user_ids', '=', self.user_projectmanager.id)], fields=['sequence:avg'], groupby=['personal_stage_type_ids'])
|
||||
self.assertEqual(len(self.manager_stages), len(read_group_manager),
|
||||
'read_group should return %d groups' % len(self.user_stages))
|
||||
total = 0
|
||||
total_stage_0 = 0
|
||||
total_stage_1 = 0
|
||||
for group in read_group_manager:
|
||||
total += group['personal_stage_type_ids_count']
|
||||
# Check that we have a task in both stages
|
||||
if group['personal_stage_type_ids'][0] == self.manager_stages[0].id:
|
||||
total_stage_0 += 1
|
||||
elif group['personal_stage_type_ids'][0] == self.manager_stages[1].id:
|
||||
total_stage_1 += 1
|
||||
self.assertEqual(2, total,
|
||||
'read_group should not have returned more tasks than the user is assigned to.')
|
||||
self.assertEqual(1, total_stage_0)
|
||||
self.assertEqual(1, total_stage_1)
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestPersonalStageTour(HttpCase, TestProjectCommon):
|
||||
|
||||
def test_personal_stage_tour(self):
|
||||
# Test customizing personal stages as a project user
|
||||
self.start_tour('/web', 'personal_stage_tour', login="armandel")
|
||||
83
odoo-bringout-oca-ocb-project/project/tests/test_portal.py
Normal file
83
odoo-bringout-oca-ocb-project/project/tests/test_portal.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.project.tests.test_access_rights import TestProjectPortalCommon
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestPortalProject(TestProjectPortalCommon):
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_portal_project_access_rights(self):
|
||||
pigs = self.project_pigs
|
||||
pigs.write({'privacy_visibility': 'portal'})
|
||||
|
||||
# Do: Alfred reads project -> ok (employee ok public)
|
||||
pigs.with_user(self.user_projectuser).read(['user_id'])
|
||||
# Test: all project tasks visible
|
||||
tasks = self.env['project.task'].with_user(self.user_projectuser).search([('project_id', '=', pigs.id)])
|
||||
self.assertEqual(tasks, self.task_1 | self.task_2 | self.task_3 | self.task_4 | self.task_5 | self.task_6,
|
||||
'access rights: project user should see all tasks of a portal project')
|
||||
|
||||
# Do: Bert reads project -> crash, no group
|
||||
self.assertRaises(AccessError, pigs.with_user(self.user_noone).read, ['user_id'])
|
||||
# Test: no project task searchable
|
||||
self.assertRaises(AccessError, self.env['project.task'].with_user(self.user_noone).search, [('project_id', '=', pigs.id)])
|
||||
|
||||
# Data: task follower
|
||||
pigs.with_user(self.user_projectmanager).message_subscribe(partner_ids=[self.user_portal.partner_id.id])
|
||||
self.task_1.with_user(self.user_projectuser).message_subscribe(partner_ids=[self.user_portal.partner_id.id])
|
||||
self.task_3.with_user(self.user_projectuser).message_subscribe(partner_ids=[self.user_portal.partner_id.id])
|
||||
# Do: Chell reads project -> ok (portal ok public)
|
||||
pigs.with_user(self.user_portal).read(['user_id'])
|
||||
# Do: Donovan reads project -> ko (public ko portal)
|
||||
self.assertRaises(AccessError, pigs.with_user(self.user_public).read, ['user_id'])
|
||||
# Test: no access right to project.task
|
||||
self.assertRaises(AccessError, self.env['project.task'].with_user(self.user_public).search, [])
|
||||
# Data: task follower cleaning
|
||||
self.task_1.with_user(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_portal.partner_id.id])
|
||||
self.task_3.with_user(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_portal.partner_id.id])
|
||||
|
||||
def test_reset_access_token_when_privacy_visibility_changes(self):
|
||||
self.assertNotEqual(self.project_pigs.privacy_visibility, 'portal', 'Make sure the privacy visibility is not yet the portal one.')
|
||||
self.assertFalse(self.project_pigs.access_token, 'The access token should not be set on the project since it is not public')
|
||||
self.project_pigs.privacy_visibility = 'portal'
|
||||
self.assertFalse(self.project_pigs.access_token, 'The access token should not yet available since the project has not been shared yet.')
|
||||
wizard = self.env['project.share.wizard'].create({
|
||||
'access_mode': 'read',
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_pigs.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.partner_1.id),
|
||||
]
|
||||
})
|
||||
wizard.action_send_mail()
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs)
|
||||
self.assertTrue(self.project_pigs.access_token, 'The access token should be set since the project has been shared.')
|
||||
self.assertTrue(self.task_1.access_token, 'The access token should be set since the task has been shared.')
|
||||
access_token = self.project_pigs.access_token
|
||||
task_access_token = self.task_1.access_token
|
||||
self.project_pigs.privacy_visibility = 'followers'
|
||||
self.assertFalse(self.project_pigs.access_token, 'The access token should no longer be set since now the project is private.')
|
||||
self.assertFalse(all(self.project_pigs.tasks.mapped('access_token')), 'The access token should no longer be set in any tasks linked to the project since now the project is private.')
|
||||
self.project_pigs.privacy_visibility = 'portal'
|
||||
self.assertFalse(self.project_pigs.access_token, 'The access token should still not be set since now the project has not been shared yet.')
|
||||
self.assertFalse(all(self.project_pigs.tasks.mapped('access_token')), 'The access token should no longer be set in any tasks linked to the project since now the project is private.')
|
||||
wizard.action_send_mail()
|
||||
self.assertTrue(self.project_pigs.access_token, 'The access token should now be regenerated for this project since that project has been shared to an external partner.')
|
||||
self.assertFalse(self.task_1.access_token)
|
||||
task_wizard = self.env['portal.share'].create({
|
||||
'res_model': 'project.task',
|
||||
'res_id': self.task_1.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.partner_1.id),
|
||||
],
|
||||
})
|
||||
task_wizard.action_send_mail()
|
||||
self.assertTrue(self.task_1.access_token, 'The access token should be set since the task has been shared.')
|
||||
self.assertNotEqual(self.project_pigs.access_token, access_token, 'The new access token generated for the project should not be the old one.')
|
||||
self.assertNotEqual(self.task_1.access_token, task_access_token, 'The new access token generated for the task should not be the old one.')
|
||||
self.project_pigs.privacy_visibility = 'employees'
|
||||
self.assertFalse(self.project_pigs.access_token, 'The access token should no longer be set since now the project is only available by internal users.')
|
||||
self.assertFalse(all(self.project_pigs.tasks.mapped('access_token')), 'The access token should no longer be set in any tasks linked to the project since now the project is only available by internal users.')
|
||||
161
odoo-bringout-oca-ocb-project/project/tests/test_project_base.py
Normal file
161
odoo-bringout-oca-ocb-project/project/tests/test_project_base.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestProjectCommon(TransactionCase):
|
||||
|
||||
@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': 'armandel',
|
||||
'password': 'armandel',
|
||||
'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_ids': cls.user_projectuser,
|
||||
'project_id': cls.project_pigs.id})
|
||||
cls.task_2 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Pigs ManagerTask',
|
||||
'user_ids': cls.user_projectmanager,
|
||||
'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'].message_process(model, mail)
|
||||
return self.env[target_model].search([(target_field, '=', subject)])
|
||||
|
||||
|
||||
class TestProjectBase(TestProjectCommon):
|
||||
|
||||
def test_delete_project_with_tasks(self):
|
||||
"""Test all tasks linked to a project are removed when the user removes this project. """
|
||||
task_type = self.env['project.task.type'].create({'name': 'Won', 'sequence': 1, 'fold': True})
|
||||
project_unlink = self.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'rev',
|
||||
'privacy_visibility': 'employees',
|
||||
'alias_name': 'rev',
|
||||
'partner_id': self.partner_1.id,
|
||||
'type_ids': task_type,
|
||||
})
|
||||
|
||||
self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Pigs UserTask',
|
||||
'user_ids': self.user_projectuser,
|
||||
'project_id': project_unlink.id,
|
||||
'stage_id': task_type.id})
|
||||
|
||||
task_count = len(project_unlink.tasks)
|
||||
self.assertEqual(task_count, 1, "The project should have 1 task")
|
||||
|
||||
project_unlink.unlink()
|
||||
self.assertNotEqual(task_count, 0, "The all tasks linked to project should be deleted when user delete the project")
|
||||
|
||||
def test_auto_assign_stages_when_importing_tasks(self):
|
||||
self.assertFalse(self.project_pigs.type_ids)
|
||||
self.assertEqual(len(self.project_goats.type_ids), 2)
|
||||
first_stage = self.project_goats.type_ids[0]
|
||||
self.env['project.task']._load_records_create([{
|
||||
'name': 'First Task',
|
||||
'project_id': self.project_pigs.id,
|
||||
'stage_id': first_stage.id,
|
||||
}])
|
||||
self.assertEqual(self.project_pigs.type_ids, first_stage)
|
||||
self.env['project.task']._load_records_create([
|
||||
{
|
||||
'name': 'task',
|
||||
'project_id': self.project_pigs.id,
|
||||
'stage_id': stage.id,
|
||||
} for stage in self.project_goats.type_ids
|
||||
])
|
||||
self.assertEqual(self.project_pigs.type_ids, self.project_goats.type_ids)
|
||||
|
||||
def test_filter_visibility_unread_messages(self):
|
||||
"""Tests the visibility of the "Unread messages" filter in the project task search view
|
||||
according to the notification type of the user.
|
||||
A user with the email notification type must not see the Unread messages filter
|
||||
A user with the inbox notification type must see the Unread messages filter"""
|
||||
user1 = self.user_projectuser
|
||||
user2 = self.user_projectuser.copy()
|
||||
user1.notification_type = 'email'
|
||||
user2.notification_type = 'inbox'
|
||||
for user, filter_invisible_expected in ((user1, True), (user2, None)):
|
||||
Task = self.env['project.task'].with_user(user)
|
||||
arch = Task.get_view(self.env.ref('project.view_task_search_form').id, 'search')['arch']
|
||||
tree = etree.fromstring(arch)
|
||||
node = tree.xpath('//filter[@name="message_needaction"]')[0]
|
||||
modifiers = json.loads(node.get('modifiers') or '{}')
|
||||
self.assertEqual(modifiers.get('invisible'), filter_invisible_expected)
|
||||
|
|
@ -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")
|
||||
468
odoo-bringout-oca-ocb-project/project/tests/test_project_flow.py
Normal file
468
odoo-bringout-oca-ocb-project/project/tests/test_project_flow.py
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
from odoo import Command
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
EMAIL_TPL = """Return-Path: <whatever-2a840@postmaster.twitter.com>
|
||||
X-Original-To: {to}
|
||||
Delivered-To: {to}
|
||||
To: {to}
|
||||
cc: {cc}
|
||||
Received: by mail1.odoo.com (Postfix, from userid 10002)
|
||||
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
|
||||
Message-ID: {msg_id}
|
||||
Date: Tue, 29 Nov 2011 12:43:21 +0530
|
||||
From: {email_from}
|
||||
MIME-Version: 1.0
|
||||
Subject: {subject}
|
||||
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
|
||||
|
||||
Hello,
|
||||
|
||||
This email should create a new entry in your module. Please check that it
|
||||
effectively works.
|
||||
|
||||
Thanks,
|
||||
|
||||
--
|
||||
Raoul Boitempoils
|
||||
Integrator at Agrolait"""
|
||||
|
||||
|
||||
class TestProjectFlow(TestProjectCommon, MailCommon):
|
||||
|
||||
def test_project_process_project_manager_duplicate(self):
|
||||
pigs = self.project_pigs.with_user(self.user_projectmanager)
|
||||
dogs = pigs.copy()
|
||||
self.assertEqual(len(dogs.tasks), 2, 'project: duplicating a project must duplicate its tasks')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread')
|
||||
def test_task_process_without_stage(self):
|
||||
# Do: incoming mail from an unknown partner on an alias creates a new task 'Frogs'
|
||||
task = self.format_and_process(
|
||||
EMAIL_TPL, to='project+pigs@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
|
||||
email_from='%s' % self.user_projectuser.email,
|
||||
subject='Frogs', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
|
||||
target_model='project.task')
|
||||
|
||||
# Test: one task created by mailgateway administrator
|
||||
self.assertEqual(len(task), 1, 'project: message_process: a new project.task should have been created')
|
||||
# Test: check partner in message followers
|
||||
self.assertIn(self.partner_2, task.message_partner_ids, "Partner in message cc is not added as a task followers.")
|
||||
# Test: messages
|
||||
self.assertEqual(len(task.message_ids), 1,
|
||||
'project: message_process: newly created task should have 1 message: email')
|
||||
self.assertEqual(task.message_ids.subtype_id, self.env.ref('project.mt_task_new'),
|
||||
'project: message_process: first message of new task should have Task Created subtype')
|
||||
self.assertEqual(task.message_ids.author_id, self.user_projectuser.partner_id,
|
||||
'project: message_process: second message should be the one from Agrolait (partner failed)')
|
||||
self.assertEqual(task.message_ids.subject, 'Frogs',
|
||||
'project: message_process: second message should be the one from Agrolait (subject failed)')
|
||||
# Test: task content
|
||||
self.assertEqual(task.name, 'Frogs', 'project_task: name should be the email subject')
|
||||
self.assertEqual(task.project_id, self.project_pigs, 'project_task: incorrect project')
|
||||
self.assertEqual(task.stage_id.sequence, False, "project_task: shouldn't have a stage, i.e. sequence=False")
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread')
|
||||
def test_task_process_with_stages(self):
|
||||
# Do: incoming mail from an unknown partner on an alias creates a new task 'Cats'
|
||||
task = self.format_and_process(
|
||||
EMAIL_TPL, to='project+goats@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
|
||||
email_from='%s' % self.user_projectuser.email,
|
||||
subject='Cats', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
|
||||
target_model='project.task')
|
||||
|
||||
# Test: one task created by mailgateway administrator
|
||||
self.assertEqual(len(task), 1, 'project: message_process: a new project.task should have been created')
|
||||
# Test: check partner in message followers
|
||||
self.assertIn(self.partner_2, task.message_partner_ids, "Partner in message cc is not added as a task followers.")
|
||||
# Test: messages
|
||||
self.assertEqual(len(task.message_ids), 1,
|
||||
'project: message_process: newly created task should have 1 messages: email')
|
||||
self.assertEqual(task.message_ids.subtype_id, self.env.ref('project.mt_task_new'),
|
||||
'project: message_process: first message of new task should have Task Created subtype')
|
||||
self.assertEqual(task.message_ids.author_id, self.user_projectuser.partner_id,
|
||||
'project: message_process: first message should be the one from Agrolait (partner failed)')
|
||||
self.assertEqual(task.message_ids.subject, 'Cats',
|
||||
'project: message_process: first message should be the one from Agrolait (subject failed)')
|
||||
# Test: task content
|
||||
self.assertEqual(task.name, 'Cats', 'project_task: name should be the email subject')
|
||||
self.assertEqual(task.project_id, self.project_goats, 'project_task: incorrect project')
|
||||
self.assertEqual(task.stage_id.sequence, 1, "project_task: should have a stage with sequence=1")
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread')
|
||||
def test_task_from_email_alias(self):
|
||||
# Do: incoming mail from a known partner email on an alias creates a new task 'Super Frog'
|
||||
task = self.format_and_process(
|
||||
EMAIL_TPL, to='project+goats@mydomain.com, valid.lelitre@agrolait.com', cc='valid.other@gmail.com',
|
||||
email_from='%s' % self.user_portal.email,
|
||||
subject='Super Frog', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
|
||||
target_model='project.task')
|
||||
|
||||
# Test: one task created by mailgateway administrator
|
||||
self.assertEqual(len(task), 1, 'project: message_process: a new project.task should have been created')
|
||||
# Test: check partner in message followers
|
||||
self.assertIn(self.partner_2, task.message_partner_ids, "Partner in message cc is not added as a task followers.")
|
||||
# Test: check partner has not been assgined
|
||||
self.assertFalse(task.user_ids, "Partner is not added as an assignees")
|
||||
# Test: messages
|
||||
self.assertEqual(len(task.message_ids), 1,
|
||||
'project: message_process: newly created task should have 1 messages: email')
|
||||
self.assertEqual(task.message_ids.subtype_id, self.env.ref('project.mt_task_new'),
|
||||
'project: message_process: first message of new task should have Task Created subtype')
|
||||
self.assertEqual(task.message_ids.author_id, self.user_portal.partner_id,
|
||||
'project: message_process: first message should be the one from Agrolait (partner failed)')
|
||||
self.assertEqual(task.message_ids.subject, 'Super Frog',
|
||||
'project: message_process: first message should be the one from Agrolait (subject failed)')
|
||||
# Test: task content
|
||||
self.assertEqual(task.name, 'Super Frog', 'project_task: name should be the email subject')
|
||||
self.assertEqual(task.project_id, self.project_goats, 'project_task: incorrect project')
|
||||
self.assertEqual(task.stage_id.sequence, 1, "project_task: should have a stage with sequence=1")
|
||||
self.assertEqual(
|
||||
task.description,
|
||||
Markup(
|
||||
'<pre>Hello,\n\nThis email should create a new entry in your module. Please check that it\neffectively works.\n\nThanks,\n<span data-o-mail-quote="1">\n--\nRaoul Boitempoils\nIntegrator at Agrolait</span></pre>\n'
|
||||
),
|
||||
'The task description should be the email content.',
|
||||
)
|
||||
|
||||
def test_subtask_process(self):
|
||||
"""
|
||||
Check subtask mecanism and change it from project.
|
||||
|
||||
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_ids': self.user_projectuser,
|
||||
'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_ids': self.user_projectuser,
|
||||
'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.with_context(default_project_id=self.project_goats.id, default_parent_id=parent_task.id).create({
|
||||
'name': 'Task Child with project',
|
||||
'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,
|
||||
'display_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.parent_id.partner_id,
|
||||
"When a project partner_id has been set, a subtask should have the same partner as its parent")
|
||||
|
||||
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, parent_task.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({
|
||||
'display_project_id': self.project_pigs.id
|
||||
})
|
||||
|
||||
self.assertNotEqual(
|
||||
child_task_1.partner_id, self.project_pigs.partner_id,
|
||||
"When the project changes, the subtask should keep its partner id as its partner id is set.")
|
||||
|
||||
# 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({
|
||||
'display_project_id': self.project_goats.id
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
child_task_2.partner_id, parent_task.partner_id,
|
||||
"When the project changes, the subtask should keep the same partner id even it has a 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_ids': self.user_projectuser,
|
||||
'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
|
||||
self.env.invalidate_all()
|
||||
|
||||
self.assertEqual(rating_good.rating_text, 'top')
|
||||
self.assertEqual(rating_bad.rating_text, 'ok')
|
||||
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_goats.rating_avg, 0, 'Since there is no rating in this project, the Average Rating should be equal to 0.')
|
||||
self.assertEqual(self.project_pigs.rating_percentage_satisfaction, 0) # There is a rating but not a "great" on, just an "okay".
|
||||
self.assertEqual(self.project_pigs.rating_avg, rating_bad.rating, 'Since there is only one rating the Average Rating should be equal to the rating value of this one.')
|
||||
|
||||
# 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
|
||||
self.env.invalidate_all()
|
||||
|
||||
rating_avg = (rating_good.rating + rating_bad.rating) / 2
|
||||
self.assertEqual(first_task.rating_count, 2, "Task should have two ratings associated with it")
|
||||
self.assertEqual(first_task.rating_avg_text, 'top')
|
||||
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)
|
||||
self.assertEqual(self.project_pigs.rating_avg, rating_avg)
|
||||
self.assertEqual(self.project_pigs.rating_avg_percentage, rating_avg / 5)
|
||||
|
||||
# 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
|
||||
self.env.invalidate_all()
|
||||
|
||||
self.assertEqual(rating_good.parent_res_id, self.project_goats.id)
|
||||
self.assertEqual(self.project_goats.rating_percentage_satisfaction, 50)
|
||||
self.assertEqual(self.project_goats.rating_avg, rating_avg)
|
||||
self.assertEqual(self.project_pigs.rating_percentage_satisfaction, -1)
|
||||
self.assertEqual(self.project_pigs.rating_avg, 0)
|
||||
|
||||
def test_task_with_no_project(self):
|
||||
"""
|
||||
With this test, we want to make sure the fact that a task has no project doesn't affect the entire
|
||||
behaviours of projects.
|
||||
|
||||
1) Try to compute every field of a task which has no project.
|
||||
2) Try to compute every field of a project and assert it isn't affected by this use case.
|
||||
"""
|
||||
task_without_project = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test task without project'
|
||||
})
|
||||
|
||||
for field in task_without_project._fields.keys():
|
||||
try:
|
||||
task_without_project[field]
|
||||
except Exception as e:
|
||||
raise AssertionError("Error raised unexpectedly while computing a field of the task ! Exception : " + e.args[0])
|
||||
|
||||
for field in self.project_pigs._fields.keys():
|
||||
try:
|
||||
self.project_pigs[field]
|
||||
except Exception as e:
|
||||
raise AssertionError("Error raised unexpectedly while computing a field of the project ! Exception : " + e.args[0])
|
||||
|
||||
# tasks with no project set should only be visible to the users assigned to them
|
||||
task_without_project.user_ids = [Command.link(self.user_projectuser.id)]
|
||||
task_without_project.with_user(self.user_projectuser).read(['name'])
|
||||
with self.assertRaises(AccessError):
|
||||
task_without_project.with_user(self.user_projectmanager).read(['name'])
|
||||
|
||||
# Tests that tasks assigned to the current user should be in the right default stage
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Test Task!',
|
||||
'user_ids': [Command.link(self.env.user.id)],
|
||||
})
|
||||
stages = task._get_default_personal_stage_create_vals(self.env.user.id)
|
||||
self.assertEqual(task.personal_stage_id.stage_id.name, stages[0].get('name'), "tasks assigned to the current user should be in the right default stage")
|
||||
|
||||
def test_send_rating_review(self):
|
||||
project_settings = self.env["res.config.settings"].create({'group_project_rating': True})
|
||||
project_settings.execute()
|
||||
self.assertTrue(self.project_goats.rating_active, 'The customer ratings should be enabled in this project.')
|
||||
|
||||
won_stage = self.project_goats.type_ids[-1]
|
||||
rating_request_mail_template = self.env.ref('project.rating_project_request_email_template')
|
||||
won_stage.write({'rating_template_id': rating_request_mail_template.id})
|
||||
tasks = self.env['project.task'].with_context(mail_create_nolog=True, default_project_id=self.project_goats.id).create([
|
||||
{'name': 'Goat Task 1', 'user_ids': [Command.set([])]},
|
||||
{'name': 'Goat Task 2', 'user_ids': [Command.link(self.user_projectuser.id)]},
|
||||
{
|
||||
'name': 'Goat Task 3',
|
||||
'user_ids': [
|
||||
Command.link(self.user_projectmanager.id),
|
||||
Command.link(self.user_projectuser.id),
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
tasks.with_user(self.user_projectmanager).write({'stage_id': won_stage.id})
|
||||
|
||||
tasks.invalidate_model(['rating_ids'])
|
||||
for task in tasks:
|
||||
self.assertEqual(len(task.rating_ids), 1, 'This task should have a generated rating when it arrives in the Won stage.')
|
||||
rating_request_message = task.message_ids[:1]
|
||||
if not task.user_ids or len(task.user_ids) > 1:
|
||||
self.assertFalse(task.rating_ids.rated_partner_id, 'This rating should have no assigned user if the task related have no assignees or more than one assignee.')
|
||||
self.assertEqual(rating_request_message.email_from, self.user_projectmanager.partner_id.email_formatted, 'The message should have the email of the Project Manager as email from.')
|
||||
else:
|
||||
self.assertEqual(task.rating_ids.rated_partner_id, task.user_ids.partner_id, 'The rating should have an assigned user if the task has only one assignee.')
|
||||
self.assertEqual(rating_request_message.email_from, task.user_ids.partner_id.email_formatted, 'The message should have the email of the assigned user in the task as email from.')
|
||||
self.assertTrue(self.partner_1 in rating_request_message.partner_ids, 'The customer of the task should be in the partner_ids of the rating request message.')
|
||||
|
||||
def test_email_track_template(self):
|
||||
""" Update some tracked fields linked to some template -> message with onchange """
|
||||
project_settings = self.env["res.config.settings"].create({'group_project_stages': True})
|
||||
project_settings.execute()
|
||||
|
||||
mail_template = self.env['mail.template'].create({
|
||||
'name': 'Test template',
|
||||
'subject': 'Test',
|
||||
'body_html': '<p>Test</p>',
|
||||
'auto_delete': True,
|
||||
'model_id': self.env.ref('project.model_project_project_stage').id,
|
||||
})
|
||||
project_A = self.env['project.project'].create({
|
||||
'name': 'project_A',
|
||||
'privacy_visibility': 'followers',
|
||||
'alias_name': 'project A',
|
||||
'partner_id': self.partner_1.id,
|
||||
})
|
||||
init_stage = project_A.stage_id.name
|
||||
|
||||
project_stage = self.env.ref('project.project_project_stage_1')
|
||||
self.assertNotEqual(project_A.stage_id, project_stage)
|
||||
|
||||
# Assign email template
|
||||
project_stage.mail_template_id = mail_template.id
|
||||
self.flush_tracking()
|
||||
init_nb_log = len(project_A.message_ids)
|
||||
project_A.stage_id = project_stage.id
|
||||
self.flush_tracking()
|
||||
self.assertNotEqual(init_stage, project_A.stage_id.name)
|
||||
|
||||
self.assertEqual(len(project_A.message_ids), init_nb_log + 2,
|
||||
"should have 2 new messages: one for tracking, one for template")
|
||||
|
||||
def test_project_notify_get_recipients_groups(self):
|
||||
projects = self.env['project.project'].create([
|
||||
{
|
||||
'name': 'public project',
|
||||
'privacy_visibility': 'portal',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
{
|
||||
'name': 'internal project',
|
||||
'privacy_visibility': 'employees',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
{
|
||||
'name': 'private project',
|
||||
'privacy_visibility': 'followers',
|
||||
'partner_id': self.partner_1.id,
|
||||
},
|
||||
])
|
||||
for project in projects:
|
||||
groups = project._notify_get_recipients_groups()
|
||||
groups_per_key = {g[0]: g for g in groups}
|
||||
for key, group in groups_per_key.items():
|
||||
has_button_access = group[2]['has_button_access']
|
||||
if key in ['portal', 'portal_customer']:
|
||||
self.assertEqual(
|
||||
has_button_access,
|
||||
project.name == 'public project',
|
||||
"Only the public project should have its name clickable in the email sent to the customer when an email is sent via a email template set in the project stage for instance."
|
||||
)
|
||||
elif key == 'user':
|
||||
self.assertTrue(has_button_access)
|
||||
|
||||
def test_private_task_search_tag(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Test Private Task',
|
||||
})
|
||||
# Tag name_search should not raise Error if project_id is False
|
||||
task.tag_ids.with_context(project_id=task.project_id.id).name_search(
|
||||
args=["!", ["id", "in", []]])
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectMilestone(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.milestone = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Milestone',
|
||||
'project_id': cls.project_pigs.id,
|
||||
})
|
||||
|
||||
def test_milestones_settings_change(self):
|
||||
# To be sure the feature is disabled globally to begin the test.
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
self.assertFalse(self.env.user.has_group('project.group_project_milestone'), 'The "Milestones" feature should not be globally enabled by default.')
|
||||
self.assertFalse(self.project_pigs.allow_milestones, 'The "Milestones" feature should not be enabled by default.')
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
self.assertTrue(self.env.user.has_group('project.group_project_milestone'), 'The "Milestones" feature should globally be enabled.')
|
||||
self.assertTrue(self.project_pigs.allow_milestones, 'The "Milestones" feature should be enabled by default on the project when the feature is enabled.')
|
||||
project = self.env['project.project'].create({'name': 'Test allow_milestones on New Project'})
|
||||
self.assertTrue(project.allow_milestones, 'The "Milestones" feature should be enabled by default when the feature is enabled globally.')
|
||||
|
||||
with Form(self.env['project.project']) as project_form:
|
||||
project_form.name = 'My Mouses Project'
|
||||
self.assertTrue(project_form.allow_milestones, 'New projects allow_milestones should be True by default.')
|
||||
|
||||
def test_change_project_in_task(self):
|
||||
""" Test when a task is linked to a milestone and when we change its project the milestone is removed
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Set a milestone on the task
|
||||
2) Change the project of that task
|
||||
3) Check no milestone is linked to the task
|
||||
"""
|
||||
self.task_1.milestone_id = self.milestone
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone)
|
||||
|
||||
self.task_1.project_id = self.project_goats
|
||||
self.assertFalse(self.task_1.milestone_id, 'No milestone should be linked to the task since its project has changed')
|
||||
|
||||
def test_duplicate_project_duplicates_milestones_on_tasks(self):
|
||||
"""
|
||||
Test when we duplicate the project with tasks linked to its' milestones,
|
||||
that the tasks in the new project are also linked to the duplicated milestones of the new project
|
||||
We can't really robustly test that the mapping of task -> milestone is the same in the old and new project,
|
||||
the workaround way of testing the mapping is basing ourselves on unique names and check that those are equals in the test.
|
||||
"""
|
||||
# original unique_names, used to map between the original -> copy
|
||||
unique_name_1 = "unique_name_1"
|
||||
unique_name_2 = "unique_name_2"
|
||||
unique_names = [unique_name_1, unique_name_2]
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Test project',
|
||||
'allow_milestones': True,
|
||||
})
|
||||
milestones = self.env['project.milestone'].create([{
|
||||
'name': unique_name_1,
|
||||
'project_id': project.id,
|
||||
}, {
|
||||
'name': unique_name_2,
|
||||
'project_id': project.id,
|
||||
}])
|
||||
tasks = self.env['project.task'].create([{
|
||||
'name': unique_name_1,
|
||||
'project_id': project.id,
|
||||
'milestone_id': milestones[0].id,
|
||||
}, {
|
||||
'name': unique_name_2,
|
||||
'project_id': project.id,
|
||||
'milestone_id': milestones[1].id,
|
||||
}])
|
||||
self.assertEqual(tasks[0].milestone_id, milestones[0])
|
||||
self.assertEqual(tasks[1].milestone_id, milestones[1])
|
||||
project_copy = project.copy()
|
||||
self.assertNotEqual(project_copy.milestone_ids, False)
|
||||
self.assertEqual(project.milestone_ids.mapped('name'), project_copy.milestone_ids.mapped('name'))
|
||||
self.assertNotEqual(project_copy.task_ids, False)
|
||||
for milestone in project_copy.task_ids.milestone_id:
|
||||
self.assertTrue(milestone in project_copy.milestone_ids)
|
||||
for unique_name in unique_names:
|
||||
orig_task = project.task_ids.filtered(lambda t: t.name == unique_name)
|
||||
copied_task = project_copy.task_ids.filtered(lambda t: t.name == unique_name)
|
||||
self.assertEqual(orig_task.name, copied_task.name, "The copied_task should be a copy of the original task")
|
||||
self.assertNotEqual(copied_task.milestone_id, False,
|
||||
"We should copy the milestone and it shouldn't be reset to false from _compute_milestone_id")
|
||||
self.assertEqual(orig_task.milestone_id.name, copied_task.milestone_id.name,
|
||||
"the copied milestone should be a copy if the original ")
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase, users, tagged
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
|
||||
|
||||
class TestProjectProfitabilityCommon(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.partner = cls.env['res.partner'].create({
|
||||
'name': 'Georges',
|
||||
'email': 'georges@project-profitability.com'})
|
||||
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan A',
|
||||
'company_id': False,
|
||||
})
|
||||
cls.analytic_account = cls.env['account.analytic.account'].create({
|
||||
'name': 'Project - AA',
|
||||
'code': 'AA-1234',
|
||||
'plan_id': cls.analytic_plan.id,
|
||||
})
|
||||
cls.project = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Project',
|
||||
'partner_id': cls.partner.id,
|
||||
'analytic_account_id': cls.analytic_account.id,
|
||||
})
|
||||
cls.task = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Task',
|
||||
'project_id': cls.project.id,
|
||||
})
|
||||
cls.project_profitability_items_empty = {
|
||||
'revenues': {'data': [], 'total': {'invoiced': 0.0, 'to_invoice': 0.0}},
|
||||
'costs': {'data': [], 'total': {'billed': 0.0, 'to_bill': 0.0}},
|
||||
}
|
||||
|
||||
|
||||
class TestProfitability(TestProjectProfitabilityCommon):
|
||||
def test_project_profitability(self):
|
||||
""" Test the project profitability has no data found
|
||||
|
||||
In this module, the project profitability should have no data.
|
||||
So the no revenue and cost should be found.
|
||||
"""
|
||||
profitability_items = self.project._get_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
profitability_items,
|
||||
self.project_profitability_items_empty,
|
||||
'The profitability data of the project should be return no data and so 0 for each total amount.'
|
||||
)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectProfitabilityAccess(TestProjectProfitabilityCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.project_user = mail_new_test_user(cls.env, 'Project User', groups='project.group_project_user')
|
||||
cls.project_manager = mail_new_test_user(cls.env, 'Project Admin', groups='project.group_project_manager')
|
||||
|
||||
@users('Project User', 'Project Admin')
|
||||
def test_project_profitability_read(self):
|
||||
""" Test the project profitability read access rights
|
||||
|
||||
In other modules, project profitability may contain some data.
|
||||
The project user and project admin should have read access rights to project profitability.
|
||||
"""
|
||||
self.project.with_user(self.env.user)._get_profitability_items(False)
|
||||
|
|
@ -0,0 +1,856 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from odoo.tests.common import TransactionCase, 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(TransactionCase):
|
||||
@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_model()
|
||||
|
||||
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):
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
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': False,
|
||||
'tue': False,
|
||||
'wed': False,
|
||||
'thu': False,
|
||||
'fri': False,
|
||||
'sat': False,
|
||||
'sun': False,
|
||||
})
|
||||
|
||||
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))
|
||||
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=datetime(2020, 1, 10),
|
||||
repeat_interval=6, # twice a year
|
||||
repeat_unit='month',
|
||||
repeat_type='until',
|
||||
repeat_until=datetime(2021, 1, 11),
|
||||
repeat_on_month='date',
|
||||
repeat_on_year=False,
|
||||
weekdays=[TH(+1)],
|
||||
repeat_day='3', # the 3rd of the month
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=1)
|
||||
|
||||
self.assertEqual(len(dates), 2)
|
||||
self.assertEqual(dates[0], datetime(2020, 7, 3))
|
||||
self.assertEqual(dates[1], datetime(2021, 1, 3))
|
||||
|
||||
# Should generate a date at the last day of the current month
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2022, 2, 26),
|
||||
repeat_interval=1,
|
||||
repeat_unit='month',
|
||||
repeat_type='until',
|
||||
repeat_until=date(2022, 2, 28),
|
||||
repeat_on_month='date',
|
||||
repeat_on_year=False,
|
||||
weekdays=False,
|
||||
repeat_day=31,
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=5)
|
||||
|
||||
self.assertEqual(len(dates), 1)
|
||||
self.assertEqual(dates[0], date(2022, 2, 28))
|
||||
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2022, 11, 26),
|
||||
repeat_interval=3,
|
||||
repeat_unit='month',
|
||||
repeat_type='until',
|
||||
repeat_until=date(2024, 2, 29),
|
||||
repeat_on_month='date',
|
||||
repeat_on_year=False,
|
||||
weekdays=False,
|
||||
repeat_day=25,
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=5)
|
||||
|
||||
self.assertEqual(len(dates), 5)
|
||||
self.assertEqual(dates[0], date(2023, 2, 25))
|
||||
self.assertEqual(dates[1], date(2023, 5, 25))
|
||||
self.assertEqual(dates[2], date(2023, 8, 25))
|
||||
self.assertEqual(dates[3], date(2023, 11, 25))
|
||||
self.assertEqual(dates[4], date(2024, 2, 25))
|
||||
|
||||
# Use the exact same parameters than the previous test but with a repeat_day that is not passed yet
|
||||
# So we generate an additional date in the current month
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2022, 11, 26),
|
||||
repeat_interval=3,
|
||||
repeat_unit='month',
|
||||
repeat_type='until',
|
||||
repeat_until=date(2024, 2, 29),
|
||||
repeat_on_month='date',
|
||||
repeat_on_year=False,
|
||||
weekdays=False,
|
||||
repeat_day=31,
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=5)
|
||||
|
||||
self.assertEqual(len(dates), 6)
|
||||
self.assertEqual(dates[0], date(2022, 11, 30))
|
||||
self.assertEqual(dates[1], date(2023, 2, 28))
|
||||
self.assertEqual(dates[2], date(2023, 5, 31))
|
||||
self.assertEqual(dates[3], date(2023, 8, 31))
|
||||
self.assertEqual(dates[4], date(2023, 11, 30))
|
||||
self.assertEqual(dates[5], date(2024, 2, 29))
|
||||
|
||||
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))
|
||||
|
||||
def test_recurrence_cron_repeat_after_subtasks(self):
|
||||
|
||||
def get_task_and_subtask_counts(domain):
|
||||
tasks = self.env['project.task'].search(domain)
|
||||
return tasks, len(tasks), len(tasks.filtered('parent_id'))
|
||||
|
||||
# Required for `child_ids` to be visible in the view
|
||||
# {'invisible': [('allow_subtasks', '=', False)]}
|
||||
self.project_recurring.allow_subtasks = True
|
||||
parent_task = self.env['project.task'].create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_recurring.id
|
||||
})
|
||||
child_task = self.env['project.task'].create({
|
||||
'name': 'Child Task',
|
||||
'parent_id': parent_task.id,
|
||||
})
|
||||
domain = [('project_id', '=', self.project_recurring.id)]
|
||||
with freeze_time("2020-01-01"):
|
||||
with Form(child_task.with_context({'tracking_disable': True})) as form:
|
||||
form.description = 'my super recurring task bla bla bla'
|
||||
form.date_deadline = datetime(2020, 2, 1)
|
||||
form.display_project_id = parent_task.project_id
|
||||
|
||||
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'
|
||||
subtask = form.save()
|
||||
subtask.planned_hours = 2
|
||||
|
||||
self.assertEqual(subtask.recurrence_id.next_recurrence_date, date(2020, 1, 15))
|
||||
project_tasks, project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 2)
|
||||
self.assertEqual(project_subtask_count, 1)
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
self.assertEqual(self.env['project.task'].search_count(domain), 2, 'no extra task should be created')
|
||||
self.assertEqual(subtask.recurrence_id.recurrence_left, 2)
|
||||
for task in project_tasks:
|
||||
self.assertEqual(task.display_project_id, parent_task.project_id, "All tasks should have a display project id set")
|
||||
|
||||
with freeze_time("2020-01-15"):
|
||||
project_tasks, project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 2)
|
||||
self.assertEqual(project_subtask_count, 1)
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
project_tasks, project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 3)
|
||||
self.assertEqual(project_subtask_count, 2)
|
||||
self.assertEqual(subtask.recurrence_id.recurrence_left, 1)
|
||||
for task in project_tasks:
|
||||
self.assertEqual(task.display_project_id, parent_task.project_id, "All tasks should have a display project id set")
|
||||
|
||||
with freeze_time("2020-02-15"):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
project_tasks, project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 4)
|
||||
self.assertEqual(project_subtask_count, 3)
|
||||
self.assertEqual(subtask.recurrence_id.recurrence_left, 0)
|
||||
for task in project_tasks:
|
||||
self.assertEqual(task.display_project_id, parent_task.project_id, "All tasks should have a display project id set")
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
_, project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 4)
|
||||
self.assertEqual(project_subtask_count, 3)
|
||||
self.assertEqual(subtask.recurrence_id.recurrence_left, 0)
|
||||
|
||||
tasks = self.env['project.task'].search(domain)
|
||||
self.assertEqual(len(tasks), 4)
|
||||
|
||||
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_after_subsubtasks(self):
|
||||
"""
|
||||
Tests how the recurrence is working when a task has subtasks that have recurrence too
|
||||
We have at the beginning:
|
||||
index Task name Recurrent parent
|
||||
0 Parent Task no no
|
||||
1 Subtask 1 no Parent task
|
||||
2 Subtask 2 Montly, 15, for 2 tasks Parent task
|
||||
3 Grand child task 1 Daily, 5 tasks Subtask 2 that has recurrence
|
||||
4 Grand child task 2 no Subtask 2 that has recurrence
|
||||
5 Grand child task 3 no Grand child task 2
|
||||
6 Grand child task 4 no Grand child task 3
|
||||
7 Grand child task 5 no Grand child task 4
|
||||
1) After 5 days (including today), there will be 5 occurences of *task index 3*.
|
||||
2) After next 15th of the month, there will be 2 occurences of *task index 2* and a *copy of tasks 3, 4, 5, 6* (not 7)
|
||||
3) 5 days afterwards, there will be 5 occurences of the *copy of task index 3*
|
||||
4) The 15th of the next month, there won't be any other new occurence since all recurrences have been consumed.
|
||||
"""
|
||||
|
||||
def get_task_and_subtask_counts(domain):
|
||||
tasks = self.env['project.task'].search(domain)
|
||||
return len(tasks), len(tasks.filtered('parent_id'))
|
||||
|
||||
# Phase 0 : Initialize test case
|
||||
# Required for `child_ids` to be visible in the view
|
||||
# {'invisible': [('allow_subtasks', '=', False)]}
|
||||
self.project_recurring.allow_subtasks = True
|
||||
parent_task = self.env['project.task'].create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_recurring.id
|
||||
})
|
||||
domain = [('project_id', '=', self.project_recurring.id)]
|
||||
child_task_1, child_task_2_recurrence = self.env['project.task'].create([
|
||||
{'name': 'Child task 1'},
|
||||
{'name': 'Child task 2 that have recurrence'},
|
||||
])
|
||||
with Form(parent_task.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(child_task_1)
|
||||
task_form.child_ids.add(child_task_2_recurrence)
|
||||
|
||||
grand_child_task_1 = self.env['project.task'].create({
|
||||
'name': 'Grandchild task 1 (recurrent)',
|
||||
})
|
||||
grand_child_task_2_recurrence = self.env['project.task'].create({
|
||||
'name': 'Grandchild task 2',
|
||||
})
|
||||
with freeze_time("2020-01-01"):
|
||||
recurrent_subtask = parent_task.child_ids[0]
|
||||
with Form(recurrent_subtask.with_context(tracking_disable=True)) as task_form:
|
||||
task_form.recurring_task = True
|
||||
task_form.repeat_interval = 1
|
||||
task_form.repeat_unit = 'month'
|
||||
task_form.repeat_type = 'after'
|
||||
task_form.repeat_number = 1
|
||||
task_form.repeat_on_month = 'date'
|
||||
task_form.repeat_day = '15'
|
||||
task_form.date_deadline = datetime(2020, 2, 1)
|
||||
task_form.child_ids.add(grand_child_task_1)
|
||||
task_form.child_ids.add(grand_child_task_2_recurrence)
|
||||
# configure recurring subtask
|
||||
recurrent_subsubtask = recurrent_subtask.child_ids.filtered(lambda t: t.name == 'Grandchild task 1 (recurrent)')
|
||||
non_recurrent_subsubtask = recurrent_subtask.child_ids.filtered(lambda t: t.name == 'Grandchild task 2')
|
||||
with Form(recurrent_subsubtask.with_context(tracking_disable=True)) as subtask_form:
|
||||
subtask_form.recurring_task = True
|
||||
subtask_form.repeat_interval = 1
|
||||
subtask_form.repeat_unit = 'day'
|
||||
subtask_form.repeat_type = 'after'
|
||||
subtask_form.repeat_number = 4
|
||||
subtask_form.date_deadline = datetime(2020, 2, 3)
|
||||
|
||||
grand_child_task_3, grand_child_task_4, grand_child_task_5 = self.env['project.task'].create([
|
||||
{'name': 'Grandchild task 3'},
|
||||
{'name': 'Grandchild task 4'},
|
||||
{'name': 'Grandchild task 5'},
|
||||
])
|
||||
# create non-recurring grandchild subtasks
|
||||
with Form(non_recurrent_subsubtask.with_context(tracking_disable=True)) as subtask_form:
|
||||
subtask_form.child_ids.add(grand_child_task_3)
|
||||
non_recurrent_subsubtask = non_recurrent_subsubtask.child_ids
|
||||
with Form(non_recurrent_subsubtask.with_context(tracking_disable=True)) as subtask_form:
|
||||
subtask_form.child_ids.add(grand_child_task_4)
|
||||
non_recurrent_subsubtask = non_recurrent_subsubtask.child_ids
|
||||
with Form(non_recurrent_subsubtask.with_context(tracking_disable=True)) as subtask_form:
|
||||
subtask_form.child_ids.add(grand_child_task_5)
|
||||
|
||||
self.assertTrue(recurrent_subtask.recurrence_id)
|
||||
self.assertEqual(recurrent_subtask.recurrence_id.next_recurrence_date, date(2020, 1, 15))
|
||||
project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 8)
|
||||
self.assertEqual(project_subtask_count, 7)
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
self.assertEqual(self.env['project.task'].search_count(domain), 8, 'no extra task should be created')
|
||||
all_tasks = self.env['project.task'].search(domain)
|
||||
task_names = all_tasks.parent_id.mapped('name')
|
||||
self.assertEqual(task_names.count('Parent Task'), 1)
|
||||
self.assertEqual(task_names.count('Child task 2 that have recurrence'), 1)
|
||||
self.assertEqual(task_names.count('Grandchild task 2'), 1)
|
||||
self.assertEqual(task_names.count('Grandchild task 3'), 1)
|
||||
self.assertEqual(task_names.count('Grandchild task 4'), 1)
|
||||
|
||||
# Phase 1 : Verify recurrences of Grandchild task 1 (recurrent)
|
||||
n = 8
|
||||
for i in range(1, 5):
|
||||
with freeze_time("2020-01-%02d" % (i + 1)):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, n + i) # + 1 occurence of task 3
|
||||
self.assertEqual(project_subtask_count, n + i - 1)
|
||||
with freeze_time("2020-01-11"):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 12) # total = 5 occurences of task 3
|
||||
self.assertEqual(project_subtask_count, 11)
|
||||
|
||||
# Phase 2 : Verify recurrences of Child task 2 that have recurrence
|
||||
with freeze_time("2020-01-15"):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
all_tasks = self.env['project.task'].search(domain)
|
||||
task_names = all_tasks.parent_id.mapped('name')
|
||||
self.assertEqual(task_names.count('Parent Task'), 1)
|
||||
self.assertEqual(task_names.count('Child task 2 that have recurrence'), 2)
|
||||
self.assertEqual(task_names.count('Grandchild task 2'), 2)
|
||||
self.assertEqual(task_names.count('Grandchild task 3'), 2)
|
||||
self.assertEqual(task_names.count('Grandchild task 4'), 1)
|
||||
self.assertEqual(len(task_names), 8)
|
||||
self.assertEqual(project_task_count, 12 + 1 + 4) # 12 + the recurring task 5 + the 2 childs (3, 4) + 1 grandchild (5) + 1 grandgrandchild (6)
|
||||
self.assertEqual(project_subtask_count, 16)
|
||||
bottom_genealogy = all_tasks.filtered(lambda t: not t.child_ids.exists())
|
||||
bottom_genealogy_name = bottom_genealogy.mapped('name')
|
||||
self.assertEqual(bottom_genealogy_name.count('Child task 1'), 1)
|
||||
self.assertEqual(bottom_genealogy_name.count('Grandchild task 1 (recurrent)'), 6)
|
||||
# Grandchild task 5 should not be copied !
|
||||
self.assertEqual(bottom_genealogy_name.count('Grandchild task 5'), 1)
|
||||
|
||||
# Phase 3 : Verify recurrences of the copy of Grandchild task 1 (recurrent)
|
||||
n = 17
|
||||
for i in range(1, 5):
|
||||
with freeze_time("2020-01-%02d" % (i + 15)):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, n + i)
|
||||
self.assertEqual(project_subtask_count, n + i - 1)
|
||||
with freeze_time("2020-01-25"):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 21)
|
||||
self.assertEqual(project_subtask_count, 20)
|
||||
|
||||
# Phase 4 : No more recurrence
|
||||
with freeze_time("2020-02-15"):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
project_task_count, project_subtask_count = get_task_and_subtask_counts(domain)
|
||||
self.assertEqual(project_task_count, 21)
|
||||
self.assertEqual(project_subtask_count, 20)
|
||||
|
||||
all_tasks = self.env['project.task'].search(domain)
|
||||
self.assertEqual(len(all_tasks), 21)
|
||||
deadlines = all_tasks.sorted('create_date').mapped('date_deadline')
|
||||
self.assertTrue(bool(deadlines[-4]))
|
||||
self.assertTrue(bool(deadlines[-3]))
|
||||
del deadlines[-4]
|
||||
del deadlines[-3]
|
||||
self.assertTrue(not any(deadlines), "Deadline should not be copied")
|
||||
|
||||
bottom_genealogy = all_tasks.filtered(lambda t: not t.child_ids.exists())
|
||||
bottom_genealogy_name = bottom_genealogy.mapped('name')
|
||||
self.assertEqual(bottom_genealogy_name.count('Child task 1'), 1)
|
||||
self.assertEqual(bottom_genealogy_name.count('Grandchild task 1 (recurrent)'), 10)
|
||||
self.assertEqual(bottom_genealogy_name.count('Grandchild task 5'), 1)
|
||||
|
||||
for f in self.env['project.task.recurrence']._get_recurring_fields():
|
||||
self.assertTrue(all_tasks[0][f] == all_tasks[1][f] == all_tasks[2][f], "Field %s should have been copied" % f)
|
||||
|
||||
def test_compute_recurrence_message_with_lang_not_set(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Test task with user language not set',
|
||||
'project_id': self.project_recurring.id,
|
||||
'recurring_task': True,
|
||||
'repeat_interval': 1,
|
||||
'repeat_unit': 'week',
|
||||
'repeat_type': 'after',
|
||||
'repeat_number': 2,
|
||||
'mon': True,
|
||||
})
|
||||
|
||||
self.env.user.lang = None
|
||||
task._compute_recurrence_message()
|
||||
|
||||
def test_disabling_recurrence(self):
|
||||
"""
|
||||
Disabling the recurrence of one task in a recurrence suite should disable *all*
|
||||
recurrences option on the tasks linked to that recurrence
|
||||
"""
|
||||
with freeze_time("2020-01-01"):
|
||||
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)])
|
||||
|
||||
task_b.recurring_task = False
|
||||
|
||||
self.assertFalse(any((task_a + task_b + task_c).mapped('recurring_task')),
|
||||
"All tasks in the recurrence should have their recurrence disabled")
|
||||
|
||||
def test_recurrence_weekday_per_month(self):
|
||||
with freeze_time("2023-10-01"):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'test recurring task',
|
||||
'project_id': self.project_recurring.id,
|
||||
'recurring_task': True,
|
||||
# Second Tuesday of the month
|
||||
'repeat_interval': 1,
|
||||
'repeat_unit': 'month',
|
||||
'repeat_week': 'second',
|
||||
'repeat_on_month': 'day',
|
||||
'repeat_on_year': 'date',
|
||||
'repeat_weekday': 'tue',
|
||||
'repeat_type': 'forever',
|
||||
})
|
||||
self.assertEqual(task.recurrence_id.next_recurrence_date, date(2023, 10, 10))
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProjectReport(TestProjectCommon):
|
||||
def test_avg_rating_measure(self):
|
||||
rating_vals = {
|
||||
'res_model_id': self.env['ir.model']._get('project.task').id,
|
||||
'rated_partner_id': self.partner_1.id,
|
||||
'partner_id': self.partner_1.id,
|
||||
'consumed': True,
|
||||
}
|
||||
self.env['rating.rating'].create([
|
||||
{**rating_vals, 'rating': 5, 'res_id': self.task_1.id},
|
||||
{**rating_vals, 'rating': 4, 'res_id': self.task_1.id},
|
||||
{**rating_vals, 'rating': 4.25, 'res_id': self.task_2.id},
|
||||
])
|
||||
self.assertEqual(self.task_1.rating_avg, 4.5)
|
||||
self.assertEqual(self.task_1.rating_last_value, 4.0)
|
||||
|
||||
self.assertEqual(self.task_2.rating_avg, 4.25)
|
||||
self.assertEqual(self.task_2.rating_last_value, 4.25)
|
||||
|
||||
task_3 = self.env['project.task'].create({
|
||||
'name': 'task 3',
|
||||
'project_id': self.project_pigs.id,
|
||||
'partner_id': self.partner_1.id,
|
||||
'user_ids': self.task_1.user_ids,
|
||||
})
|
||||
self.assertEqual(task_3.rating_avg, 0)
|
||||
self.assertEqual(task_3.rating_last_value, 0)
|
||||
|
||||
# fix cache consistency
|
||||
self.env['project.task'].invalidate_model(['rating_avg', 'rating_last_value'])
|
||||
|
||||
tasks = [self.task_1, self.task_2, task_3]
|
||||
for task in tasks:
|
||||
rating_values = task.read(['rating_avg', 'rating_last_value'])[0]
|
||||
task_report = self.env['report.project.task.user'].search_read([('project_id', '=', self.project_pigs.id), ('task_id', '=', task.id)], ['rating_avg', 'rating_last_value'])[0]
|
||||
self.assertDictEqual(task_report, rating_values, 'The rating average and the last rating value for the task 1 should be the same in the report and on the task.')
|
||||
|
|
@ -0,0 +1,458 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
class TestProjectSharingCommon(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
project_sharing_stages_vals_list = [
|
||||
(0, 0, {'name': 'To Do', 'sequence': 1}),
|
||||
(0, 0, {'name': 'Done', 'sequence': 10, 'fold': True, 'rating_template_id': cls.env.ref('project.rating_project_request_email_template').id}),
|
||||
]
|
||||
|
||||
cls.partner_portal = cls.env['res.partner'].create({
|
||||
'name': 'Chell Gladys',
|
||||
'email': 'chell@gladys.portal',
|
||||
'company_id': False,
|
||||
'user_ids': [Command.link(cls.user_portal.id)]})
|
||||
|
||||
cls.project_cows = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Cows',
|
||||
'privacy_visibility': 'portal',
|
||||
'alias_name': 'project+cows',
|
||||
'type_ids': project_sharing_stages_vals_list,
|
||||
})
|
||||
cls.project_portal = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Portal',
|
||||
'privacy_visibility': 'portal',
|
||||
'alias_name': 'project+portal',
|
||||
'partner_id': cls.user_portal.partner_id.id,
|
||||
'type_ids': project_sharing_stages_vals_list,
|
||||
})
|
||||
cls.project_portal.message_subscribe(partner_ids=[cls.partner_portal.id])
|
||||
|
||||
cls.project_no_collabo = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'No Collabo',
|
||||
'privacy_visibility': 'followers',
|
||||
'alias_name': 'project+nocollabo',
|
||||
})
|
||||
|
||||
cls.task_cow = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Cow UserTask',
|
||||
'user_ids': cls.user_projectuser,
|
||||
'project_id': cls.project_cows.id,
|
||||
})
|
||||
cls.task_portal = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Portal UserTask',
|
||||
'user_ids': cls.user_projectuser,
|
||||
'project_id': cls.project_portal.id,
|
||||
})
|
||||
cls.task_no_collabo = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'No Collabo Task',
|
||||
'project_id': cls.project_no_collabo.id,
|
||||
})
|
||||
|
||||
cls.task_tag = cls.env['project.tags'].create({'name': 'Foo'})
|
||||
|
||||
cls.project_sharing_form_view_xml_id = 'project.project_sharing_project_task_view_form'
|
||||
|
||||
def get_project_sharing_form_view(self, record, with_user=None):
|
||||
return Form(
|
||||
record.with_user(with_user or self.env.user),
|
||||
view=self.project_sharing_form_view_xml_id
|
||||
)
|
||||
|
||||
@tagged('project_sharing')
|
||||
class TestProjectSharing(TestProjectSharingCommon):
|
||||
|
||||
def test_project_share_wizard(self):
|
||||
""" Test Project Share Wizard
|
||||
|
||||
Test Cases:
|
||||
==========
|
||||
1) Create the wizard record
|
||||
2) Check if no access rights are given to a portal user
|
||||
3) Add access rights to a portal user
|
||||
"""
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_portal.id,
|
||||
'access_mode': 'edit',
|
||||
})
|
||||
self.assertFalse(project_share_wizard.partner_ids, 'No collaborator should be in the wizard.')
|
||||
self.assertFalse(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should not have accessed in project sharing views.')
|
||||
project_share_wizard.write({'partner_ids': [Command.link(self.user_portal.partner_id.id)]})
|
||||
project_share_wizard.action_send_mail()
|
||||
self.assertEqual(len(self.project_portal.collaborator_ids), 1, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.')
|
||||
self.assertDictEqual({
|
||||
'partner_id': self.project_portal.collaborator_ids.partner_id,
|
||||
'project_id': self.project_portal.collaborator_ids.project_id,
|
||||
}, {
|
||||
'partner_id': self.user_portal.partner_id,
|
||||
'project_id': self.project_portal,
|
||||
}, 'The access rights added should be the read access for the portal project for Chell Gladys.')
|
||||
self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should have read access to the portal project with project sharing feature.')
|
||||
|
||||
def test_project_sharing_access(self):
|
||||
""" Check if the different user types can access to project sharing feature as expected. """
|
||||
with self.assertRaises(AccessError, msg='The public user should not have any access to project sharing feature of the portal project.'):
|
||||
self.project_portal.with_user(self.user_public)._check_project_sharing_access()
|
||||
self.assertTrue(self.project_portal.with_user(self.user_projectuser)._check_project_sharing_access(), 'The internal user should have all accesses to project sharing feature of the portal project.')
|
||||
self.assertFalse(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should not have any access to project sharing feature of the portal project.')
|
||||
self.project_portal.write({'collaborator_ids': [Command.create({'partner_id': self.user_portal.partner_id.id})]})
|
||||
self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user can access to project sharing feature of the portal project.')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_create_task_in_project_sharing(self):
|
||||
""" Test when portal user creates a task in project sharing views.
|
||||
|
||||
Test Cases:
|
||||
==========
|
||||
1) Give the 'read' access mode to a portal user in a project and try to create task with this user.
|
||||
2) Give the 'comment' access mode to a portal user in a project and try to create task with this user.
|
||||
3) Give the 'edit' access mode to a portal user in a project and try to create task with this user.
|
||||
3.1) Try to change the project of the new task with this user.
|
||||
"""
|
||||
self.project_portal.allow_subtasks = True
|
||||
Task = self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id, 'default_user_ids': [(4, self.user_portal.id)]})
|
||||
# 1) Give the 'read' access mode to a portal user in a project and try to create task with this user.
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."):
|
||||
with self.get_project_sharing_form_view(Task, self.user_portal) as form:
|
||||
form.name = 'Test'
|
||||
task = form.save()
|
||||
|
||||
self.project_portal.write({
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id}),
|
||||
],
|
||||
})
|
||||
with self.get_project_sharing_form_view(Task, self.user_portal) as form:
|
||||
form.name = 'Test'
|
||||
with form.child_ids.new() as subtask_form:
|
||||
subtask_form.name = 'Test Subtask'
|
||||
task = form.save()
|
||||
self.assertEqual(task.name, 'Test')
|
||||
self.assertEqual(task.project_id, self.project_portal)
|
||||
self.assertFalse(task.portal_user_names)
|
||||
|
||||
# Check creating a sub-task while creating the parent task works as expected.
|
||||
self.assertEqual(task.child_ids.name, 'Test Subtask')
|
||||
self.assertEqual(task.child_ids.project_id, self.project_portal)
|
||||
self.assertFalse(task.child_ids.portal_user_names, 'by default no user should be assigned to a subtask created by the portal user.')
|
||||
self.assertFalse(task.child_ids.user_ids, 'No user should be assigned to the new subtask.')
|
||||
|
||||
# 3.1) Try to change the project of the new task with this user.
|
||||
with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
|
||||
form.project_id = self.project_cows
|
||||
task = form.save()
|
||||
|
||||
Task = Task.with_user(self.user_portal)
|
||||
|
||||
# Allow to set as parent a task he has access to
|
||||
task = Task.create({'name': 'foo', 'parent_id': self.task_portal.id})
|
||||
self.assertEqual(task.parent_id, self.task_portal)
|
||||
# Disallow to set as parent a task he doesn't have access to
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user to set a parent task he doesn't have access to."):
|
||||
Task.create({'name': 'foo', 'parent_id': self.task_no_collabo.id})
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user to set a parent task he doesn't have access to."):
|
||||
task = Task.with_context(default_parent_id=self.task_no_collabo.id).create({'name': 'foo'})
|
||||
|
||||
# Create/Update a forbidden task through child_ids
|
||||
with self.assertRaisesRegex(AccessError, "You cannot write on description"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.create({'name': 'Foo', 'description': 'Foo'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.update(self.task_no_collabo.id, {'name': 'Foo'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Task'"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.delete(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.unlink(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.link(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.create({'name': 'foo', 'child_ids': [Command.set([self.task_no_collabo.id])]})
|
||||
|
||||
# Same thing but using context defaults
|
||||
with self.assertRaisesRegex(AccessError, "You cannot write on description"):
|
||||
Task.with_context(default_child_ids=[Command.create({'name': 'Foo', 'description': 'Foo'})]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.update(self.task_no_collabo.id, {'name': 'Foo'})]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.delete(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.unlink(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.link(self.task_no_collabo.id)]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
Task.with_context(default_child_ids=[Command.set([self.task_no_collabo.id])]).create({'name': 'foo'})
|
||||
|
||||
# Create/update a tag through tag_ids
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to create 'Project Tags'"):
|
||||
Task.create({'name': 'foo', 'tag_ids': [Command.create({'name': 'Bar'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Project Tags'"):
|
||||
Task.create({'name': 'foo', 'tag_ids': [Command.update(self.task_tag.id, {'name': 'Bar'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Project Tags'"):
|
||||
Task.create({'name': 'foo', 'tag_ids': [Command.delete(self.task_tag.id)]})
|
||||
|
||||
# Same thing but using context defaults
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to create 'Project Tags'"):
|
||||
Task.with_context(default_tag_ids=[Command.create({'name': 'Bar'})]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Project Tags'"):
|
||||
Task.with_context(default_tag_ids=[Command.update(self.task_tag.id, {'name': 'Bar'})]).create({'name': 'foo'})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Project Tags'"):
|
||||
Task.with_context(default_tag_ids=[Command.delete(self.task_tag.id)]).create({'name': 'foo'})
|
||||
|
||||
task = Task.create({'name': 'foo', 'tag_ids': [Command.link(self.task_tag.id)]})
|
||||
self.assertEqual(task.tag_ids, self.task_tag)
|
||||
|
||||
Task.create({'name': 'foo', 'tag_ids': [Command.set([self.task_tag.id])]})
|
||||
self.assertEqual(task.tag_ids, self.task_tag)
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
|
||||
def test_edit_task_in_project_sharing(self):
|
||||
""" Test when portal user creates a task in project sharing views.
|
||||
|
||||
Test Cases:
|
||||
==========
|
||||
1) Give the 'read' access mode to a portal user in a project and try to edit task with this user.
|
||||
2) Give the 'comment' access mode to a portal user in a project and try to edit task with this user.
|
||||
3) Give the 'edit' access mode to a portal user in a project and try to create task with this user.
|
||||
3.1) Try to change the project of the new task with this user.
|
||||
3.2) Create a sub-task
|
||||
3.3) Create a second sub-task
|
||||
"""
|
||||
# 0) Allow to create subtasks in the project tasks
|
||||
# Required for `child_ids` to be visible in the view
|
||||
# {'invisible': [('allow_subtasks', '=', False)]}
|
||||
self.project_cows.allow_subtasks = True
|
||||
# 1) Give the 'read' access mode to a portal user in a project and try to create task with this user.
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."):
|
||||
with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form:
|
||||
form.name = 'Test'
|
||||
task = form.save()
|
||||
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'access_mode': 'edit',
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_cows.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.user_portal.partner_id.id),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id, 'uid': self.user_portal.id}), self.user_portal) as form:
|
||||
form.name = 'Test'
|
||||
task = form.save()
|
||||
self.assertEqual(task.name, 'Test')
|
||||
self.assertEqual(task.project_id, self.project_cows)
|
||||
|
||||
# 3.1) Try to change the project of the new task with this user.
|
||||
with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
|
||||
with self.get_project_sharing_form_view(task, self.user_portal) as form:
|
||||
form.project_id = self.project_portal
|
||||
|
||||
# 3.2) Create a sub-task
|
||||
with self.get_project_sharing_form_view(task, self.user_portal) as form:
|
||||
with form.child_ids.new() as subtask_form:
|
||||
subtask_form.name = 'Test Subtask'
|
||||
with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
|
||||
subtask_form.display_project_id = self.project_portal
|
||||
self.assertEqual(task.child_ids.name, 'Test Subtask')
|
||||
self.assertEqual(task.child_ids.project_id, self.project_cows)
|
||||
self.assertFalse(task.child_ids.portal_user_names, 'by default no user should be assigned to a subtask created by the portal user.')
|
||||
self.assertFalse(task.child_ids.user_ids, 'No user should be assigned to the new subtask.')
|
||||
|
||||
task2 = self.env['project.task'] \
|
||||
.with_context({
|
||||
'tracking_disable': True,
|
||||
'default_project_id': self.project_cows.id,
|
||||
'default_user_ids': [Command.set(self.user_portal.ids)],
|
||||
}) \
|
||||
.with_user(self.user_portal) \
|
||||
.create({'name': 'Test'})
|
||||
self.assertFalse(task2.portal_user_names, 'the portal user should not be assigned when the portal user creates a task into the project shared.')
|
||||
|
||||
# 3.3) Create a second sub-task
|
||||
with self.get_project_sharing_form_view(task, self.user_portal) as form:
|
||||
with form.child_ids.new() as subtask_form:
|
||||
subtask_form.name = 'Test Subtask'
|
||||
self.assertEqual(len(task.child_ids), 2, 'Check 2 subtasks has correctly been created by the user portal.')
|
||||
|
||||
# Allow to set as parent a task he has access to
|
||||
task.write({'parent_id': self.task_portal.id})
|
||||
self.assertEqual(task.parent_id, self.task_portal)
|
||||
# Disallow to set as parent a task he doesn't have access to
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user to set a parent task he doesn't have access to."):
|
||||
task.write({'parent_id': self.task_no_collabo.id})
|
||||
|
||||
# Create/Update a forbidden task through child_ids
|
||||
with self.assertRaisesRegex(AccessError, "You cannot write on description"):
|
||||
task.write({'child_ids': [Command.create({'name': 'Foo', 'description': 'Foo'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
task.write({'child_ids': [Command.update(self.task_no_collabo.id, {'name': 'Foo'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Task'"):
|
||||
task.write({'child_ids': [Command.delete(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
task.write({'child_ids': [Command.unlink(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
task.write({'child_ids': [Command.link(self.task_no_collabo.id)]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Task'"):
|
||||
task.write({'child_ids': [Command.set([self.task_no_collabo.id])]})
|
||||
|
||||
# Create/update a tag through tag_ids
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to create 'Project Tags'"):
|
||||
task.write({'tag_ids': [Command.create({'name': 'Bar'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to modify 'Project Tags'"):
|
||||
task.write({'tag_ids': [Command.update(self.task_tag.id, {'name': 'Bar'})]})
|
||||
with self.assertRaisesRegex(AccessError, "not allowed to delete 'Project Tags'"):
|
||||
task.write({'tag_ids': [Command.delete(self.task_tag.id)]})
|
||||
|
||||
task.write({'tag_ids': [Command.link(self.task_tag.id)]})
|
||||
self.assertEqual(task.tag_ids, self.task_tag)
|
||||
|
||||
task.write({'tag_ids': [Command.unlink(self.task_tag.id)]})
|
||||
self.assertFalse(task.tag_ids)
|
||||
|
||||
task.write({'tag_ids': [Command.link(self.task_tag.id)]})
|
||||
task.write({'tag_ids': [Command.clear()]})
|
||||
self.assertFalse(task.tag_ids, [])
|
||||
|
||||
task.write({'tag_ids': [Command.set([self.task_tag.id])]})
|
||||
self.assertEqual(task.tag_ids, self.task_tag)
|
||||
|
||||
|
||||
def test_portal_user_cannot_see_all_assignees(self):
|
||||
""" Test when the portal sees a task he cannot see all the assignees.
|
||||
|
||||
Because of a ir.rule in res.partner filters the assignees, the portal
|
||||
can only see the assignees in the same company than him.
|
||||
|
||||
Test Cases:
|
||||
==========
|
||||
1) add many assignees in a task
|
||||
2) check the portal user can read no assignee in this task. Should have an AccessError exception
|
||||
"""
|
||||
self.task_cow.write({'user_ids': [Command.link(self.user_projectmanager.id)]})
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user to access to a task he does not follow it and its project."):
|
||||
self.task_cow.with_user(self.user_portal).read(['portal_user_names'])
|
||||
self.assertEqual(len(self.task_cow.user_ids), 2, '2 users should be assigned in this task.')
|
||||
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'access_mode': 'edit',
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_cows.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.user_portal.partner_id.id),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
self.assertFalse(self.task_cow.with_user(self.user_portal).user_ids, 'the portal user should see no assigness in the task.')
|
||||
task_portal_read = self.task_cow.with_user(self.user_portal).read(['portal_user_names'])
|
||||
self.assertEqual(self.task_cow.portal_user_names, task_portal_read[0]['portal_user_names'], 'the portal user should see assignees name in the task via the `portal_user_names` field.')
|
||||
|
||||
def test_portal_user_can_change_stage_with_rating(self):
|
||||
""" Test portal user can change the stage of task to a stage with rating template email
|
||||
|
||||
The user should be able to change the stage and the email should be sent as expected
|
||||
if a email template is set in `rating_template_id` field in the new stage.
|
||||
"""
|
||||
self.project_portal.write({
|
||||
'rating_active': True,
|
||||
'rating_status': 'stage',
|
||||
'collaborator_ids': [
|
||||
Command.create({'partner_id': self.user_portal.partner_id.id}),
|
||||
],
|
||||
})
|
||||
self.task_portal.with_user(self.user_portal).write({'stage_id': self.project_portal.type_ids[-1].id})
|
||||
|
||||
def test_orm_method_with_true_false_domain(self):
|
||||
""" Test orm method overriden in project for project sharing works with TRUE_LEAF/FALSE_LEAF
|
||||
|
||||
Test Case
|
||||
=========
|
||||
1) Share a project in edit mode for portal user
|
||||
2) Search the portal task contained in the project shared by using a domain with TRUE_LEAF
|
||||
3) Check the task is found with the `search` method
|
||||
4) filter the task with `TRUE_DOMAIN` and check if the task is always returned by `filtered_domain` method
|
||||
5) filter the task with `FALSE_DOMAIN` and check if no task is returned by `filtered_domain` method
|
||||
6) Search the task with `FALSE_LEAF` and check no task is found with `search` method
|
||||
7) Call `read_group` method with `TRUE_LEAF` in the domain and check if the task is found
|
||||
8) Call `read_group` method with `FALSE_LEAF` in the domain and check if no task is found
|
||||
"""
|
||||
domain = [('id', '=', self.task_portal.id)]
|
||||
self.project_portal.write({
|
||||
'collaborator_ids': [Command.create({
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
})],
|
||||
})
|
||||
task = self.env['project.task'].with_user(self.user_portal).search(
|
||||
expression.AND([
|
||||
expression.TRUE_DOMAIN,
|
||||
domain,
|
||||
])
|
||||
)
|
||||
self.assertTrue(task, 'The task should be found.')
|
||||
self.assertEqual(task, task.filtered_domain(expression.TRUE_DOMAIN), 'The task found should be kept since the domain is truly')
|
||||
self.assertFalse(task.filtered_domain(expression.FALSE_DOMAIN), 'The task should not be found since the domain is falsy')
|
||||
task = self.env['project.task'].with_user(self.user_portal).search(
|
||||
expression.AND([
|
||||
expression.FALSE_DOMAIN,
|
||||
domain,
|
||||
]),
|
||||
)
|
||||
self.assertFalse(task, 'No task should be found since the domain contained a falsy tuple.')
|
||||
|
||||
task_read_group = self.env['project.task'].read_group(
|
||||
expression.AND([expression.TRUE_DOMAIN, domain]),
|
||||
['id'],
|
||||
[],
|
||||
)
|
||||
self.assertEqual(task_read_group[0]['__count'], 1, 'The task should be found with the read_group method containing a truly tuple.')
|
||||
self.assertEqual(task_read_group[0]['id'], self.task_portal.id, 'The task should be found with the read_group method containing a truly tuple.')
|
||||
|
||||
task_read_group = self.env['project.task'].read_group(
|
||||
expression.AND([expression.FALSE_DOMAIN, domain]),
|
||||
['id'],
|
||||
[],
|
||||
)
|
||||
self.assertFalse(task_read_group[0]['__count'], 'No result should found with the read_group since the domain is falsy.')
|
||||
|
||||
def test_milestone_read_access_right(self):
|
||||
""" This test ensures that a portal user has read access on the milestone of the project that was shared with him """
|
||||
|
||||
project_milestone = self.env['project.milestone'].create({
|
||||
'name': 'Test Project Milestone',
|
||||
'project_id': self.project_portal.id,
|
||||
})
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user to access to a milestone if he's not a collaborator of its project."):
|
||||
project_milestone.with_user(self.user_portal).read(['name'])
|
||||
|
||||
self.project_portal.write({
|
||||
'collaborator_ids': [Command.create({
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
})],
|
||||
})
|
||||
# Reading the milestone should no longer trigger an access error.
|
||||
project_milestone.with_user(self.user_portal).read(['name'])
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user to update a milestone."):
|
||||
project_milestone.with_user(self.user_portal).write(['name'])
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user to delete a milestone."):
|
||||
project_milestone.with_user(self.user_portal).unlink()
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user to create a milestone."):
|
||||
self.env['project.milestone'].with_user(self.user_portal).create({
|
||||
'name': 'Test Project new Milestone',
|
||||
'project_id': self.project_portal.id,
|
||||
})
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
from collections import OrderedDict
|
||||
from lxml import etree
|
||||
from re import search
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged, HttpCase
|
||||
|
||||
from .test_project_sharing import TestProjectSharingCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProjectSharingPortalAccess(TestProjectSharingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
project_share_wizard = cls.env['project.share.wizard'].create({
|
||||
'access_mode': 'edit',
|
||||
'res_model': 'project.project',
|
||||
'res_id': cls.project_portal.id,
|
||||
'partner_ids': [
|
||||
Command.link(cls.partner_portal.id),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
Task = cls.env['project.task']
|
||||
cls.read_protected_fields_task = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in Task._fields.items()
|
||||
if k in Task.SELF_READABLE_FIELDS
|
||||
])
|
||||
cls.write_protected_fields_task = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in Task._fields.items()
|
||||
if k in Task.SELF_WRITABLE_FIELDS
|
||||
])
|
||||
cls.readonly_protected_fields_task = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in Task._fields.items()
|
||||
if k in Task.SELF_READABLE_FIELDS and k not in Task.SELF_WRITABLE_FIELDS
|
||||
])
|
||||
cls.other_fields_task = OrderedDict([
|
||||
(k, v)
|
||||
for k, v in Task._fields.items()
|
||||
if k not in Task.SELF_READABLE_FIELDS
|
||||
])
|
||||
|
||||
def test_readonly_fields(self):
|
||||
""" The fields are not writeable should not be editable by the portal user. """
|
||||
view_infos = self.task_portal.get_view(self.env.ref(self.project_sharing_form_view_xml_id).id)
|
||||
fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')]
|
||||
project_task_fields = {
|
||||
field_name
|
||||
for field_name in fields
|
||||
if field_name not in self.write_protected_fields_task
|
||||
}
|
||||
with self.get_project_sharing_form_view(self.task_portal, self.user_portal) as form:
|
||||
for field in project_task_fields:
|
||||
with self.assertRaises(AssertionError, msg="Field '%s' should be readonly in the project sharing form view "):
|
||||
form.__setattr__(field, 'coucou')
|
||||
|
||||
def test_read_task_with_portal_user(self):
|
||||
self.task_portal.with_user(self.user_portal).read(self.read_protected_fields_task)
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
self.task_portal.with_user(self.user_portal).read(self.other_fields_task)
|
||||
|
||||
def test_write_with_portal_user(self):
|
||||
for field in self.readonly_protected_fields_task:
|
||||
with self.assertRaises(AccessError):
|
||||
self.task_portal.with_user(self.user_portal).write({field: 'dummy'})
|
||||
|
||||
for field in self.other_fields_task:
|
||||
with self.assertRaises(AccessError):
|
||||
self.task_portal.with_user(self.user_portal).write({field: 'dummy'})
|
||||
|
||||
|
||||
class TestProjectSharingChatterAccess(TestProjectSharingCommon, HttpCase):
|
||||
@mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
|
||||
def test_post_chatter_as_portal_user(self):
|
||||
self.project_no_collabo.privacy_visibility = 'portal'
|
||||
self.env['project.share.wizard'].create({
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_no_collabo.id,
|
||||
'access_mode': 'edit',
|
||||
'partner_ids': [Command.set([self.user_portal.partner_id.id])],
|
||||
}).action_send_mail()
|
||||
message = self.env['mail.message'].search([
|
||||
('partner_ids', 'in', self.user_portal.partner_id.id),
|
||||
])
|
||||
|
||||
share_link = str(message.body.split('href="')[1].split('">')[0])
|
||||
match = search(r"access_token=([^&]+)&pid=([^&]+)&hash=([^&]*)", share_link)
|
||||
access_token, pid, _hash = match.groups()
|
||||
|
||||
res = self.url_open(
|
||||
url="/mail/chatter_post",
|
||||
data=json.dumps({
|
||||
"params": {
|
||||
"res_model": 'project.task',
|
||||
"res_id": self.task_no_collabo.id,
|
||||
"message": '(-b ±√[b²-4ac]) / 2a',
|
||||
"attachment_ids": None,
|
||||
"attachment_tokens": None,
|
||||
"token": access_token,
|
||||
"pid": pid,
|
||||
"hash": _hash,
|
||||
},
|
||||
}),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
self.assertTrue(
|
||||
self.env['mail.message'].sudo().search([
|
||||
('author_id', '=', self.user_portal.partner_id.id),
|
||||
])
|
||||
)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProjectSharingUi(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
user = cls.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nolog': True}).create({
|
||||
'name': 'Georges',
|
||||
'login': 'georges1',
|
||||
'password': 'georges1',
|
||||
'email': 'georges@project.portal',
|
||||
'signature': 'SignGeorges',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [Command.set([cls.env.ref('base.group_portal').id])],
|
||||
})
|
||||
|
||||
cls.partner_portal = cls.env['res.partner'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Georges',
|
||||
'email': 'georges@project.portal',
|
||||
'company_id': False,
|
||||
'user_ids': [user.id],
|
||||
})
|
||||
cls.project_portal = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Project Sharing',
|
||||
'privacy_visibility': 'portal',
|
||||
'alias_name': 'project+sharing',
|
||||
'partner_id': cls.partner_portal.id,
|
||||
'type_ids': [
|
||||
Command.create({'name': 'To Do', 'sequence': 1}),
|
||||
Command.create({'name': 'Done', 'sequence': 10})
|
||||
],
|
||||
})
|
||||
cls.env['res.config.settings'].create({'group_project_milestone': True}).execute()
|
||||
|
||||
def test_01_project_sharing(self):
|
||||
""" Test Project Sharing UI with an internal user """
|
||||
self.start_tour("/web", 'project_sharing_tour', login="admin")
|
||||
|
||||
def test_02_project_sharing(self):
|
||||
""" Test project sharing ui with a portal user.
|
||||
|
||||
The additional data created here are the data created in the first test with the tour js.
|
||||
|
||||
Since a problem to logout Mitchell Admin to log in as Georges user, this test is created
|
||||
to launch a tour with portal user.
|
||||
"""
|
||||
project_share_wizard = self.env['project.share.wizard'].create({
|
||||
'access_mode': 'edit',
|
||||
'res_model': 'project.project',
|
||||
'res_id': self.project_portal.id,
|
||||
'partner_ids': [
|
||||
Command.link(self.partner_portal.id),
|
||||
],
|
||||
})
|
||||
project_share_wizard.action_send_mail()
|
||||
|
||||
self.project_portal.write({
|
||||
'task_ids': [Command.create({
|
||||
'name': "Test Project Sharing",
|
||||
'stage_id': self.project_portal.type_ids.filtered(lambda stage: stage.sequence == 10)[:1].id,
|
||||
})],
|
||||
})
|
||||
self.start_tour("/my/projects", 'portal_project_sharing_tour', login='georges1')
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectSubtasks(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Enable the company setting
|
||||
cls.env['res.config.settings'].create({
|
||||
'group_subtask_project': True
|
||||
}).execute()
|
||||
|
||||
def test_task_display_project_with_default_form(self):
|
||||
"""
|
||||
Create a task in the default task form should take the project set in the form or the default project in the context
|
||||
"""
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.name = 'Test Task 1'
|
||||
task_form.project_id = self.project_pigs
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id})) as task_form:
|
||||
task_form.name = 'Test Task 2'
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
def test_task_display_project_with_task_form2(self):
|
||||
"""
|
||||
Create a task in the task form 2 should take the project set in the form or the default project in the context
|
||||
"""
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True}), view="project.view_task_form2") as task_form:
|
||||
task_form.name = 'Test Task 1'
|
||||
task_form.project_id = self.project_pigs
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id}), view="project.view_task_form2") as task_form:
|
||||
task_form.name = 'Test Task 2'
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
def test_task_display_project_with_quick_create_task_form(self):
|
||||
"""
|
||||
Create a task in the quick create form should take the default project in the context
|
||||
"""
|
||||
with Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id}), view="project.quick_create_task_form") as task_form:
|
||||
task_form.name = 'Test Task 2'
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id.")
|
||||
|
||||
def test_task_display_project_with_any_task_form(self):
|
||||
"""
|
||||
Create a task in any form should take the default project in the context
|
||||
"""
|
||||
form_views = self.env['ir.ui.view'].search([('model', '=', 'project.task'), ('type', '=', 'form')])
|
||||
for form_view in form_views:
|
||||
task_form = Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_pigs.id, 'default_name': 'Test Task 1'}), view=form_view)
|
||||
# Some views have the `name` field invisible
|
||||
# As the goal is simply to test the default project field and not the name, we can skip setting the name
|
||||
# in the view and set it using `default_name` instead
|
||||
task = task_form.save()
|
||||
|
||||
self.assertEqual(task.project_id, self.project_pigs, "The project should be assigned from the default project, form_view name : %s." % form_view.name)
|
||||
self.assertEqual(task.display_project_id, task.project_id, "The display project of a first layer task should be assigned to project_id, form_view name : %s." % form_view.name)
|
||||
|
||||
def test_subtask_display_project(self):
|
||||
"""
|
||||
1) Create a subtask
|
||||
- Should have the same project as its parent
|
||||
- Shouldn't have a display project set.
|
||||
2) Set display project on subtask
|
||||
- Should not change parent project
|
||||
- Should change the subtask project
|
||||
- Display project should be correct
|
||||
3) Reset the display project to False
|
||||
- Should make the project equal to parent project
|
||||
- Display project should be correct
|
||||
4) Change parent task project
|
||||
- Should make the subtask project follow parent project
|
||||
- Display project should stay false
|
||||
5) Set display project on subtask and change parent task project
|
||||
- Should make the subtask project follow new display project id
|
||||
- Display project should be correct
|
||||
6) Remove parent task:
|
||||
- The project id should remain unchanged
|
||||
- The display project id should follow the project id
|
||||
7) Remove display project id then parent id:
|
||||
- The project id should be the one from the parent :
|
||||
- Since the display project id was removed, the project id is updated to the parent one
|
||||
- The display project id should follow the project id
|
||||
"""
|
||||
# 1)
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertFalse(self.task_1.child_ids.display_project_id, "The display project of a sub task should be false to project_id.")
|
||||
|
||||
# 2)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs, "Changing the project of a subtask should not change parent project")
|
||||
self.assertEqual(self.task_1.child_ids.display_project_id, self.project_goats, "Display Project of the task should be well assigned")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_goats, "Changing display project id on a subtask should change project id")
|
||||
|
||||
# 3)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
|
||||
self.assertFalse(self.task_1.child_ids.display_project_id, "Display Project of the task should be well assigned, to False")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_pigs, "Resetting display project to False on a subtask should change project id to parent project id")
|
||||
|
||||
# 4)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.project_id = self.project_goats
|
||||
|
||||
self.assertEqual(self.task_1.project_id, self.project_goats, "Parent project should change.")
|
||||
self.assertFalse(self.task_1.child_ids.display_project_id, "Display Project of the task should be False")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_goats, "Resetting display project to False on a subtask should follow project of its parent")
|
||||
|
||||
# 5)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
task_form.project_id = self.project_pigs
|
||||
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs, "Parent project should change back.")
|
||||
self.assertEqual(self.task_1.child_ids.display_project_id, self.project_goats, "Display Project of the task should be well assigned")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_goats, "Changing display project id on a subtask should change project id")
|
||||
|
||||
# Debug mode required for `parent_id` to be visible in the view
|
||||
with self.debug_mode():
|
||||
# 6)
|
||||
with Form(self.task_1.child_ids.with_context({'tracking_disable': True})) as subtask_form:
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
orphan_subtask = subtask_form.save()
|
||||
|
||||
self.assertEqual(orphan_subtask.display_project_id, self.project_goats, "Display Project of the task should be well assigned")
|
||||
self.assertEqual(orphan_subtask.project_id, self.project_goats, "Changing display project id on a subtask should change project id")
|
||||
self.assertFalse(orphan_subtask.parent_id, "Parent should be false")
|
||||
|
||||
# 7)
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
with Form(self.task_1.child_ids.with_context({'tracking_disable': True})) as subtask_form:
|
||||
subtask_form.display_project_id = self.env['project.project']
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
orphan_subtask = subtask_form.save()
|
||||
|
||||
self.assertEqual(orphan_subtask.project_id, self.project_pigs, "Removing parent should not change project")
|
||||
self.assertEqual(orphan_subtask.display_project_id, self.project_pigs, "Removing parent should make the display project set as project.")
|
||||
|
||||
def test_subtask_stage(self):
|
||||
"""
|
||||
The stage of the new child must be the default one of the project
|
||||
"""
|
||||
stage_a = self.env['project.task.type'].create({'name': 'a', 'sequence': 1})
|
||||
stage_b = self.env['project.task.type'].create({'name': 'b', 'sequence': 10})
|
||||
self.project_pigs.type_ids |= stage_a
|
||||
self.project_pigs.type_ids |= stage_b
|
||||
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should be the default one of the project.")
|
||||
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.stage_id = stage_b
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should remain the same while changing parent task stage.")
|
||||
|
||||
test_subtask_2 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 2',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.remove(test_subtask_1.id)
|
||||
task_form.child_ids.add(test_subtask_2)
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should be the default one of the project even if parent stage id is different.")
|
||||
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id.name, "New", "The stage of the child task should be the default one of the display project id, once set.")
|
||||
|
||||
def test_copy_project_with_subtasks(self):
|
||||
self.project_goats.allow_subtasks = True
|
||||
self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({'name': 'child 1', 'stage_id': self.project_goats.type_ids[0].id}),
|
||||
Command.create({'name': 'child 2', 'display_project_id': self.project_goats.id}),
|
||||
Command.create({'name': 'child 3', 'display_project_id': self.project_pigs.id}),
|
||||
Command.create({'name': 'child 4 with subtask', 'child_ids': [Command.create({'name': 'child 5'}), Command.create({'name': 'child 6 with project', 'display_project_id': self.project_goats.id})]}),
|
||||
Command.create({'name': 'child archived', 'active': False}),
|
||||
],
|
||||
'stage_id': self.project_goats.type_ids[0].id
|
||||
})
|
||||
task_count_with_subtasks_including_archived_in_project_goats = self.project_goats.with_context(active_test=False).task_count_with_subtasks
|
||||
task_count_in_project_pigs = self.project_pigs.task_count
|
||||
self.project_goats._compute_task_count() # recompute without archived tasks and subtasks
|
||||
task_count_in_project_goats = self.project_goats.task_count
|
||||
project_goats_duplicated = self.project_goats.copy()
|
||||
self.project_pigs._compute_task_count() # retrigger since a new task should be added in the project after the duplication of Project Goats
|
||||
self.assertEqual(
|
||||
project_goats_duplicated.with_context(active_test=False).task_count_with_subtasks,
|
||||
task_count_with_subtasks_including_archived_in_project_goats - 1,
|
||||
'The number of duplicated tasks (subtasks included) should be equal to the number of all tasks (with active subtasks included) of both projects, '
|
||||
'that is only the active subtasks are duplicated.')
|
||||
self.assertEqual(self.project_goats.task_count, task_count_in_project_goats, 'The number of tasks should be the same before and after the duplication of this project.')
|
||||
self.assertEqual(self.project_pigs.task_count, task_count_in_project_pigs + 1, 'The project pigs should an additional task after the duplication of the project goats.')
|
||||
self.assertEqual(project_goats_duplicated.tasks[0].child_ids[0].stage_id.id, self.project_goats.type_ids[0].id, 'The stage of subtasks should be copied too.')
|
||||
|
||||
def test_subtask_creation_with_form(self):
|
||||
"""
|
||||
1) test the creation of sub-tasks through the notebook
|
||||
2) set a parent task on an existing task
|
||||
3) test the creation of sub-sub-tasks
|
||||
4) check the correct nb of sub-tasks is displayed in the 'sub-tasks' stat button and on the parent task kanban card
|
||||
5) sub-tasks should be copied when the parent task is duplicated
|
||||
"""
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
|
||||
task_form = Form(self.task_1.with_context({'tracking_disable': True}))
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
task_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
task = task_form.save()
|
||||
|
||||
child_subtask = self.task_1.child_ids[0]
|
||||
test_subtask_2 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 2',
|
||||
})
|
||||
|
||||
with Form(child_subtask.with_context(tracking_disable=True)) as subtask_form:
|
||||
subtask_form.child_ids.add(test_subtask_2)
|
||||
subtask_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
|
||||
self.assertEqual(task.subtask_count, 2, "Parent task should have 2 children")
|
||||
task_2 = task.copy()
|
||||
self.assertEqual(task_2.subtask_count, 2, "If the parent task is duplicated then the sub task should be copied")
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged, HttpCase
|
||||
from odoo import Command
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectTags(HttpCase, TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env['project.tags'].create([
|
||||
{'name': 'Corkscrew tailed', 'project_ids': [Command.link(cls.project_pigs.id)]},
|
||||
{'name': 'Horned', 'project_ids': [Command.link(cls.project_goats.id)]},
|
||||
{
|
||||
'name': '4 Legged',
|
||||
'project_ids': [
|
||||
Command.link(cls.project_goats.id),
|
||||
Command.link(cls.project_pigs.id),
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
cls.project_pigs.write({
|
||||
'stage_id': cls.env['project.project.stage'].create({
|
||||
'name': 'pig stage',
|
||||
}).id,
|
||||
})
|
||||
cls.project_goats.write({
|
||||
'stage_id': cls.env['project.project.stage'].create({
|
||||
'name': 'goat stage',
|
||||
}).id,
|
||||
})
|
||||
|
||||
cls.env["res.config.settings"].create({'group_project_stages': True}).execute()
|
||||
|
||||
cls.env['ir.filters'].create([
|
||||
{
|
||||
'name': 'Corkscrew tail tag filter',
|
||||
'model_id': 'project.project',
|
||||
'domain': '[("tag_ids", "ilike", "Corkscrew")]',
|
||||
},
|
||||
{
|
||||
'name': 'horned tag filter',
|
||||
'model_id': 'project.project',
|
||||
'domain': '[("tag_ids", "ilike", "horned")]',
|
||||
},
|
||||
{
|
||||
'name': '4 Legged tag filter',
|
||||
'model_id': 'project.project',
|
||||
'domain': '[("tag_ids", "ilike", "4 Legged")]',
|
||||
},
|
||||
])
|
||||
|
||||
def test_01_project_tags(self):
|
||||
self.start_tour("/web", 'project_tags_filter_tour', login="admin")
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
class TestProjectTaskType(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestProjectTaskType, cls).setUpClass()
|
||||
|
||||
cls.stage_created = cls.env['project.task.type'].create({
|
||||
'name': 'Stage Already Created',
|
||||
})
|
||||
|
||||
def test_create_stage(self):
|
||||
'''
|
||||
Verify that it is not possible to add to a newly created stage a `user_id` and a `project_ids`
|
||||
'''
|
||||
with self.assertRaises(UserError):
|
||||
self.env['project.task.type'].create({
|
||||
'name': 'New Stage',
|
||||
'user_id': self.uid,
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
|
||||
def test_modify_existing_stage(self):
|
||||
'''
|
||||
- case 1: [`user_id`: not set, `project_ids`: not set] | Add `user_id` and `project_ids` => UserError
|
||||
- case 2: [`user_id`: set, `project_ids`: not set] | Add `project_ids` => UserError
|
||||
- case 3: [`user_id`: not set, `project_ids`: set] | Add `user_id` => UserError
|
||||
'''
|
||||
# case 1
|
||||
with self.assertRaises(UserError):
|
||||
self.stage_created.write({
|
||||
'user_id': self.uid,
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
|
||||
# case 2
|
||||
self.stage_created.write({
|
||||
'user_id': self.uid,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.stage_created.write({
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
|
||||
# case 3
|
||||
self.stage_created.write({
|
||||
'user_id': False,
|
||||
'project_ids': [self.project_goats.id],
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.stage_created.write({
|
||||
'user_id': self.uid,
|
||||
})
|
||||
|
||||
def test_group_by_personal_stage(self):
|
||||
"""
|
||||
Check the consistence of search_read and read_group when one groups project.tasks by personal stages.
|
||||
|
||||
Supose we have a user and his manager. Group all tasks by personal stage in the "list view".
|
||||
A `web_read_group` is performed to classify the tasks and a `web_search_read` is performed to display the lines.
|
||||
We check the consitency of both operations for tasks that are not linked to a personal stage of the current user.
|
||||
"""
|
||||
|
||||
if 'hr.employee' not in self.env:
|
||||
self.skipTest("This test requires to set a manager")
|
||||
project = self.project_goats
|
||||
user = self.user_projectmanager
|
||||
manager_user = self.env['res.users'].create({
|
||||
'name': 'Roger Employee',
|
||||
'login': 'Roger',
|
||||
'email': 'rog.projectmanager@example.com',
|
||||
'groups_id': [Command.set([self.ref('base.group_user'), self.ref('project.group_project_manager')])],
|
||||
})
|
||||
manager = self.env['hr.employee'].create({
|
||||
'user_id': manager_user.id,
|
||||
'image_1920': False,
|
||||
})
|
||||
(user | manager_user).employee_id.write({'parent_id': manager.id})
|
||||
user_personal_stages = self.env['project.task.type'].search([('user_id', '=', user.id)])
|
||||
# we create tasks for the user with different types of assignement
|
||||
self.env['project.task'].with_user(user).create([
|
||||
{
|
||||
'name': f"Task: {stage.id}",
|
||||
'project_id': project.id,
|
||||
'personal_stage_type_id': stage.id,
|
||||
'user_ids': [Command.link(user.id)],
|
||||
}
|
||||
for stage in user_personal_stages],
|
||||
)
|
||||
self.env['project.task'].with_user(user).create([
|
||||
{
|
||||
'name': f"Task: {stage.id}",
|
||||
'project_id': project.id,
|
||||
'personal_stage_type_id': stage.id,
|
||||
'user_ids': [Command.link(user.id), Command.link(manager_user.id)],
|
||||
}
|
||||
for stage in user_personal_stages],
|
||||
)
|
||||
# this task is created to create the default personal stages of manager user
|
||||
self.env['project.task'].with_user(manager_user).create({
|
||||
'name': "Manager's task",
|
||||
'project_id': project.id,
|
||||
'user_ids': [Command.link(manager_user.id)],
|
||||
})
|
||||
manager_user_personal_stages = self.env['project.task.type'].search([('user_id', '=', manager_user.id)])
|
||||
self.env['project.task'].with_user(manager_user).create([
|
||||
{
|
||||
'name': f"Task : {stage.id}",
|
||||
'project_id': project.id,
|
||||
'stage_id': stage.id,
|
||||
'user_ids': [Command.link(manager_user.id)],
|
||||
}
|
||||
for stage in manager_user_personal_stages],
|
||||
)
|
||||
|
||||
self.env.uid = user.id
|
||||
base_domain = [("user_ids.employee_parent_id.user_id", "=", manager_user.id)]
|
||||
tasks = self.env['project.task'].with_user(user.id).search(base_domain)
|
||||
tasks_with_personal_stage = tasks.filtered(lambda t: user in t.personal_stage_type_id.user_id)
|
||||
tasks_without_personal_stage = tasks - tasks_with_personal_stage
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"project_id",
|
||||
"milestone_id",
|
||||
"partner_id",
|
||||
"user_ids",
|
||||
"activity_ids",
|
||||
"stage_id",
|
||||
"personal_stage_type_ids",
|
||||
"tag_ids",
|
||||
"priority",
|
||||
"company_id",
|
||||
]
|
||||
groupby = ["personal_stage_type_ids"]
|
||||
user_read_group = self.env['project.task'].with_user(user).read_group(domain=base_domain, fields=fields, groupby=groupby)
|
||||
number_of_tasks_in_groups = sum(gr['personal_stage_type_ids_count'] if gr['personal_stage_type_ids'] and gr['personal_stage_type_ids'][0] in user_personal_stages.ids else 0 for gr in user_read_group)
|
||||
self.assertEqual(len(tasks_with_personal_stage), number_of_tasks_in_groups)
|
||||
tasks_found_for_user = [task['id'] for task in self.env['project.task'].with_user(user.id).search_read(domain=base_domain, fields=fields)]
|
||||
self.assertEqual(tasks.ids, tasks_found_for_user)
|
||||
domain = ["&", ("personal_stage_type_ids", "=", False), ("user_ids.employee_parent_id.user_id", "=", manager_user.id)]
|
||||
tasks_diplayed_without_personal_stage = [task['id'] for task in self.env['project.task'].with_user(user.id).search_read(domain=domain, fields=fields)]
|
||||
self.assertEqual(tasks_without_personal_stage.ids, tasks_diplayed_without_personal_stage)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# 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):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env['res.config.settings'].create({'group_project_milestone': True}).execute()
|
||||
|
||||
def test_01_project_tour(self):
|
||||
self.start_tour("/web", 'project_tour', login="admin")
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import users
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectUpdateAccessRights(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestProjectUpdateAccessRights, cls).setUpClass()
|
||||
cls.project_update_1 = cls.env['project.update'].create({
|
||||
'name': "Test Project Update",
|
||||
'project_id': cls.project_pigs.id,
|
||||
'status': 'on_track',
|
||||
})
|
||||
cls.project_milestone = cls.env['project.milestone'].create({
|
||||
'name': 'Test Projec Milestone',
|
||||
'project_id': cls.project_pigs.id,
|
||||
})
|
||||
cls.base_user = mail_new_test_user(cls.env, 'Base user', groups='base.group_user')
|
||||
cls.project_user = mail_new_test_user(cls.env, 'Project user', groups='project.group_project_user')
|
||||
cls.project_manager = mail_new_test_user(cls.env, 'Project admin', groups='project.group_project_manager')
|
||||
cls.portal_user = mail_new_test_user(cls.env, 'Portal user', groups='base.group_portal')
|
||||
|
||||
@users('Project user', 'Project admin', 'Base user')
|
||||
def test_project_update_user_can_read(self):
|
||||
self.project_update_1.with_user(self.env.user).name
|
||||
|
||||
@users('Base user')
|
||||
def test_project_update_user_no_write(self):
|
||||
with self.assertRaises(AccessError, msg="%s should not be able to write in the project update" % self.env.user.name):
|
||||
self.project_update_1.with_user(self.env.user).name = "Test write"
|
||||
|
||||
@users('Project admin')
|
||||
def test_project_update_admin_can_write(self):
|
||||
self.project_update_1.with_user(self.env.user).name = "Test write"
|
||||
|
||||
@users('Base user')
|
||||
def test_project_update_user_no_unlink(self):
|
||||
with self.assertRaises(AccessError, msg="%s should not be able to unlink in the project update" % self.env.user.name):
|
||||
self.project_update_1.with_user(self.env.user).unlink()
|
||||
|
||||
@users('Project admin')
|
||||
def test_project_update_admin_unlink(self):
|
||||
self.project_update_1.with_user(self.env.user).unlink()
|
||||
|
||||
@users('Portal user')
|
||||
def test_project_update_portal_user_no_read(self):
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to read in the project update"):
|
||||
self.project_update_1.with_user(self.env.user).name
|
||||
|
||||
@users('Portal user')
|
||||
def test_project_update_portal_user_no_write(self):
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to write in the project update"):
|
||||
self.project_update_1.with_user(self.env.user).name = 'Test write'
|
||||
|
||||
@users('Portal user')
|
||||
def test_project_update_portal_user_no_create(self):
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to create in the project update model"):
|
||||
self.env['project.update'].with_user(self.env.user).create({
|
||||
'name': 'Test Create with portal user',
|
||||
'project_id': self.project_pigs.id,
|
||||
'state': 'on_track',
|
||||
})
|
||||
|
||||
@users('Portal user')
|
||||
def test_project_update_portal_user_no_unlink(self):
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to unlink in the project update"):
|
||||
self.project_update_1.with_user(self.env.user).unlink()
|
||||
|
||||
@users('Portal user')
|
||||
def test_project_milestone_portal_user_no_read(self):
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to read in the project update"):
|
||||
self.project_milestone.with_user(self.env.user).name
|
||||
|
||||
@users('Portal user')
|
||||
def test_project_milestone_portal_user_no_write(self):
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to write in the project update"):
|
||||
self.project_milestone.with_user(self.env.user).name = 'Test write'
|
||||
|
||||
@users('Portal user')
|
||||
def test_project_milestone_portal_user_no_create(self):
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to create in the project update model"):
|
||||
self.env['project.update'].with_user(self.env.user).create({
|
||||
'name': 'Test Create with portal user',
|
||||
'project_id': self.project_pigs.id,
|
||||
})
|
||||
|
||||
@users('Portal user')
|
||||
def test_project_milestone_portal_user_no_unlink(self):
|
||||
with self.assertRaises(AccessError, msg=f"{self.env.user.name} should not be able to unlink in the project update"):
|
||||
self.project_milestone.with_user(self.env.user).unlink()
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectUpdate(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
|
||||
def test_project_update_form(self):
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 1"
|
||||
milestone_form.deadline = fields.Date.today()
|
||||
|
||||
try:
|
||||
with Form(self.env['project.update'].with_context({'default_project_id': self.project_pigs.id})) as update_form:
|
||||
update_form.name = "Test"
|
||||
update_form.progress = 65
|
||||
update = update_form.save()
|
||||
except Exception as e:
|
||||
raise AssertionError("Error raised unexpectedly while filling the project update form ! Exception : " + e.args[0])
|
||||
|
||||
self.assertEqual(update.user_id, self.env.user, "The author is the user who created the update.")
|
||||
self.assertNotEqual(len(update.description), 0, "The description should not be empty.")
|
||||
self.assertTrue("Activities" in update.description, "The description should contain 'Activities'.")
|
||||
self.assertEqual(update.status, 'on_track', "The status should be the default one.")
|
||||
|
||||
with Form(self.env['project.update'].with_context({'default_project_id': self.project_pigs.id})) as update_form:
|
||||
update_form.name = "Test 2"
|
||||
update = update_form.save()
|
||||
self.assertEqual(update.progress, 65, "The default progress is the one from the previous update by default")
|
||||
|
||||
def test_project_update_description(self):
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 1"
|
||||
milestone_form.deadline = fields.Date.today()
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 2"
|
||||
milestone_form.deadline = fields.Date.today()
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 3"
|
||||
milestone_form.deadline = fields.Date.today() + relativedelta(years=2)
|
||||
|
||||
template_values = self.env['project.update']._get_template_values(self.project_pigs)
|
||||
|
||||
self.assertTrue(template_values['milestones']['show_section'], 'The milestone section should not be visible since the feature is disabled')
|
||||
self.assertEqual(len(template_values['milestones']['list']), 2, "Milestone list length should be equal to 2")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 3, "Milestone created length tasks should be equal to 3")
|
||||
|
||||
self.project_pigs.write({'allow_milestones': False})
|
||||
|
||||
template_values = self.env['project.update']._get_template_values(self.project_pigs)
|
||||
|
||||
self.assertFalse(template_values['milestones']['show_section'], 'The milestone section should not be visible since the feature is disabled')
|
||||
self.assertEqual(len(template_values['milestones']['list']), 0, "Milestone list length should be equal to 0 because the Milestones feature is disabled.")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 0, "Milestone created length tasks should be equal to 0 because the Milestones feature is disabled.")
|
||||
|
||||
self.project_pigs.write({'allow_milestones': True})
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
|
||||
template_values = self.env['project.update']._get_template_values(self.project_pigs)
|
||||
|
||||
self.assertFalse(template_values['milestones']['show_section'], 'The milestone section should not be visible since the feature is disabled')
|
||||
self.assertEqual(len(template_values['milestones']['list']), 0, "Milestone list length should be equal to 0 because the Milestones feature is disabled.")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 0, "Milestone created length tasks should be equal to 0 because the Milestones feature is disabled.")
|
||||
|
||||
def test_project_update_panel(self):
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 1"
|
||||
milestone_form.deadline = fields.Date.today() + relativedelta(years=-1)
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 2"
|
||||
milestone_form.deadline = fields.Date.today() + relativedelta(years=-1)
|
||||
milestone_form.is_reached = True
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 3"
|
||||
milestone_form.deadline = fields.Date.today() + relativedelta(years=2)
|
||||
|
||||
panel_data = self.project_pigs.get_panel_data()
|
||||
|
||||
self.assertEqual(len(panel_data['milestones']['data']), 3, "Panel data should contain 'milestone' entry")
|
||||
self.assertFalse(panel_data['milestones']['data'][0]['is_deadline_exceeded'], "Milestone is achieved")
|
||||
self.assertTrue(panel_data['milestones']['data'][1]['is_deadline_exceeded'], "Milestone is exceeded")
|
||||
self.assertTrue(panel_data['milestones']['data'][0]['is_reached'], "Milestone is done")
|
||||
self.assertFalse(panel_data['milestones']['data'][1]['is_reached'], "Milestone isn't done")
|
||||
# sorting
|
||||
self.assertEqual(panel_data['milestones']['data'][0]['name'], "Test 2", "Sorting isn't correct")
|
||||
self.assertEqual(panel_data['milestones']['data'][1]['name'], "Test 1", "Sorting isn't correct")
|
||||
self.assertEqual(panel_data['milestones']['data'][2]['name'], "Test 3", "Sorting isn't correct")
|
||||
|
||||
# Disable the "Milestones" feature in the project and check the "Milestones" section is not loaded for this project.
|
||||
self.project_pigs.write({'allow_milestones': False})
|
||||
panel_data = self.project_pigs.get_panel_data()
|
||||
self.assertNotIn('milestones', panel_data, 'Since the "Milestones" feature is disabled in this project, the "Milestones" section is not loaded.')
|
||||
|
||||
# Disable globally the Milestones feature and check the Milestones section is not loaded.
|
||||
self.project_pigs.write({'allow_milestones': True})
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
panel_data = self.project_pigs.get_panel_data()
|
||||
self.assertNotIn('milestones', panel_data, 'Since the "Milestones" feature is globally disabled, the "Milestones" section is not loaded.')
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProjectUpdateUi(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Enable the "Milestones" feature to be able to create milestones on this tour.
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
|
||||
def test_01_project_tour(self):
|
||||
self.start_tour("/web", 'project_update_tour', login="admin")
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged, Form, TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestResConfigSettings(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.analytic_plan_1, cls.analytic_plan_2 = cls.env['account.analytic.plan'].create([
|
||||
{
|
||||
'name': 'Plan 1',
|
||||
'default_applicability': 'optional',
|
||||
'company_id': False,
|
||||
}, {
|
||||
'name': 'Plan 2',
|
||||
'default_applicability': 'optional',
|
||||
'company_id': False,
|
||||
},
|
||||
])
|
||||
cls.company_1, cls.company_2 = cls.env['res.company'].create([
|
||||
{'name': 'Test Company 1'},
|
||||
{'name': 'Test Company 2'},
|
||||
])
|
||||
(cls.analytic_plan_1 + cls.analytic_plan_2).write({
|
||||
'company_id': cls.company_1.id,
|
||||
})
|
||||
|
||||
def test_set_default_analytic_plan(self):
|
||||
"""
|
||||
Test that we can set the default analytic plan in the settings per company.
|
||||
When there are no analytic plans for the company, a new one named "Default" should be created.
|
||||
"""
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
settings_company_1 = self.env['res.config.settings'].with_company(self.company_1).create({})
|
||||
with Form(settings_company_1) as form:
|
||||
self.assertEqual(form.analytic_plan_id, self.analytic_plan_1)
|
||||
form.analytic_plan_id = self.analytic_plan_2
|
||||
form.save()
|
||||
self.assertEqual(settings_company_1.analytic_plan_id, self.analytic_plan_2)
|
||||
settings_company_2 = self.env['res.config.settings'].with_company(self.company_2).create({})
|
||||
with Form(settings_company_2) as form:
|
||||
self.assertNotEqual(form.analytic_plan_id, self.analytic_plan_1)
|
||||
self.assertNotEqual(form.analytic_plan_id, self.analytic_plan_2)
|
||||
self.assertEqual(form.analytic_plan_id.name, "Default")
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
from datetime import date
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestTaskDependencies(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.project_pigs.write({
|
||||
'allow_task_dependencies': True,
|
||||
})
|
||||
cls.task_3 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Pigs UserTask 2',
|
||||
'user_ids': cls.user_projectuser,
|
||||
'project_id': cls.project_pigs.id,
|
||||
})
|
||||
|
||||
def flush_tracking(self):
|
||||
""" Force the creation of tracking values. """
|
||||
self.env.flush_all()
|
||||
self.cr.precommit.run()
|
||||
|
||||
def test_task_dependencies(self):
|
||||
""" Test the task dependencies feature
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Add task2 as dependency in task1
|
||||
2) Checks if the task1 has the task in depend_on_ids field.
|
||||
"""
|
||||
self.assertEqual(len(self.task_1.depend_on_ids), 0, "The task 1 should not have any dependency.")
|
||||
self.task_1.write({
|
||||
'depend_on_ids': [Command.link(self.task_2.id)],
|
||||
})
|
||||
self.assertEqual(len(self.task_1.depend_on_ids), 1, "The task 1 should have a dependency.")
|
||||
self.task_1.write({
|
||||
'depend_on_ids': [Command.link(self.task_3.id)],
|
||||
})
|
||||
self.assertEqual(len(self.task_1.depend_on_ids), 2, "The task 1 should have two dependencies.")
|
||||
|
||||
def test_cyclic_dependencies(self):
|
||||
""" Test the cyclic dependencies
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Check initial setting on three tasks
|
||||
2) Add task2 as dependency in task1
|
||||
3) Add task3 as dependency in task2
|
||||
4) Add task1 as dependency in task3 and check a validation error is raised
|
||||
5) Add task1 as dependency in task2 and check a validation error is raised
|
||||
"""
|
||||
# 1) Check initial setting on three tasks
|
||||
self.assertTrue(
|
||||
len(self.task_1.depend_on_ids) == len(self.task_2.depend_on_ids) == len(self.task_3.depend_on_ids) == 0,
|
||||
"The three tasks should depend on no tasks.")
|
||||
self.assertTrue(self.task_1.allow_task_dependencies, 'The task dependencies feature should be enable.')
|
||||
self.assertTrue(self.task_2.allow_task_dependencies, 'The task dependencies feature should be enable.')
|
||||
self.assertTrue(self.task_3.allow_task_dependencies, 'The task dependencies feature should be enable.')
|
||||
|
||||
# 2) Add task2 as dependency in task1
|
||||
self.task_1.write({
|
||||
'depend_on_ids': [Command.link(self.task_2.id)],
|
||||
})
|
||||
self.assertEqual(len(self.task_1.depend_on_ids), 1, 'The task 1 should have one dependency.')
|
||||
|
||||
# 3) Add task3 as dependency in task2
|
||||
self.task_2.write({
|
||||
'depend_on_ids': [Command.link(self.task_3.id)],
|
||||
})
|
||||
self.assertEqual(len(self.task_2.depend_on_ids), 1, "The task 2 should have one dependency.")
|
||||
|
||||
# 4) Add task1 as dependency in task3 and check a validation error is raised
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
self.task_3.write({
|
||||
'depend_on_ids': [Command.link(self.task_1.id)],
|
||||
})
|
||||
self.assertEqual(len(self.task_3.depend_on_ids), 0, "The dependency should not be added in the task 3 because of a cyclic dependency.")
|
||||
|
||||
# 5) Add task1 as dependency in task2 and check a validation error is raised
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
self.task_2.write({
|
||||
'depend_on_ids': [Command.link(self.task_1.id)],
|
||||
})
|
||||
self.assertEqual(len(self.task_2.depend_on_ids), 1, "The number of dependencies should no change in the task 2 because of a cyclic dependency.")
|
||||
|
||||
def test_tracking_dependencies(self):
|
||||
# Enable the company setting
|
||||
self.env['res.config.settings'].create({
|
||||
'group_project_task_dependencies': True
|
||||
}).execute()
|
||||
# `depend_on_ids` is tracked
|
||||
self.task_1.with_context(mail_notrack=True).write({
|
||||
'depend_on_ids': [Command.link(self.task_2.id)]
|
||||
})
|
||||
self.cr.precommit.clear()
|
||||
# Check that changing a dependency tracked field in task_2 logs a message in task_1.
|
||||
self.task_2.write({'date_deadline': date(1983, 3, 1)}) # + 1 message in task_1 and task_2
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 1,
|
||||
'Changing the deadline on task 2 should have logged a message in task 1.')
|
||||
|
||||
# Check that changing a dependency tracked field in task_1 does not log a message in task_2.
|
||||
self.task_1.date_deadline = date(2020, 1, 2) # + 1 message in task_1
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_2.message_ids), 1,
|
||||
'Changing the deadline on task 1 should not have logged a message in task 2.')
|
||||
|
||||
# Check that changing a field that is not tracked at all on task 2 does not impact task 1.
|
||||
self.task_2.color = 100 # no new message
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 2,
|
||||
'Changing the color on task 2 should not have logged a message in task 1 since it is not tracked.')
|
||||
|
||||
# Check that changing multiple fields does not log more than one message.
|
||||
self.task_2.write({
|
||||
'date_deadline': date(2020, 1, 1),
|
||||
'kanban_state': 'blocked',
|
||||
}) # + 1 message in task_1 and task_2
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 3,
|
||||
'Changing multiple fields on task 2 should only log one message in task 1.')
|
||||
|
||||
def test_task_dependencies_settings_change(self):
|
||||
|
||||
def set_task_dependencies_setting(enabled):
|
||||
features_config = self.env["res.config.settings"].create({'group_project_task_dependencies': enabled})
|
||||
features_config.execute()
|
||||
|
||||
self.project_pigs.write({
|
||||
'allow_task_dependencies': False,
|
||||
})
|
||||
|
||||
# As the Project General Setting group_project_task_dependencies needs to be toggled in order
|
||||
# to be applied on the existing projects we need to force it so that it does not depends on anything
|
||||
# (like demo data for instance)
|
||||
set_task_dependencies_setting(False)
|
||||
set_task_dependencies_setting(True)
|
||||
self.assertTrue(self.project_pigs.allow_task_dependencies, "Projects allow_task_dependencies should follow group_project_task_dependencies setting changes")
|
||||
|
||||
self.project_chickens = self.env['project.project'].create({
|
||||
'name': 'My Chicken Project'
|
||||
})
|
||||
self.assertTrue(self.project_chickens.allow_task_dependencies, "New Projects allow_task_dependencies should default to group_project_task_dependencies")
|
||||
|
||||
set_task_dependencies_setting(False)
|
||||
self.assertFalse(self.project_pigs.allow_task_dependencies, "Projects allow_task_dependencies should follow group_project_task_dependencies setting changes")
|
||||
|
||||
self.project_ducks = self.env['project.project'].create({
|
||||
'name': 'My Ducks Project'
|
||||
})
|
||||
self.assertFalse(self.project_ducks.allow_task_dependencies, "New Projects allow_task_dependencies should default to group_project_task_dependencies")
|
||||
|
||||
def test_duplicate_project_with_task_dependencies(self):
|
||||
self.project_pigs.allow_task_dependencies = True
|
||||
self.task_1.depend_on_ids = self.task_2
|
||||
pigs_copy = self.project_pigs.copy()
|
||||
|
||||
task1_copy = pigs_copy.task_ids.filtered(lambda t: t.name == 'Pigs UserTask')
|
||||
task2_copy = pigs_copy.task_ids.filtered(lambda t: t.name == 'Pigs ManagerTask')
|
||||
|
||||
self.assertEqual(len(task1_copy), 1, "Should only contain 1 copy of UserTask")
|
||||
self.assertEqual(len(task2_copy), 1, "Should only contain 1 copy of ManagerTask")
|
||||
|
||||
self.assertEqual(task1_copy.depend_on_ids.ids, [task2_copy.id],
|
||||
"Copy should only create a relation between both copy if they are both part of the project")
|
||||
|
||||
task1_copy.depend_on_ids = self.task_1
|
||||
|
||||
pigs_copy_copy = pigs_copy.copy()
|
||||
task1_copy_copy = pigs_copy_copy.task_ids.filtered(lambda t: t.name == 'Pigs UserTask')
|
||||
|
||||
self.assertEqual(task1_copy_copy.depend_on_ids.ids, [self.task_1.id],
|
||||
"Copy should not alter the relation if the other task is in a different project")
|
||||
|
||||
def test_duplicate_project_with_subtask_dependencies(self):
|
||||
self.project_goats.allow_task_dependencies = True
|
||||
self.project_goats.allow_subtasks = True
|
||||
parent_task = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({'name': 'Node 1'}),
|
||||
Command.create({'name': 'SuperNode 2', 'child_ids': [Command.create({'name': 'Node 2'})]}),
|
||||
Command.create({'name': 'Node 3'}),
|
||||
],
|
||||
})
|
||||
|
||||
node1 = parent_task.child_ids[0]
|
||||
node2 = parent_task.child_ids[1].child_ids
|
||||
node3 = parent_task.child_ids[2]
|
||||
|
||||
node1.dependent_ids = node2
|
||||
node2.dependent_ids = node3
|
||||
|
||||
# Test copying the whole Node tree
|
||||
parent_task_copy = parent_task.copy()
|
||||
parent_copy_node1 = parent_task_copy.child_ids[0]
|
||||
parent_copy_node2 = parent_task_copy.child_ids[1].child_ids
|
||||
parent_copy_node3 = parent_task_copy.child_ids[2]
|
||||
|
||||
# Relation should only be copied between the newly created node
|
||||
self.assertEqual(len(parent_copy_node1.dependent_ids), 1)
|
||||
self.assertEqual(parent_copy_node1.dependent_ids.ids, parent_copy_node2.ids, 'Node1copy - Node2copy relation should be present')
|
||||
self.assertEqual(len(parent_copy_node2.dependent_ids), 1)
|
||||
self.assertEqual(parent_copy_node2.dependent_ids.ids, parent_copy_node3.ids, 'Node2copy - Node3copy relation should be present')
|
||||
|
||||
# Original Node should not have new relation
|
||||
self.assertEqual(len(node1.dependent_ids), 1)
|
||||
self.assertEqual(node1.dependent_ids.ids, node2.ids, 'Only Node1 - Node2 relation should be present')
|
||||
self.assertEqual(len(node2.dependent_ids), 1)
|
||||
self.assertEqual(node2.dependent_ids.ids, node3.ids, 'Only Node2 - Node3 relation should be present')
|
||||
|
||||
# Test copying Node inside the chain
|
||||
single_copy_node2 = node2.copy()
|
||||
|
||||
# Relation should be present between the other original node and the newly copied node
|
||||
self.assertEqual(len(single_copy_node2.depend_on_ids), 1)
|
||||
self.assertEqual(single_copy_node2.depend_on_ids.ids, node1.ids, 'Node1 - Node2copy relation should be present')
|
||||
self.assertEqual(len(single_copy_node2.dependent_ids), 1)
|
||||
self.assertEqual(single_copy_node2.dependent_ids.ids, node3.ids, 'Node2copy - Node3 relation should be present')
|
||||
|
||||
# Original Node should have new relations
|
||||
self.assertEqual(len(node1.dependent_ids), 2)
|
||||
self.assertEqual(len(node3.depend_on_ids), 2)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
class TestTaskFollow(TestProjectCommon):
|
||||
|
||||
def test_follow_on_create(self):
|
||||
# Tests that the user is follower of the task upon creation
|
||||
self.assertTrue(self.user_projectuser.partner_id in self.task_1.message_partner_ids)
|
||||
|
||||
def test_follow_on_write(self):
|
||||
# Tests that the user is follower of the task upon writing new assignees
|
||||
self.task_2.user_ids += self.user_projectmanager
|
||||
self.assertTrue(self.user_projectmanager.partner_id in self.task_2.message_partner_ids)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestTaskTracking(TestProjectCommon):
|
||||
|
||||
def flush_tracking(self):
|
||||
""" Force the creation of tracking values. """
|
||||
self.env.flush_all()
|
||||
self.cr.precommit.run()
|
||||
|
||||
def test_many2many_tracking(self):
|
||||
# Basic test
|
||||
# Assign new user
|
||||
self.cr.precommit.clear()
|
||||
self.task_1.user_ids += self.user_projectmanager
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 1,
|
||||
"Assigning a new user should log a message.")
|
||||
# No change
|
||||
self.task_1.user_ids += self.user_projectmanager
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 1,
|
||||
"Assigning an already assigned user should not log a message.")
|
||||
# Removing assigness
|
||||
self.task_1.user_ids = False
|
||||
self.flush_tracking()
|
||||
self.assertEqual(len(self.task_1.message_ids), 2,
|
||||
"Removing both assignees should only log one message.")
|
||||
|
||||
def test_many2many_tracking_context(self):
|
||||
# Test that the many2many tracking does not throw an error when using
|
||||
# default values for fields that exists both on tasks and on messages
|
||||
# Using an invalid value for this test
|
||||
self.task_1.with_context(default_parent_id=-1).user_ids += self.user_projectmanager
|
||||
Loading…
Add table
Add a link
Reference in a new issue