mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-25 13:32:00 +02:00
1049 lines
48 KiB
Python
1049 lines
48 KiB
Python
# -*- coding: utf-8 -*-
|
||
# 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 import Form, TransactionCase, new_test_user
|
||
from odoo.exceptions import AccessError, RedirectWarning, UserError, ValidationError
|
||
|
||
|
||
class TestCommonTimesheet(TransactionCase):
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super(TestCommonTimesheet, cls).setUpClass()
|
||
|
||
# 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
|
||
rule = cls.env.ref('timesheet_grid.hr_timesheet_rule_approver_update', raise_if_not_found=False)
|
||
if rule:
|
||
rule.active = False
|
||
|
||
# customer partner
|
||
cls.partner = cls.env['res.partner'].create({
|
||
'name': 'Customer Task',
|
||
'email': 'customer@task.com',
|
||
'phone': '42',
|
||
})
|
||
|
||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||
'name': 'Timesheet Plan Test',
|
||
})
|
||
cls.analytic_account = cls.env['account.analytic.account'].create({
|
||
'name': 'Analytic Account for Test Customer',
|
||
'partner_id': cls.partner.id,
|
||
'plan_id': cls.analytic_plan.id,
|
||
'code': 'TEST'
|
||
})
|
||
|
||
# project and tasks
|
||
cls.project_customer = cls.env['project.project'].create({
|
||
'name': 'Project X',
|
||
'allow_timesheets': True,
|
||
'partner_id': cls.partner.id,
|
||
'account_id': cls.analytic_account.id,
|
||
})
|
||
cls.task1 = cls.env['project.task'].create({
|
||
'name': 'Task One',
|
||
'priority': '0',
|
||
'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',
|
||
'state': '1_done',
|
||
'project_id': cls.project_customer.id,
|
||
})
|
||
# users
|
||
cls.user_employee = cls.env['res.users'].create({
|
||
'name': 'User Employee',
|
||
'login': 'user_employee',
|
||
'email': 'useremployee@test.com',
|
||
'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',
|
||
'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',
|
||
'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',
|
||
})
|
||
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().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.
|
||
# but there is an ir.rule about it, crashing during its evaluation
|
||
rule = self.env.ref('timesheet_grid.timesheet_line_rule_user_update-unlink', raise_if_not_found=False)
|
||
if rule:
|
||
rule.active = False
|
||
|
||
def test_log_timesheet(self):
|
||
""" Test when log timesheet: check analytic account, user and employee are correctly set. """
|
||
Timesheet = self.env['account.analytic.line']
|
||
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,
|
||
'task_id': self.task1.id,
|
||
'name': 'my first timesheet',
|
||
'unit_amount': 4,
|
||
})
|
||
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.")
|
||
|
||
# employee 1 cannot log timesheet for employee 2
|
||
with self.assertRaises(AccessError):
|
||
timesheet2 = Timesheet.with_user(self.user_employee).create({
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': 'a second timesheet but for employee 2',
|
||
'unit_amount': 3,
|
||
'employee_id': self.empl_employee2.id,
|
||
})
|
||
|
||
# manager log timesheet for employee 2
|
||
timesheet3 = Timesheet.with_user(self.user_manager).create({
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': 'a second timesheet but for employee 2',
|
||
'unit_amount': 7,
|
||
'employee_id': self.empl_employee2.id,
|
||
})
|
||
self.assertEqual(timesheet3.user_id, self.user_employee2, 'Timesheet user should be the one linked to the given employee')
|
||
self.assertEqual(timesheet3.product_uom_id, timesheet_uom, "The UoM of the timesheet 3 should be the one set on the company of the analytic account.")
|
||
|
||
# employee 1 log some timesheet on project (no task)
|
||
timesheet4 = Timesheet.with_user(self.user_employee).create({
|
||
'project_id': self.project_customer.id,
|
||
'name': 'my first timesheet',
|
||
'unit_amount': 4,
|
||
})
|
||
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 """
|
||
# employee 1 log some timesheet on task 1
|
||
Timesheet = self.env['account.analytic.line']
|
||
timesheet1 = Timesheet.with_user(self.user_employee).create({
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': 'my first timesheet',
|
||
'unit_amount': 4,
|
||
})
|
||
# then employee 2 try to modify it
|
||
with self.assertRaises(AccessError):
|
||
timesheet1.with_user(self.user_employee2).write({
|
||
'name': 'i try to update this timesheet',
|
||
'unit_amount': 2,
|
||
})
|
||
# manager can modify all timesheet
|
||
timesheet1.with_user(self.user_manager).write({
|
||
'unit_amount': 8,
|
||
'employee_id': self.empl_employee2.id,
|
||
})
|
||
self.assertEqual(timesheet1.user_id, self.user_employee2, 'Changing timesheet employee should change the related user')
|
||
|
||
def test_create_unlink_project(self):
|
||
""" Check project creation, and if necessary the analytic account generated when project should track time. """
|
||
# create project wihtout tracking time, nor provide AA
|
||
non_tracked_project = self.env['project.project'].create({
|
||
'name': 'Project without timesheet',
|
||
'allow_timesheets': False,
|
||
'partner_id': self.partner.id,
|
||
})
|
||
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({
|
||
'name': 'Project with timesheet',
|
||
'allow_timesheets': True,
|
||
'partner_id': self.partner.id,
|
||
})
|
||
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,
|
||
'account_id': tracked_project.account_id.id,
|
||
})
|
||
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({
|
||
'name': 'task in tracked project',
|
||
'project_id': tracked_project.id,
|
||
})
|
||
with self.assertRaises(UserError):
|
||
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.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 = Task.create({
|
||
'name': 'Task Child',
|
||
'parent_id': self.task1.id,
|
||
})
|
||
|
||
task_grandchild = Task.create({
|
||
'name': 'Task Grandchild',
|
||
'parent_id': task_child.id,
|
||
})
|
||
|
||
# create a second project
|
||
self.project_customer2 = self.env['project.project'].create({
|
||
'name': 'Project NUMBER DEUX',
|
||
'allow_timesheets': True,
|
||
})
|
||
# employee 1 log some timesheet on task 1 and its subtasks
|
||
Timesheet.create([{
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': 'my first timesheet',
|
||
'unit_amount': 4,
|
||
'employee_id': self.empl_employee.id,
|
||
}, {
|
||
'project_id': self.project_customer.id,
|
||
'task_id': task_child.id,
|
||
'name': 'my second timesheet',
|
||
'unit_amount': 4,
|
||
'employee_id': self.empl_employee.id,
|
||
}, {
|
||
'project_id': self.project_customer.id,
|
||
'task_id': task_grandchild.id,
|
||
'name': 'my third timesheet',
|
||
'unit_amount': 4,
|
||
'employee_id': self.empl_employee.id,
|
||
}])
|
||
|
||
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, 3, "3 timesheets should be linked to Project1")
|
||
self.assertEqual(timesheet_count2, 0, "No timesheets should be linked to Project2")
|
||
self.assertEqual(len(self.task1.timesheet_ids), 1, "The timesheet should be linked to task1")
|
||
self.assertEqual(len(task_child.timesheet_ids), 1, "The timesheet should be linked to task_child")
|
||
self.assertEqual(len(task_grandchild.timesheet_ids), 1, "The timesheet should be linked to task_grandchild")
|
||
|
||
# change project of task 1 from form to trigger onchange
|
||
with Form(self.task1) as task_form:
|
||
task_form.project_id = self.project_customer2
|
||
|
||
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, "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 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']
|
||
self.empl_employee.hourly_cost = 5.0
|
||
self.empl_employee2.hourly_cost = 6.0
|
||
# create a timesheet for each employee
|
||
timesheet_1 = Timesheet.with_user(self.user_employee).create({
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': '/',
|
||
'unit_amount': 1,
|
||
})
|
||
timesheet_2 = Timesheet.with_user(self.user_employee2).create({
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': '/',
|
||
'unit_amount': 1,
|
||
})
|
||
timesheets = timesheet_1 + timesheet_2
|
||
|
||
with self.assertRaises(AccessError):
|
||
# should raise since employee 1 doesn't have the access rights to update employee's 2 timesheet
|
||
timesheets.with_user(self.empl_employee.user_id).write({
|
||
'unit_amount': 2,
|
||
})
|
||
|
||
timesheets.with_user(self.user_manager).write({
|
||
'unit_amount': 2,
|
||
})
|
||
|
||
# since timesheet costs are different for both employees, we should get different amounts
|
||
self.assertRecordValues(timesheets.with_user(self.user_manager), [{
|
||
'amount': -10.0,
|
||
}, {
|
||
'amount': -12.0,
|
||
}])
|
||
|
||
def test_recompute_partner_from_task_customer_change(self):
|
||
partner2 = self.env['res.partner'].create({
|
||
'name': 'Customer Task 2',
|
||
'email': 'customer2@task.com',
|
||
'phone': '43',
|
||
})
|
||
|
||
timesheet_entry = self.env['account.analytic.line'].create({
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': 'my only timesheet',
|
||
'unit_amount': 4,
|
||
'user_id': self.user_employee.id,
|
||
})
|
||
|
||
self.assertEqual(timesheet_entry.partner_id, self.partner, "The timesheet entry's partner should be equal to the task's partner/customer")
|
||
|
||
self.task1.write({'partner_id': partner2})
|
||
|
||
self.assertEqual(timesheet_entry.partner_id, partner2, "The timesheet entry's partner should still be equal to the task's partner/customer, after the change")
|
||
|
||
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.
|
||
'''
|
||
|
||
project_manager = self.env['res.users'].create({
|
||
'name': 'user_project_manager',
|
||
'login': 'user_project_manager',
|
||
'group_ids': [(6, 0, [self.ref('project.group_project_manager')])],
|
||
})
|
||
|
||
project = self.env['project.project'].create({
|
||
'name': 'Project With Timesheets',
|
||
'privacy_visibility': 'employees',
|
||
'allow_timesheets': True,
|
||
'user_id': project_manager.id,
|
||
})
|
||
second_project = self.env['project.project'].create({
|
||
'name': 'Project w/ timesheets',
|
||
'privacy_visibility': 'employees',
|
||
'allow_timesheets': True,
|
||
'user_id': project_manager.id,
|
||
})
|
||
|
||
task_1 = self.env['project.task'].create({
|
||
'name': 'First task',
|
||
'user_ids': self.user_employee2,
|
||
'project_id': project.id
|
||
})
|
||
|
||
timesheet = self.env['account.analytic.line'].create({
|
||
'name': 'FirstTimeSheet',
|
||
'project_id': project.id,
|
||
'task_id': task_1.id,
|
||
'unit_amount': 2,
|
||
'employee_id': self.empl_employee2.id
|
||
})
|
||
|
||
task_1.with_user(project_manager).write({
|
||
'project_id': second_project.id
|
||
})
|
||
|
||
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.
|
||
'''
|
||
company_2 = self.env['res.company'].create({'name': 'Company 2'})
|
||
company_3 = self.env['res.company'].create({'name': 'Company 3'})
|
||
|
||
analytic_plan = self.env['account.analytic.plan'].create({
|
||
'name': 'Plan Test',
|
||
})
|
||
analytic_account = self.env['account.analytic.account'].create({
|
||
'name': 'Aa Aa',
|
||
'plan_id': analytic_plan.id,
|
||
'company_id': company_3.id,
|
||
})
|
||
project = self.env['project.project'].create({
|
||
'name': 'Aa Project',
|
||
'company_id': company_3.id,
|
||
'account_id': analytic_account.id,
|
||
})
|
||
task = self.env['project.task'].create({
|
||
'name': 'Aa Task',
|
||
'project_id': project.id,
|
||
})
|
||
|
||
Timesheet = self.env['account.analytic.line'].with_context(allowed_company_ids=[company_3.id, company_2.id, self.env.company.id])
|
||
timesheet = Timesheet.create({
|
||
'name': 'Timesheet',
|
||
'project_id': project.id,
|
||
'task_id': task.id,
|
||
'unit_amount': 2,
|
||
'user_id': self.user_manager.id,
|
||
'company_id': company_3.id,
|
||
})
|
||
self.assertEqual(timesheet.employee_id, self.user_manager.employee_id, 'As there is a unique employee for this user, it must be found')
|
||
|
||
self.env['hr.employee'].with_company(company_2).create({
|
||
'name': 'Employee 2',
|
||
'user_id': self.user_manager.id,
|
||
})
|
||
with self.assertRaises(ValidationError):
|
||
# As there are several employees for this user, but none of them in this company, none must be found
|
||
Timesheet.create({
|
||
'name': 'Timesheet',
|
||
'project_id': project.id,
|
||
'task_id': task.id,
|
||
'unit_amount': 2,
|
||
'user_id': self.user_manager.id,
|
||
'company_id': company_3.id,
|
||
})
|
||
|
||
def test_create_timesheet_with_multi_company(self):
|
||
""" Always set the current company in the timesheet, not the employee company """
|
||
company_4 = self.env['res.company'].create({'name': 'Company 4'})
|
||
empl_employee, archived_employee = self.env['hr.employee'].with_company(company_4).create([
|
||
{'name': 'Employee 3'},
|
||
{'name': 'Employee 4', 'active': False},
|
||
])
|
||
|
||
Timesheet = self.env['account.analytic.line'].with_context(allowed_company_ids=[company_4.id, self.env.company.id])
|
||
|
||
timesheet = Timesheet.create({
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': 'my first timesheet',
|
||
'unit_amount': 4,
|
||
'employee_id': empl_employee.id,
|
||
})
|
||
self.assertEqual(timesheet.company_id.id, self.env.company.id)
|
||
|
||
with self.assertRaises(UserError, msg="The employee must be active to encode a timesheet"):
|
||
Timesheet.create({
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': 'my first timesheet',
|
||
'unit_amount': 4,
|
||
'employee_id': archived_employee.id,
|
||
})
|
||
|
||
def test_subtask_log_timesheet(self):
|
||
""" Test parent task takes into account the timesheets of its sub-tasks.
|
||
Test Case:
|
||
----------
|
||
1) Create parent task
|
||
2) Create child/subtask task
|
||
3) Enter the 8 hour timesheet in the child task
|
||
4) Check subtask Effective hours in parent task
|
||
"""
|
||
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': 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')
|
||
|
||
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')
|
||
self.assertFalse(self.project_customer.total_timesheet_time, 'The total time recorded should be equal to 0 since no timesheet is recorded.')
|
||
|
||
timesheet1, timesheet2 = self.env['account.analytic.line'].with_user(self.user_employee).create([
|
||
{'unit_amount': 1.0, 'project_id': self.project_customer.id},
|
||
{'unit_amount': 3.0, 'project_id': self.project_customer.id, 'product_uom_id': False},
|
||
])
|
||
self.assertEqual(
|
||
timesheet1.product_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.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.'
|
||
)
|
||
self.assertEqual(self.project_customer.timesheet_ids, timesheet1 + timesheet2)
|
||
self.assertEqual(
|
||
self.project_customer.total_timesheet_time,
|
||
timesheet1.unit_amount + timesheet2.unit_amount,
|
||
'The total timesheet time of this project should be equal to 4.'
|
||
)
|
||
def test_create_timesheet_with_archived_employee(self):
|
||
''' the timesheet can be created or edited only with an active employee
|
||
'''
|
||
self.empl_employee2.active = False
|
||
batch_vals = {
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'name': 'archived employee timesheet',
|
||
'unit_amount': 3,
|
||
'employee_id': self.empl_employee2.id
|
||
}
|
||
|
||
self.assertRaises(UserError, self.env['account.analytic.line'].create, batch_vals)
|
||
|
||
batch_vals["employee_id"] = self.empl_employee.id
|
||
timesheet = self.env['account.analytic.line'].create(batch_vals)
|
||
|
||
with self.assertRaises(UserError):
|
||
timesheet.employee_id = self.empl_employee2
|
||
|
||
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):
|
||
"""
|
||
We check that we don't over transform the timesheet unit amount when changing
|
||
the company encoding timesheet uom, we keep it in the project as hours.
|
||
So it will be transformed only once when encoding the timesheet.
|
||
"""
|
||
Timesheet = self.env['account.analytic.line']
|
||
project = self.env['project.project'].create({
|
||
'name': 'Project',
|
||
'allow_timesheets': True,
|
||
'partner_id': self.partner.id,
|
||
})
|
||
project.allocated_hours = 40.0
|
||
|
||
Timesheet.create({
|
||
'name': 'FirstTimeSheet',
|
||
'project_id': project.id,
|
||
'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, 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_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_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([
|
||
{
|
||
'name': 'Timesheet',
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'unit_amount': 3/60,
|
||
'employee_id': self.empl_employee.id,
|
||
}, {
|
||
'name': 'Timesheet',
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'unit_amount': 4/60,
|
||
'employee_id': self.empl_employee.id,
|
||
}, {
|
||
'name': 'Timesheet',
|
||
'project_id': self.project_customer.id,
|
||
'task_id': self.task1.id,
|
||
'unit_amount': 4/60,
|
||
'employee_id': self.empl_employee.id,
|
||
},
|
||
])
|
||
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 aren’t 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 aren’t 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)
|