19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

View file

@ -2,4 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_timesheet
from . import test_project_task_quick_create
from . import test_portal_timesheet
from . import test_project_project
from . import test_project_template
from . import test_employee_delete_wizard
from . import test_timesheet_import_template

View file

@ -0,0 +1,26 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr.tests.common import TestHrCommon
class TestEmployeeDeleteWizard(TestHrCommon):
def setUp(self):
super().setUp()
def test_delete_wizard_single_employee_with_timesheet(self):
""" Test the deletion wizard in the case of a single employee """
employee_A = self.env['hr.employee'].create([
{
'name': 'Employee A',
'user_id': False,
'work_email': 'employee_A@example.com',
}
])
delete_wizard = self.env['hr.employee.delete.wizard'].create({
'employee_ids': [employee_A.id],
})
returned_action_Record = delete_wizard.action_archive()
self.assertEqual(returned_action_Record['context']['active_ids'], [employee_A.id], "Employee should have been selected")
self.assertEqual(returned_action_Record['context']['employee_termination'], True, "Employee Termination should have been set")

View file

@ -18,6 +18,96 @@ class TestPortalTimesheet(TestProjectSharingCommon):
Command.create({'partner_id': self.user_portal.partner_id.id}),
],
})
for view in ['form', 'tree']:
for view in ['form', 'list']:
# Ensure that uom.uom records are not present in cache
self.env.invalidate_all()
# Should not raise any access error
self.env['account.analytic.line'].with_user(self.user_portal).get_view(view_type=view)
def test_action_view_subtask_timesheet(self):
""" Ensure that the action view_subtask_timesheet is accessible without
raising an error for all portal users
"""
# A portal collaborator is added to a project to enable the rule analytic.account.analytic.line.timesheet.portal.user
self.project_portal.write({
'collaborator_ids': [
Command.create({'partner_id': self.user_portal.partner_id.id}),
],
})
action = self.task_portal.action_view_subtask_timesheet()
tree_view_id = form_view_id = kanban_view_id = False
for view_id, view_type in action['views']:
if view_type == 'list':
tree_view_id = view_id
elif view_type == 'form':
form_view_id = view_id
elif view_type == 'kanban':
kanban_view_id = view_id
action = self.task_portal.with_user(self.user_portal).action_view_subtask_timesheet()
portal_tree_view_id = self.env['ir.model.data']._xmlid_to_res_id('hr_timesheet.hr_timesheet_line_portal_tree')
portal_form_view_id = self.env['ir.model.data']._xmlid_to_res_id('hr_timesheet.timesheet_view_form_portal_user')
portal_kanban_view_id = self.env['ir.model.data']._xmlid_to_res_id('hr_timesheet.view_kanban_account_analytic_line_portal_user')
if portal_tree_view_id and portal_form_view_id and portal_kanban_view_id:
# no need to check that if the views are not installed or already removed
for view_id, view_type in action['views']:
if view_type == 'list':
self.assertEqual(view_id, portal_tree_view_id)
elif view_type == 'form':
self.assertEqual(view_id, portal_form_view_id)
elif view_type == 'kanban':
self.assertEqual(view_id, portal_kanban_view_id)
self.env['ir.ui.view'].browse([portal_tree_view_id, portal_form_view_id, portal_kanban_view_id]).unlink()
action = self.task_portal.with_user(self.user_portal).action_view_subtask_timesheet()
for view_id, view_type in action['views']:
if view_type == 'list':
self.assertEqual(view_id, tree_view_id)
elif view_type == 'form':
self.assertEqual(view_id, form_view_id)
elif view_type == 'kanban':
self.assertEqual(view_id, kanban_view_id)
def test_timesheet_visibility_portal(self):
"""
Steps:
1. Retrieve the domain that determines timesheet visibility for the portal user.
2. Create an employee linked to the project user.
3. Create a timesheet entry associated with a specific project and task.
4. Assign the portal user as the partner on the task.
5. Search for timesheets using the retrieved domain.
6. Verify that the created timesheet is visible to the portal user.
7. Remove the portal user as the partner of the task.
8. Search for timesheets again using the same domain.
9. Verify that the timesheet is no longer visible to the portal user.
10. Assign the portal user as the partner of the project.
11. Search for timesheets again using the same domain.
12. Verify that the timesheet is now visible to the portal user.
"""
AnalyticLineModel = self.env['account.analytic.line']
timesheet_domain = AnalyticLineModel.with_user(self.user_portal)._timesheet_get_portal_domain()
employee = self.env['hr.employee'].create({
'name': 'Project User Employee',
'user_id': self.user_projectuser.id,
})
timesheet_entry = AnalyticLineModel.create({
'name': 'Timesheet',
'project_id': self.project_cows.id,
'task_id': self.task_cow.id,
'employee_id': employee.id,
})
self.task_cow.write({'partner_id': self.user_portal.partner_id.id})
timesheets = AnalyticLineModel.search(timesheet_domain)
self.assertIn(timesheet_entry.id, timesheets.ids, "Portal user should see the timesheet when set as the partner on the task.")
self.task_cow.write({'partner_id': False})
timesheets = AnalyticLineModel.search(timesheet_domain)
self.assertNotIn(timesheet_entry.id, timesheets.ids, "Portal user should not see the timesheet when not assigned as the task's partner.")
self.project_cows.write({'partner_id': self.user_portal.partner_id.id})
timesheets = AnalyticLineModel.search(timesheet_domain)
self.assertIn(timesheet_entry.id, timesheets.ids, "Portal user should see the timesheet when set as the projects partner.")

View file

