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>
306 lines
13 KiB
Python
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"
|