Files
odoo_source/addons/project/tests/test_multicompany.py
T
c5dd03ffee [FIX] project: review UX and UI in Project app
This commit reviews the UX and UI in Project app and improves the usability of the
new features recently added (subtasks list in kanban card, state field replacing
kanban_state field, etc).

The following changes are made in this commit:

- swap of user_id icon in the kanban box and the removal of the allow_unassign field and setting
- change the ordering of the fields in the subtask list
- make small layout changes in the project.task and project.project kanban cards
- remove the "lock-icon private" sub-title of private tasks, replace it with a
  little lock icon on the bottom right icons of the kanban card.
- remove the break tag in the project.task kanban card that was unecessary
  given the new display and margin style settings
- lock icon is bigger
- kanban icons are better aligned
- state is as big as avatar
- remove the 'remaining hours on SO' field
- deadline field will be optional and hidden by default in project.task list view
- remove the rating field
- add stage field as optional in project.task list views
- priority and state fields will no longer be optional
- stage_id will be copied when duplicating a task, except when the task is
  generated through the recurrence
- remove the tooltip of the tag_ids field
- When duplicating a task having sub-tasks, '(copy)' is no longer
  added to the name of the sub-tasks of this task.
- project.task kanban view:
  * (+ x tasks) mention next to the name is removed
  * The caret is replaced with 'fa-check-square-o x/y'
    which will represent the number of sub-tasks closed
    compared to the total number of sub-tasks
  * Only open subtasks are displayed
  * When changing the state of a sub-task to a closing one,
    the sub-task is muted and removed from the list on the view reload
  * The name of the parent task on the kanban card of sub-tasks is
    displayed except when viewing the sub-tasks of a particular task
    through the sub-tasks stat button
  * project.project kanban view: the fa-check-square-o icon of
    milestones is replaced with fa-flag-o
  * project.task kanban card: the fa-play and fa-pause icons
    are moved on the right of the remaining hours widge.
  * Allow users to edit the stage_id in batch from the list view of tasks
    if all of the selected tasks are part of the same project.
  * the state will have the same size as the avatar
  * change the opacity of the tasks that are closed
  * state is at the right of subtask list
  * the striked should be replaced by the opacity on the kanban card

Enterprise PR: odoo/enterprise#38132

Task-3229873

closes odoo/odoo#116628

X-original-commit: 09b5d5843096d27b5a4ab0f603ad44bdb2e74723
Related: odoo/upgrade#4478
Related: odoo/enterprise#38771
Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
Co-authored-by: Panagiotis Kyriakou <paky@odoo.com>
Co-authored-by: Bastien (bvdn) <bvdn@odoo.com>
2023-04-03 17:07:21 +02:00

306 lines
13 KiB
Python

# -*- 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
# <field name="parent_id" groups="base.group_no_one"/>
view = self.env.ref('project.view_task_form2').sudo()
tree = etree.fromstring(view.arch)
for node in tree.xpath('//field[@name="parent_id"][@attrs]'):
node.attrib.pop('attrs')
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.assertFalse(task.project_id, "The subtask should not have any project set")
self.assertEqual(task.company_id, self.task_1.company_id, "The company of the subtask should be the one from its parent when no project is set.")
def test_cross_subtask_project(self):
# For `parent_id` to be visible in the view, you need
# 1. The debug mode
# <field name="parent_id" groups="base.group_no_one"/>
view = self.env.ref('project.view_task_form2').sudo()
tree = etree.fromstring(view.arch)
for node in tree.xpath('//field[@name="parent_id"][@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.assertFalse(task.project_id, "No project should be set on the subtask by default")
self.assertEqual(task.company_id, task.parent_id.company_id, "The company of the subtask should be the one from its parent.")
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"