@ -0,0 +1,93 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestProjectProject(TransactionCase):
def test_create_projects(self):
"""
Test creating some projects and check analytic account generated
Test case:
=========
- Create 4 projects and 2 of them should have timesheets feature enabled
- Check the projects with timesheets enabled have an analytic account generated
"""
project1, project2, project3, project4 = self.env['project.project'].create([{
'name': 'Project 1 (no timesheets)',
'allow_timesheets': False,
}, {
'name': 'Project 2 (timesheets)',
'allow_timesheets': True,
}, {
'name': 'Project 3 (no timesheets)',
'allow_timesheets': False,
}, {
'name': 'Project 4 (timesheets)',
'allow_timesheets': True,
}])
self.assertFalse(project1.account_id, 'Project 1 should not have an analytic account')
self.assertTrue(project2.account_id, 'Project 2 should have an analytic account')
self.assertEqual(
project2.name,
project2.account_id.name,
'Project 2 should have the same name as its analytic account'
)
self.assertFalse(project3.account_id, 'Project 3 should not have an analytic account')
self.assertTrue(project4.account_id, 'Project 4 should have an analytic account')
self.assertEqual(
project4.name,
project4.account_id.name,
'Project 4 should have the same name as its analytic account'
)
def test_create_projects_with_default_analytic_account(self):
project_plan_id = int(self.env['ir.config_parameter'].sudo().get_param('analytic.analytic_plan_projects'))
if not project_plan_id:
project_plan, _other_plans = self.env['account.analytic.plan']._get_all_plans()
project_plan_id = project_plan.id
analytic_account = self.env['account.analytic.account'].create({'name': 'Test Analytic Account', 'plan_id': project_plan_id})
project1, project2 = self.env['project.project'].with_context(default_account_id=analytic_account.id).create([{
'name': 'Project 1 (no timesheets)',
'allow_timesheets': False,
'account_id': analytic_account.id,
}, {
'name': 'Project 2 (timesheets)',
'allow_timesheets': True,
'account_id': analytic_account.id,
}])
self.assertEqual(project1.account_id, analytic_account, 'Project 1 should have the default analytic account')
self.assertEqual(project2.account_id, analytic_account, 'Project 2 should have the default analytic account')
def test_compute_total_timesheet_time(self):
employee = self.env['hr.employee'].create({
'name': 'Test Employee',
})
project = self.env['project.project'].create({
'name': 'Test Project',
'allocated_hours': 1.0,
})
self.env['project.task'].create({
'name': 'Test Task',
'project_id': project.id,
'timesheet_ids': [
Command.create({
'name': '/',
'employee_id': employee.id,
'unit_amount': 0.5,
}),
Command.create({
'name': '/',
'employee_id': employee.id,
'unit_amount': 0.25,
}),
],
})
self.assertEqual(project.total_timesheet_time, 0.75, 'The total timesheet time should be 0.75 (00:45) hours.')

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr_timesheet.tests.test_timesheet import TestCommonTimesheet
from odoo.tests import Form, tagged
@tagged('-at_install', 'post_install')
class TestProjectTaskQuickCreate(TestCommonTimesheet):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.project_customer.write({'allow_timesheets': True})
def test_create_task_with_valid_expressions(self):
# tuple format = (display name, [expected name, expected tags count, expected users count, expected priority, expected planned hours])
valid_expressions = {
'task A 30H 2.5h #tag1 @user_employee2 2H #tag2 @user_employee 5h !': ('task A', 2, 2, "1", 39.5),
'task A 30.H 2.h 1H #tag2 ! @user_employee ! @user_employee2 2.13h !': ('task A 30.H 2.h', 1, 2, "1", 3.13),
}
for expression, values in valid_expressions.items():
task_form = Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_customer.id}), view="project.quick_create_task_form")
task_form.display_name = expression
task = task_form.save()
results = (task.name, len(task.tag_ids), len(task.user_ids), task.priority, task.allocated_hours)
self.assertEqual(results, values)
def test_create_task_with_invalid_expressions(self):
invalid_expressions = (
'30H #tag1 @raouf1 @raouf2 !',
'30h #tag1 @raouf1 @raouf2 !',
)
for expression in invalid_expressions:
task_form = Form(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_customer.id}), view="project.quick_create_task_form")
task_form.display_name = expression
task = task_form.save()
results = (task.name, len(task.tag_ids), len(task.user_ids), task.priority, task.allocated_hours)
self.assertEqual(results, (expression, 0, 0, '0', 0))

View file

@ -0,0 +1,56 @@
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestProjectProjectTemplate(TransactionCase):
def test_template_created_not_having_analytic_account(self):
template_project, normal_project = self.env["project.project"].create(
[
{
"name": "Template Project",
"is_template": True,
"allow_timesheets": True,
},
{
"name": "Normal Project",
"is_template": False,
"allow_timesheets": True,
},
]
)
self.assertFalse(template_project.account_id, "The template project shouldn't have analytic account")
self.assertTrue(normal_project.account_id, "A normal project should have a analytic account")
def test_project_created_from_template_to_have_analytic_account(self):
template_project_timesheet, template_project_no_timesheet = self.env['project.project'].create([
{
"name": "Template Project Timesheet",
"is_template": True,
"allow_timesheets": True,
},
{
"name": "Template Project No Timesheet",
"is_template": True,
"allow_timesheets": False,
},
])
new_project_1 = template_project_timesheet.action_create_from_template()
self.assertTrue(new_project_1.account_id, "A project created from template allowing timesheet should have an analytic account")
new_project_2 = template_project_no_timesheet.action_create_from_template()
self.assertFalse(new_project_2.account_id, "A project created from template disabling timesheet should not have an analytic account")
def test_convert_project_template_into_regular_project_analytics(self):
template_project = self.env["project.project"].create(
{
"name": "Template Project Timesheet",
"is_template": True,
"allow_timesheets": True,
}
)
self.assertFalse(template_project.account_id, "The template project shouldn't have analytic account before conversion")
template_project.action_undo_convert_to_template()
self.assertFalse(template_project.is_template, "The project should not be a template anymore after conversion")
self.assertTrue(template_project.account_id, "Converting a template project with timesheets enabled into a regular project should create an analytic account")

View file

@ -2,10 +2,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
from unittest.mock import patch
from odoo import fields
from odoo.fields import Command
from odoo.tests.common import TransactionCase, Form
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests import Form, TransactionCase, new_test_user
from odoo.exceptions import AccessError, RedirectWarning, UserError, ValidationError
class TestCommonTimesheet(TransactionCase):
@ -13,8 +15,7 @@ class TestCommonTimesheet(TransactionCase):
@classmethod
def setUpClass(cls):
super(TestCommonTimesheet, cls).setUpClass()
cls.env.user.tz = "Europe/Brussels"
cls.env.company.resource_calendar_id.tz = "Europe/Brussels"
# Crappy hack to disable the rule from timesheet grid, if it exists
# The registry doesn't contain the field timesheet_manager_id.
# but there is an ir.rule about it, crashing during its evaluation
@ -30,8 +31,7 @@ class TestCommonTimesheet(TransactionCase):
})
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan Test',
'company_id': False,
'name': 'Timesheet Plan Test',
})
cls.analytic_account = cls.env['account.analytic.account'].create({
'name': 'Analytic Account for Test Customer',
@ -45,19 +45,19 @@ class TestCommonTimesheet(TransactionCase):
'name': 'Project X',
'allow_timesheets': True,
'partner_id': cls.partner.id,
'analytic_account_id': cls.analytic_account.id,
'account_id': cls.analytic_account.id,
})
cls.task1 = cls.env['project.task'].create({
'name': 'Task One',
'priority': '0',
'kanban_state': 'normal',
'state': '01_in_progress',
'project_id': cls.project_customer.id,
'partner_id': cls.partner.id,
})
cls.task2 = cls.env['project.task'].create({
'name': 'Task Two',
'priority': '1',
'kanban_state': 'done',
'state': '1_done',
'project_id': cls.project_customer.id,
})
# users
@ -65,52 +65,62 @@ class TestCommonTimesheet(TransactionCase):
'name': 'User Employee',
'login': 'user_employee',
'email': 'useremployee@test.com',
'groups_id': [(6, 0, [cls.env.ref('hr_timesheet.group_hr_timesheet_user').id])],
'group_ids': [(6, 0, [cls.env.ref('hr_timesheet.group_hr_timesheet_user').id])],
})
cls.user_employee2 = cls.env['res.users'].create({
'name': 'User Employee 2',
'login': 'user_employee2',
'email': 'useremployee2@test.com',
'groups_id': [(6, 0, [cls.env.ref('hr_timesheet.group_hr_timesheet_user').id])],
'group_ids': [(6, 0, [cls.env.ref('hr_timesheet.group_hr_timesheet_user').id])],
})
cls.user_manager = cls.env['res.users'].create({
'name': 'User Officer',
'login': 'user_manager',
'email': 'usermanager@test.com',
'groups_id': [(6, 0, [cls.env.ref('hr_timesheet.group_timesheet_manager').id])],
'group_ids': [(6, 0, [cls.env.ref('hr_timesheet.group_timesheet_manager').id])],
})
# employees
cls.empl_employee = cls.env['hr.employee'].create({
'name': 'User Empl Employee',
'user_id': cls.user_employee.id,
'employee_type': 'freelance', # Avoid searching the contract if hr_contract module is installed before this module.
})
cls.empl_employee2 = cls.env['hr.employee'].create({
'name': 'User Empl Employee 2',
'user_id': cls.user_employee2.id,
'employee_type': 'freelance',
})
cls.empl_manager = cls.env['hr.employee'].create({
'name': 'User Empl Officer',
'user_id': cls.user_manager.id,
'employee_type': 'freelance',
})
def assert_get_view_timesheet_encode_uom(self, expected):
companies = self.env['res.company'].create([
{'name': 'foo', 'timesheet_encode_uom_id': self.env.ref('uom.product_uom_hour').id},
{'name': 'bar', 'timesheet_encode_uom_id': self.env.ref('uom.product_uom_day').id},
])
for view_xml_id, xpath_expr, expected_labels in expected:
for company, expected_label in zip(companies, expected_labels):
view = self.env.ref(view_xml_id)
view = self.env[view.model].with_company(company).get_view(view.id, view.type)
tree = etree.fromstring(view['arch'])
field_node = tree.xpath(xpath_expr)[0]
self.assertEqual(field_node.get('string'), expected_label)
cls.project = cls.env['project.project'].create({
'name': 'Test Project',
'privacy_visibility': 'followers',
'task_ids': [Command.create({
'name': 'Test Task',
})],
})
cls.timesheet = cls.env['account.analytic.line'].create({
'name': 'Test Timesheet',
'project_id': cls.project.id,
'task_id': cls.project.task_ids[0].id,
'employee_id': cls.empl_employee.id,
})
cls.timesheet_manager_no_project_user = new_test_user(
cls.env,
login='no_project_user',
groups='hr_timesheet.group_timesheet_manager'
)
class TestTimesheet(TestCommonTimesheet):
def setUp(self):
super(TestTimesheet, self).setUp()
super().setUp()
# Make sure to clean the plan fields
self.env.registry._setup_models__(self.env.cr)
# Crappy hack to disable the rule from timesheet grid, if it exists
# The registry doesn't contain the field timesheet_manager_id.
@ -120,9 +130,9 @@ class TestTimesheet(TestCommonTimesheet):
rule.active = False
def test_log_timesheet(self):
""" Test when log timesheet : check analytic account, user and employee are correctly set. """
""" Test when log timesheet: check analytic account, user and employee are correctly set. """
Timesheet = self.env['account.analytic.line']
timesheet_uom = self.project_customer.analytic_account_id.company_id.project_time_mode_id
timesheet_uom = self.project_customer.account_id.company_id.project_time_mode_id
# employee 1 log some timesheet on task 1
timesheet1 = Timesheet.with_user(self.user_employee).create({
'project_id': self.project_customer.id,
@ -130,7 +140,7 @@ class TestTimesheet(TestCommonTimesheet):
'name': 'my first timesheet',
'unit_amount': 4,
})
self.assertEqual(timesheet1.account_id, self.project_customer.analytic_account_id, 'Analytic account should be the same as the project')
self.assertEqual(timesheet1.account_id, self.project_customer.account_id, 'Analytic account should be the same as the project')
self.assertEqual(timesheet1.employee_id, self.empl_employee, 'Employee should be the one of the current user')
self.assertEqual(timesheet1.partner_id, self.task1.partner_id, 'Customer of task should be the same of the one set on new timesheet')
self.assertEqual(timesheet1.product_uom_id, timesheet_uom, "The UoM of the timesheet should be the one set on the company of the analytic account.")
@ -165,7 +175,7 @@ class TestTimesheet(TestCommonTimesheet):
self.assertEqual(timesheet4.partner_id, self.project_customer.partner_id, 'Customer of new timesheet should be the same of the one set project (since no task on timesheet)')
def test_log_access_rights(self):
""" Test access rights : user can update its own timesheets only, and manager can change all """
""" Test access rights: user can update its own timesheets only, and manager can change all """
# employee 1 log some timesheet on task 1
Timesheet = self.env['account.analytic.line']
timesheet1 = Timesheet.with_user(self.user_employee).create({
@ -195,7 +205,7 @@ class TestTimesheet(TestCommonTimesheet):
'allow_timesheets': False,
'partner_id': self.partner.id,
})
self.assertFalse(non_tracked_project.analytic_account_id, "A non time-tracked project shouldn't generate an analytic account")
self.assertFalse(non_tracked_project.account_id, "A non time-tracked project shouldn't generate an analytic account")
# create a project tracking time
tracked_project = self.env['project.project'].create({
@ -203,21 +213,21 @@ class TestTimesheet(TestCommonTimesheet):
'allow_timesheets': True,
'partner_id': self.partner.id,
})
self.assertTrue(tracked_project.analytic_account_id, "A time-tracked project should generate an analytic account")
self.assertTrue(tracked_project.analytic_account_id.active, "A time-tracked project should generate an active analytic account")
self.assertEqual(tracked_project.partner_id, tracked_project.analytic_account_id.partner_id, "The generated AA should have the same partner as the project")
self.assertEqual(tracked_project.name, tracked_project.analytic_account_id.name, "The generated AA should have the same name as the project")
self.assertEqual(tracked_project.analytic_account_id.project_count, 1, "The generated AA should be linked to the project")
self.assertTrue(tracked_project.account_id, "A time-tracked project should generate an analytic account")
self.assertTrue(tracked_project.account_id.active, "A time-tracked project should generate an active analytic account")
self.assertEqual(tracked_project.partner_id, tracked_project.account_id.partner_id, "The generated AA should have the same partner as the project")
self.assertEqual(tracked_project.name, tracked_project.account_id.name, "The generated AA should have the same name as the project")
self.assertEqual(tracked_project.account_id.project_count, 1, "The generated AA should be linked to the project")
# create a project without tracking time, but with analytic account
analytic_project = self.env['project.project'].create({
'name': 'Project without timesheet but with AA',
'allow_timesheets': True,
'partner_id': self.partner.id,
'analytic_account_id': tracked_project.analytic_account_id.id,
'account_id': tracked_project.account_id.id,
})
self.assertNotEqual(analytic_project.name, tracked_project.analytic_account_id.name, "The name of the associated AA can be different from the project")
self.assertEqual(tracked_project.analytic_account_id.project_count, 2, "The AA should be linked to 2 project")
self.assertNotEqual(analytic_project.name, tracked_project.account_id.name, "The name of the associated AA can be different from the project")
self.assertEqual(tracked_project.account_id.project_count, 2, "The AA should be linked to 2 project")
# analytic linked to projects containing tasks can not be removed
task = self.env['project.task'].create({
@ -225,25 +235,26 @@ class TestTimesheet(TestCommonTimesheet):
'project_id': tracked_project.id,
})
with self.assertRaises(UserError):
tracked_project.analytic_account_id.unlink()
tracked_project.account_id.unlink()
# task can be removed, as there is no timesheet
task.unlink()
# since both projects linked to the same analytic account are empty (no task), it can be removed
tracked_project.analytic_account_id.unlink()
tracked_project.account_id.unlink()
def test_transfert_project(self):
""" Transfert task with timesheet to another project. """
Timesheet = self.env['account.analytic.line']
Task = self.env['project.task'].with_context(default_project_id=self.task1.project_id.id)
# create nested subtasks
task_child = self.env['project.task'].create({
task_child = Task.create({
'name': 'Task Child',
'parent_id': self.task1.id,
})
task_grandchild = self.env['project.task'].create({
task_grandchild = Task.create({
'name': 'Task Grandchild',
'parent_id': task_child.id,
})
@ -288,18 +299,28 @@ class TestTimesheet(TestCommonTimesheet):
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_customer.id)])
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_customer2.id)])
self.assertEqual(timesheet_count1, 0, "There are still timesheets linked to Project1")
self.assertEqual(timesheet_count1, 0, "No timesheets should be linked to Project1")
self.assertEqual(timesheet_count2, 3, "3 timesheets should be linked to Project2")
self.assertEqual(len(self.task1.timesheet_ids), 1, "The timesheet still should be linked to task1")
self.assertEqual(len(task_child.timesheet_ids), 1, "The timesheet still should be linked to task_child")
self.assertEqual(len(task_grandchild.timesheet_ids), 1, "The timesheet still should be linked to task_grandchild")
# it is forbidden to set a task with timesheet without project
# It is forbidden to unset the project of a task with timesheet
with self.assertRaises(UserError):
self.task1.write({
'project_id': False
})
def test_favorite_project_id(self):
""" Test that user without previous timesheets and without
access to the internal project has no favorite project. """
# make internal project accessible to invited internal users only
self.env.company.internal_project_id.privacy_visibility = 'followers'
favorite_project = self.env['account.analytic.line'].with_user(self.user_employee)._get_favorite_project_id()
self.assertFalse(favorite_project, "A user without timesheet and without access to the internal project should have no favorite project.")
def test_recompute_amount_for_multiple_timesheets(self):
""" Check that amount is recomputed correctly when setting unit_amount for multiple timesheets at once. """
Timesheet = self.env['account.analytic.line']
@ -360,13 +381,12 @@ class TestTimesheet(TestCommonTimesheet):
def test_task_with_timesheet_project_change(self):
'''This test checks that no error is raised when moving a task that contains timesheet to another project.
This move implying writing on the account.analytic.line.
'''
project_manager = self.env['res.users'].create({
'name': 'user_project_manager',
'login': 'user_project_manager',
'groups_id': [(6, 0, [self.ref('project.group_project_manager')])],
'group_ids': [(6, 0, [self.ref('project.group_project_manager')])],
})
project = self.env['project.project'].create({
@ -400,7 +420,15 @@ class TestTimesheet(TestCommonTimesheet):
'project_id': second_project.id
})
self.assertEqual(timesheet.project_id, second_project, 'The project_id of timesheet should be second_project')
self.assertEqual(timesheet.project_id, second_project, 'The project_id of non-validated timesheet should have changed')
def test_compute_display_name(self):
self.timesheet.with_user(self.timesheet_manager_no_project_user)._compute_display_name()
self.assertEqual(
self.timesheet.display_name,
"Test Project - Test Task",
"Display name should be correctly computed without raising AccessError."
)
def test_create_timesheet_employee_not_in_company(self):
''' ts.employee_id only if the user has an employee in the company or one employee for all companies.
@ -410,7 +438,6 @@ class TestTimesheet(TestCommonTimesheet):
analytic_plan = self.env['account.analytic.plan'].create({
'name': 'Plan Test',
'company_id': company_3.id
})
analytic_account = self.env['account.analytic.account'].create({
'name': 'Aa Aa',
@ -420,7 +447,7 @@ class TestTimesheet(TestCommonTimesheet):
project = self.env['project.project'].create({
'name': 'Aa Project',
'company_id': company_3.id,
'analytic_account_id': analytic_account.id,
'account_id': analytic_account.id,
})
task = self.env['project.task'].create({
'name': 'Aa Task',
@ -490,16 +517,46 @@ class TestTimesheet(TestCommonTimesheet):
3) Enter the 8 hour timesheet in the child task
4) Check subtask Effective hours in parent task
"""
self.task1.child_ids = [Command.set([self.task2.id])]
self.env['account.analytic.line'].create({
subtask_1, subtask_2 = self.env['project.task'].create([
{
'name': 'Subtask 1',
'project_id': self.project_customer.id,
},
{
'name': 'Subtask 2',
'project_id': self.project_customer.id,
'child_ids': [Command.create({'name': 'Subsubtask', 'project_id': self.project_customer.id})],
},
])
subsubtask = subtask_2.child_ids
self.task1.child_ids = subtask_1 + subtask_2
self.assertTrue(self.project_customer.allow_timesheets, 'The project should be timesheetable')
self.assertEqual(subtask_1.allow_timesheets, self.project_customer.allow_timesheets, 'The subtask should follow the settings of its project linked.')
Timesheet = self.env['account.analytic.line']
Timesheet.create({
'name': 'FirstTimeSheet',
'project_id': self.project_customer.id,
'task_id': self.task2.id,
'task_id': subtask_1.id,
'unit_amount': 8.0,
'employee_id': self.empl_employee2.id,
})
self.assertEqual(self.task1.subtask_effective_hours, 8, 'Hours Spent on Sub-tasks should be 8 hours in Parent Task')
self.task1.child_ids = [Command.clear()]
Timesheet.create([
{
'name': '/',
'task_id': subtask_2.id,
'unit_amount': 1.0,
'employee_id': self.empl_employee2.id,
},
{
'name': '/',
'task_id': subsubtask.id,
'unit_amount': 1.0,
'employee_id': self.empl_employee2.id,
},
])
self.assertEqual(self.task1.subtask_effective_hours, 10)
def test_ensure_product_uom_set_in_timesheet(self):
self.assertFalse(self.project_customer.timesheet_ids, 'No timesheet should be recorded in this project')
@ -511,12 +568,12 @@ class TestTimesheet(TestCommonTimesheet):
])
self.assertEqual(
timesheet1.product_uom_id,
self.project_customer.analytic_account_id.company_id.timesheet_encode_uom_id,
self.project_customer.account_id.company_id.timesheet_encode_uom_id,
'The default UoM set on the timesheet should be the one set on the company of AA.'
)
self.assertEqual(
timesheet2.product_uom_id,
self.project_customer.analytic_account_id.company_id.timesheet_encode_uom_id,
self.project_customer.account_id.company_id.timesheet_encode_uom_id,
'Even if the product_uom_id field is empty in the vals, the product_uom_id should have a UoM by default,'
' otherwise the `total_timesheet_time` in project should not included the timesheet.'
)
@ -546,16 +603,37 @@ class TestTimesheet(TestCommonTimesheet):
with self.assertRaises(UserError):
timesheet.employee_id = self.empl_employee2
def test_get_view_timesheet_encode_uom(self):
""" Test the label of timesheet time spent fields according to the company encoding timesheet uom """
self.assert_get_view_timesheet_encode_uom([
('hr_timesheet.hr_timesheet_line_form', '//field[@name="unit_amount"]', ['Hours Spent', 'Days Spent']),
('hr_timesheet.project_invoice_form', '//field[@name="allocated_hours"]', [None, 'Allocated Days']),
('hr_timesheet.view_task_form2_inherited', '//field[@name="unit_amount"]', ['Hours Spent', 'Days Spent']),
('hr_timesheet.view_task_tree2_inherited', '//field[@name="planned_hours"]', [None, 'Initially Planned Days']),
('hr_timesheet.view_task_project_user_graph_inherited', '//field[@name="hours_planned"]', [None, 'Planned Days']),
('hr_timesheet.timesheets_analysis_report_pivot_employee', '//field[@name="unit_amount"]', [None, 'Days Spent']),
])
def test_create_timesheet_with_companyless_analytic_account(self):
""" This test ensures that a timesheet can be created on an analytic account whose company_id is set to False"""
self.project_customer.account_id.company_id = False
timesheet_with_project = self.env['account.analytic.line'].with_user(self.user_employee).create(
{'unit_amount': 1.0, 'project_id': self.project_customer.id})
self.assertEqual(timesheet_with_project.product_uom_id, self.project_customer.company_id.project_time_mode_id,
"The product_uom_id of the timesheet should be equal to the project's company uom "
"if the project's analytic account has no company_id and no task_id is defined in the vals")
timesheet_with_task = self.env['account.analytic.line'].with_user(self.user_employee).create({
'unit_amount': 1.0, 'task_id': self.task1.id
})
self.assertEqual(timesheet_with_task.product_uom_id, self.task1.company_id.project_time_mode_id,
"The product_uom_id of the timesheet should be equal to the task's company uom "
"if the project's analytic account has no company_id")
# Remove the company also on the project to be sure we find a UoM
self.project_customer.company_id = False
timesheet_with_project.with_user(self.user_employee).write(
{'unit_amount': 2.0, 'project_id': self.project_customer.id})
self.assertEqual(timesheet_with_project.product_uom_id, self.env.company.project_time_mode_id,
"The product_uom_id of the timesheet should be equal to the company uom "
"if the project's analytic account and the project have no company_id")
def test_create_timesheet_with_default_employee_in_context(self):
timesheet = self.env['account.analytic.line'].with_context(default_employee_id=self.empl_employee.id).create({
'project_id': self.project_customer.id,
'task_id': self.task1.id,
'name': 'Timesheet with default employee in context',
'unit_amount': 3,
})
self.assertEqual(timesheet.employee_id, self.empl_employee)
def test_uom_change_timesheet(self):
"""
@ -577,22 +655,59 @@ class TestTimesheet(TestCommonTimesheet):
'unit_amount': 8,
'employee_id': self.empl_employee2.id
})
project_update_hrs = self.env['project.update'].create({
'name': 'Project update 1',
'project_id': project.id,
'status': 'on_track',
})
self.assertEqual(project_update_hrs.timesheet_time, 8, "Timesheet time should be 8 hours for new project update")
# Clear cached computed project values before the UoM change
self.env['project.project'].invalidate_model()
self.env.company.timesheet_encode_uom_id = self.env.ref('uom.product_uom_day')
self.assertEqual(project.total_timesheet_time, 8, "Total timesheet time should be 8 hours")
self.assertEqual(project.timesheet_encode_uom_id.name, 'Days', "Timesheet encode uom should be 'Days'")
self.assertEqual(project.total_timesheet_time, 1, "Total timesheet time should be 1 day")
project.allocated_hours = 0.0
panel_data = project.get_panel_data()
self.assertEqual(panel_data['buttons'][1]['number'], "1 Days", "Should display '1 Days'")
self.assertEqual(project.timesheet_encode_uom_id, self.env.company.timesheet_encode_uom_id, "Timesheet encode uom should be the one from the company of the env, since the project has no company.")
project_update_days = self.env['project.update'].create({
'name': 'Project update 2',
'project_id': project.id,
'status': 'on_track',
})
self.assertEqual(project_update_days.timesheet_time, 1, "Timesheet time should be 1 day for new project update")
def test_create_timesheet_with_companyless_analytic_account(self):
""" This test ensures that a timesheet can be created on an analytic account whose company_id is set to False"""
self.project_customer.analytic_account_id.company_id = False
timesheet = self.env['account.analytic.line'].with_user(self.user_employee).create(
{'unit_amount': 1.0, 'project_id': self.project_customer.id})
self.assertEqual(timesheet.product_uom_id, self.project_customer.company_id.project_time_mode_id,
"The product_uom_id of the timesheet should be equal to the project's company uom "
"if the project's analytic account has no company_id")
def test_unlink_task_with_timesheet(self):
self.env['account.analytic.line'].create({
'project_id': self.project_customer.id,
'task_id': self.task1.id,
'name': 'timesheet',
'unit_amount': 4,
'employee_id': self.empl_employee.id,
})
self.task2.unlink()
with self.assertRaises(RedirectWarning):
self.task1.unlink()
def test_percentage_of_planned_hours(self):
""" Test the percentage of planned hours on a task. """
self.task1.planned_hours = round(11/60, 2)
def test_cannot_convert_task_with_timesheets_in_private_task(self):
self.env['account.analytic.line'].create({
'name': '/',
'unit_amount': 1,
'project_id': self.project_customer.id,
'task_id': self.task1.id,
'employee_id': self.empl_employee.id,
})
with self.assertRaises(UserError):
self.task1.project_id = False
self.task1.parent_id = self.task2
self.task1.project_id = self.project_customer
with self.assertRaises(UserError):
self.task1.project_id = False
def test_percentage_of_allocated_hours(self):
""" Test the percentage of allocated hours on a task. """
self.task1.allocated_hours = 11/60
self.assertEqual(self.task1.effective_hours, 0, 'No timesheet should be created yet.')
self.assertEqual(self.task1.progress, 0, 'No timesheet should be created yet.')
self.env['account.analytic.line'].create([
@ -616,4 +731,319 @@ class TestTimesheet(TestCommonTimesheet):
'employee_id': self.empl_employee.id,
},
])
self.assertEqual(self.task1.progress, 100, 'The percentage of planned hours should be 100%.')
self.assertEqual(self.task1.progress, 1, 'The progress of allocated hours should be 1.')
def test_timesheet_update_user_on_employee(self):
timesheet = self.env['account.analytic.line'].create({
'project_id': self.project_customer.id,
'task_id': self.task1.id,
'name': 'my first timesheet',
'employee_id': self.empl_employee.id,
})
self.assertEqual(timesheet.user_id, self.empl_employee.user_id)
new_user = self.env['res.users'].create({
'name': 'Test user',
'login': 'test',
})
self.empl_employee.user_id = new_user
self.assertEqual(timesheet.user_id, new_user)
def test_analytic_plan_timesheet_creation(self):
child_analytic_plan = self.env['account.analytic.plan'].create({
'name': 'Child Analytic Plan',
'parent_id': self.analytic_plan.id,
})
child_analytic_account = self.env['account.analytic.account'].create({
'name': 'Analytic Account for Child Analytic Plan',
'partner_id': self.partner.id,
'plan_id': child_analytic_plan.id,
'code': 'TEST',
})
plan_name = f'{self.analytic_plan._column_name()}'
self.project_customer[plan_name] = child_analytic_account
timesheet = self.env['account.analytic.line'].create({
'name': 'Timesheet',
'unit_amount': 1,
'project_id': self.project_customer.id,
'employee_id': self.empl_employee.id,
})
self.assertEqual(self.project_customer.account_id, timesheet.account_id)
self.assertEqual(self.project_customer[plan_name], timesheet[plan_name])
def test_analytic_plan_timesheet_change_project(self):
timesheet = self.env['account.analytic.line'].create({
'name': 'Timesheet',
'unit_amount': 1,
'project_id': self.project_customer.id,
'employee_id': self.empl_employee.id,
})
analytic_plan = self.env['account.analytic.plan'].create({
'name': 'Analytic Plan'
})
analytic_account = self.env['account.analytic.account'].create({
'name': 'Analytic Account',
'partner_id': self.partner.id,
'plan_id': analytic_plan.id,
'code': 'TEST',
})
plan_name = f'{analytic_plan._column_name()}'
other_project = self.env['project.project'].create({
'name': 'Other Project',
'allow_timesheets': True,
plan_name: analytic_account.id,
})
timesheet.project_id = other_project
self.assertEqual(other_project.account_id, timesheet.account_id)
self.assertEqual(other_project[plan_name], timesheet[plan_name])
def test_mandatory_plan_timesheet_applicability(self):
self.env['account.analytic.applicability'].create({
'business_domain': 'timesheet',
'applicability': 'mandatory',
'analytic_plan_id': self.analytic_plan.id,
})
with self.assertRaises(ValidationError):
# The analytic plan 'self.analytic_plan' is mandatory on the project linked to the timesheet
self.env['account.analytic.line'].create({
'name': 'Timesheet',
'unit_amount': 1,
'project_id': self.project_customer.id,
'employee_id': self.empl_employee.id,
})
def test_timesheet_unactive_analytic_account(self):
self.analytic_account.active = False
with self.assertRaises(ValidationError):
# The account_id given to the timesheet must be active
self.env['account.analytic.line'].create({
'name': 'Timesheet',
'unit_amount': 1,
'project_id': self.project_customer.id,
'employee_id': self.empl_employee.id,
})
def test_project_update_reflects_task_allocated_hours_and_timesheet(self):
"""
Check if the project update is according to the project's allocated hours and timesheet.
Step:
1) set 10 allocated hours in the project_customer
2) add timesheet 2 hour in task1
3) create project update and verfity
4) repeat step 1 but allocated hour 12
5) add timesheet 3 hour in task2
6) repeat step 4
8) add timesheet 10 hour in task2
9) repeat step 4
"""
def create_timesheet(task, hours):
self.env['account.analytic.line'].create({
'name': 'Timesheet',
'task_id': task.id,
'unit_amount': hours,
'employee_id': self.empl_employee.id,
})
def create_project_update():
update_form = Form(self.env['project.update'].with_context({'default_project_id': self.project_customer.id}))
update_form.name = "Test"
update_project = update_form.save()
return [update_project.allocated_time, update_project.timesheet_time, update_project.timesheet_percentage]
self.project_customer.allocated_hours = 10
create_timesheet(self.task1, 2)
project_update_vals_list = create_project_update()
self.assertListEqual(project_update_vals_list, [10, 2, 20])
self.project_customer.allocated_hours = 12
create_timesheet(self.task2, 3)
project_update_vals_list = create_project_update()
self.assertListEqual(project_update_vals_list, [12, 5, 42])
create_timesheet(self.task2, 10)
project_update_vals_list = create_project_update()
self.assertListEqual(project_update_vals_list, [12, 15, 125])
def test_calendar_display_name(self):
timesheet = self.env['account.analytic.line'].create({
'name': 'Timesheet',
'unit_amount': 1,
'project_id': self.project_customer.id,
'employee_id': self.empl_employee.id,
})
self.assertEqual(timesheet.calendar_display_name, f"{self.project_customer.name} (1h)")
timesheet.unit_amount = 1.5
timesheet.invalidate_recordset(['calendar_display_name'])
self.assertEqual(timesheet.calendar_display_name, f"{self.project_customer.name} (1h30)")
timesheet.unit_amount = 8
timesheet.company_id.timesheet_encode_uom_id = self.env.ref('uom.product_uom_day')
timesheet.invalidate_recordset(['calendar_display_name'])
self.assertEqual(timesheet.calendar_display_name, f"{self.project_customer.name} (1d)")
timesheet.unit_amount = 12
timesheet.invalidate_recordset(['calendar_display_name'])
self.assertEqual(timesheet.calendar_display_name, f"{self.project_customer.name} (1.5d)")
def test_multi_create_timesheets_from_calendar(self):
"""
Simulate creating timesheets using the multi-create feature in the calendar view
"""
HrTimesheet = self.env['account.analytic.line'].with_context(timesheet_calendar=True)
timesheet = HrTimesheet.create({
'project_id': self.project_customer.id,
'unit_amount': 1,
'employee_id': self.empl_employee.id,
'date': '2025-05-26',
})
self.assertTrue(timesheet, "The timesheet should be created without errors")
self.assertEqual(timesheet.employee_id, self.empl_employee)
self.assertEqual(fields.Date.to_string(timesheet.date), '2025-05-26')
timesheet = HrTimesheet.create({
'project_id': self.project_customer.id,
'unit_amount': 1,
'employee_id': self.empl_employee.id,
'date': '2025-05-25', # Sunday
})
self.assertFalse(timesheet, "The timesheet should not get created on a weekend")
calendar = self.env['resource.calendar'].with_company(self.env.company).create({
'name': "part time",
'hours_per_day': 8.0,
'attendance_ids': [
(0, 0, {'name': "Monday Morning", 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': "Monday Lunch", 'dayofweek': '0', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': "Monday Evening", 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': "Thursday Morning", 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': "Thursday Lunch", 'dayofweek': '3', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': "Thursday Evening", 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': "Friday Morning", 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': "Friday Lunch", 'dayofweek': '4', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': "Friday Evening", 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
self.empl_employee.resource_calendar_id = calendar
self.empl_employee2.resource_calendar_id = calendar
with patch.object(self.env.registry['bus.bus'], '_sendone') as mock_send:
def create_timesheet_records(days):
return HrTimesheet.create([{
'project_id': self.project_customer.id,
'unit_amount': 1,
'date': f'2025-05-{day}',
'employee_id': employee.id,
} for day in days for employee in (self.empl_employee, self.empl_employee2)])
def assert_notification(mock_send, notification_type, message):
mock_send.assert_called_with(self.env.user.partner_id, 'simple_notification', {
'type': notification_type,
'message': message,
})
mock_send.reset_mock()
timesheets = create_timesheet_records(('25', '26', '27'))
self.assertEqual(len(timesheets), 2)
self.assertEqual(fields.Date.to_string(timesheets[0].date), '2025-05-26', "The timesheet creation should skip non-working days")
self.assertEqual(fields.Date.to_string(timesheets[1].date), '2025-05-26', "The timesheet creation should skip non-working days")
assert_notification(mock_send, 'danger', "Some timesheets were not created: employees arent working on the selected days")
timesheets = create_timesheet_records(('18', '25'))
self.assertEqual(len(timesheets), 0)
assert_notification(mock_send, 'danger', "No timesheets created: employees arent working on the selected days")
timesheets = create_timesheet_records(('8', '9'))
self.assertEqual(len(timesheets), 4)
assert_notification(mock_send, 'success', "Timesheets successfully created")
def test_keep_create_account_values_at_timesheet_creation(self):
field_name = self.analytic_plan._column_name()
analytic_account, another_account = self.env['account.analytic.account'].create([
{
'name': 'Analytic Account',
'partner_id': self.partner.id,
'plan_id': self.analytic_plan.id,
'code': 'TEST',
},
{
'name': 'Another Analytic Account',
'partner_id': self.partner.id,
'plan_id': self.analytic_plan.id,
'code': 'TEST2',
},
])
self.project_customer.write({
field_name: another_account.id,
})
line = self.env['account.analytic.line'].create({
'name': 'Timesheet',
'unit_amount': 1,
'project_id': self.project_customer.id,
field_name: analytic_account.id,
'employee_id': self.empl_employee.id,
})
self.assertEqual(line[field_name].id, analytic_account.id, f"The value of {field_name} shouldn't get overwritten by the project's account")
def test_keep_write_account_values_at_timesheet_update(self):
field_name = self.analytic_plan._column_name()
analytic_account, another_account = self.env['account.analytic.account'].create([
{
'name': 'Analytic Account',
'partner_id': self.partner.id,
'plan_id': self.analytic_plan.id,
'code': 'TEST',
},
{
'name': 'Another Analytic Account',
'partner_id': self.partner.id,
'plan_id': self.analytic_plan.id,
'code': 'TEST2',
},
])
self.project_customer.write({
field_name: another_account.id,
})
line = self.env['account.analytic.line'].create({
'name': 'Timesheet',
'unit_amount': 1,
'employee_id': self.empl_employee.id,
})
line.write({
'project_id': self.project_customer.id,
field_name: analytic_account.id
})
self.assertEqual(line[field_name].id, analytic_account.id, f"The value of {field_name} shouldn't get overwritten by the project's account")
def test_split_analytic_dynamic_update(self):
self.empl_employee.hourly_cost = 10.0
another_account = self.project.account_id.copy()
line = self.env['account.analytic.line'].create({
'name': 'Timesheet',
'unit_amount': 1,
'project_id': self.project_customer.id,
'account_id': self.project.account_id.id,
'employee_id': self.empl_employee.id,
})
self.assertEqual(line.amount, -10)
line.analytic_distribution = {
f"{self.project.account_id.id}": 50,
f"{another_account.id}": 50,
}
self.assertEqual(line.amount, -5) # the line is split in 2
def test_log_timesheet_with_user_has_two_employees_from_different_companies(self):
company_2 = self.env['res.company'].create({'name': 'Company 2'})
self.env['hr.employee'].with_company(company_2).create({
'name': 'Employee 2',
'user_id': self.user_manager.id,
})
timesheet = self.env['account.analytic.line'].create({
'project_id': self.project.id,
'user_id': self.user_manager.id,
})
self.assertEqual(timesheet.company_id, self.env.company)

View file

@ -0,0 +1,23 @@
from odoo.tests import TransactionCase, tagged
@tagged("post_install", "-at_install")
class TestHrTimesheetImportTemplate(TransactionCase):
def setUp(self):
super().setUp()
self.AccountLine = self.env['account.analytic.line']
def fetch_template_for_timesheet(self, is_timesheet):
return self.AccountLine.with_context(is_timesheet=is_timesheet).get_import_templates()
def test_import_template(self):
template = self.fetch_template_for_timesheet(True)
self.assertEqual(len(template), 1)
self.assertEqual(
template[0]['template'], '/hr_timesheet/static/xls/timesheets_import_template.xlsx',
)
template = self.fetch_template_for_timesheet(False)
self.assertEqual(template, [])
template = self.fetch_template_for_timesheet(None)
self.assertEqual(template, [])