Files

448 lines
22 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from datetime import date, timedelta
from freezegun import freeze_time
from unittest.mock import patch
from odoo.tests import tagged, Form
from odoo.exceptions import ValidationError
from .common import TestHelpdeskTimesheetCommon
@tagged('-at_install', 'post_install')
class TestTimesheet(TestHelpdeskTimesheetCommon):
def test_timesheet_cannot_be_linked_to_task_and_ticket(self):
""" Test if an exception is raised when we want to link a task and a ticket in a timesheet
Normally, now we cannot have a ticket and a task in one timesheet.
Test Case:
=========
1) Create ticket and a task,
2) Create timesheet with this ticket and task and check if an exception is raise.
"""
# 1) Create ticket and a task
ticket = self.helpdesk_ticket
task = self.env['project.task'].create({
'name': 'Test Task',
'project_id': self.project.id,
})
# 2) Create timesheet with this ticket and task and check if an exception is raise
with self.assertRaises(ValidationError):
self.env['account.analytic.line'].create({
'name': 'Test Timesheet',
'unit_amount': 1,
'project_id': self.project.id,
'helpdesk_ticket_id': ticket.id,
'task_id': task.id,
'employee_id': self.env['hr.employee'].create({'user_id': self.env.uid}).id,
})
def test_compute_timesheet_partner_from_ticket_customer(self):
partner2 = self.env['res.partner'].create({
'name': 'Customer ticket',
'email': '[email protected]',
})
helpdesk_ticket = self.helpdesk_ticket
timesheet_entry = self.env['account.analytic.line'].create({
'name': 'the only timesheet. So lonely...',
'helpdesk_ticket_id': helpdesk_ticket.id,
'project_id': self.helpdesk_team.project_id.id,
'employee_id': self.env['hr.employee'].create({'user_id': self.env.uid}).id,
})
self.assertEqual(timesheet_entry.partner_id, self.partner, "The timesheet entry's partner should be equal to the ticket's partner/customer")
helpdesk_ticket.write({'partner_id': partner2.id})
self.assertEqual(timesheet_entry.partner_id, partner2, "The timesheet entry's partner should still be equal to the ticket's partner/customer, after the change")
def test_helpdesk_timesheet_wizard_timezones(self):
user = self.user_employee
ticket = self.helpdesk_ticket.with_user(user)
timezones = [
'Pacific/Niue', # UTC-11,
'Europe/Brussels', # UTC+1
'Pacific/Kiritimati', # UTC+14
]
test_cases = {
'2024-01-24 08:30': (-1, +0, +0),
'2024-01-24 10:30': (-1, +0, +1),
'2024-01-24 16:30': (+0, +0, +1),
}
for utc_time, day_diffs in test_cases.items():
with freeze_time(utc_time):
for tz, diff in zip(timezones, day_diffs):
# reset all timezones on the environments
user.tz = tz
self.env.flush_all()
self.env.transaction.reset()
local_date = date.today() + timedelta(diff)
self.assertTrue(ticket.display_timesheet_timer, "The timer should be available in that ticket")
ticket.action_timer_start()
action = ticket.action_timer_stop()
wizard = self.env['hr.timesheet.stop.timer.confirmation.wizard'] \
.with_context(action['context']) \
.with_user(user) \
.new({})
timesheet = wizard.timesheet_id
wizard.action_save_timesheet()
self.assertEqual(
timesheet.date,
local_date,
f"{utc_time} UTC should be {local_date} in {tz} time",
)
def test_log_timesheet_with_ticket_analytic_account(self):
""" Test whether the analytic account of the project is set on the ticket.
Test Case:
----------
1) Create Ticket
2) Check the default analytic account of the project and ticket
"""
helpdesk_ticket = self.helpdesk_ticket
self.assertEqual(helpdesk_ticket.analytic_account_id, self.project.account_id)
def test_compute_project_id(self):
""" Test compute project_id works as expected when helpdesk_ticket_id changes on a timesheet """
helpdesk_ticket = self.env['helpdesk.ticket'].create({
'name': 'Test Ticket',
'team_id': self.helpdesk_team.id,
'partner_id': self.partner.id,
})
timesheet = self.env['account.analytic.line'].create({
'name': '/',
'project_id': self.project_customer.id,
'task_id': self.task1.id,
'employee_id': self.empl_employee.id,
})
timesheet.helpdesk_ticket_id = helpdesk_ticket
self.assertFalse(timesheet.task_id, "The task should be unset since a helpdesk ticket has been set on the timesheet")
self.assertEqual(timesheet.project_id, self.project, "The project set on the timesheet should now the project on the helpdesk team of the ticket linked.")
def test_timesheet_customer_association(self):
employee = self.env['hr.employee'].create({'user_id': self.env.uid})
ticket_form = Form(self.env['helpdesk.ticket'])
ticket_form.partner_id = self.partner
ticket_form.name = 'Test'
ticket_form.team_id = self.helpdesk_team
with ticket_form.timesheet_ids.new() as line:
line.employee_id = employee
line.name = "/"
line.unit_amount = 3
ticket = ticket_form.save()
self.assertEqual(ticket.timesheet_ids.partner_id, ticket.partner_id, "The timesheet partner should be equal to the ticket's partner/customer")
partner = self.env['res.partner'].create({
'name': 'Customer ticket',
'email': '[email protected]',
})
ticket.partner_id = partner
self.assertEqual(ticket.timesheet_ids.partner_id, partner, "The partner set on the timesheet should follow the one set on the ticket linked.")
def test_timesheet_bulk_creation_of_timesheets_for_seperate_ticket_id(self):
""" Test whether creating timesheets in bulk for separate ticket ids works """
# (1) create 2 tickets
helpdesk_ticket_1, helpdesk_ticket_2 = self.env['helpdesk.ticket'].create([{
'name': 'Test Ticket 1',
'team_id': self.helpdesk_team.id,
'partner_id': self.partner.id,
}, {
'name': 'Test Ticket 2',
'team_id': self.helpdesk_team.id,
'partner_id': self.partner.id,
}])
# (2) create timesheet for each ticket in a bulk create
timesheet_1, timesheet_2 = self.env['account.analytic.line'].create([{
'name': 'non valid timesheet test',
'helpdesk_ticket_id': helpdesk_ticket_1.id,
'employee_id': self.empl_employee.id,
}, {
'name': 'validated timesheet test',
'helpdesk_ticket_id': helpdesk_ticket_2.id,
'employee_id': self.empl_employee.id,
}])
# (3) Verify each timesheet exists
self.assertTrue(timesheet_1, "Bulk creation of timesheets should work for separate ticket ids")
self.assertTrue(timesheet_2, "Bulk creation of timesheets should work for separate ticket ids")
self.assertEqual(timesheet_1.helpdesk_ticket_id, helpdesk_ticket_1)
self.assertEqual(timesheet_2.helpdesk_ticket_id, helpdesk_ticket_2)
def test_timesheet_ticket_consistency_when_helpdesk_team_change(self):
"""
Test that the change of `helpdesk_team` of a helpdesk_ticket results in:
- non-validated timesheets have the new project associated.
- validated timesheets does not get change and keep the initial project.
"""
self.env.user.tz = 'UTC'
# Create ticket and store initial project (helpdesk_team)
helpdesk_ticket = self.env['helpdesk.ticket'].create({
'name': 'Test Ticket',
'team_id': self.helpdesk_team.id,
'partner_id': self.partner.id,
})
initial_project = helpdesk_ticket.project_id
# create a valid timesheet and non-validated timesheet under the same helpdesk_ticket/project
timesheet_valid, timesheet_non_valid = self.env['account.analytic.line'].create([{
'name': 'valid timesheet test',
'project_id': initial_project.id,
'helpdesk_ticket_id': helpdesk_ticket.id,
'employee_id': self.empl_employee.id,
}, {
'name': 'non valid timesheet test',
'project_id': initial_project.id,
'helpdesk_ticket_id': helpdesk_ticket.id,
'employee_id': self.empl_employee.id,
}])
timesheet_valid.action_validate_timesheet()
self.assertTrue(timesheet_valid.validated, "The timesheet should be validated.")
# create a new project and helpdesk_team with timesheet feature
new_project = self.env['project.project'].create({
'name': 'Project 2',
'allow_timesheets': True,
'partner_id': self.partner.id,
})
new_helpdesk_team = self.env['helpdesk.team'].create({
'name': 'Test Team 2',
'use_helpdesk_timesheet': True,
'project_id': new_project.id,
})
# change the helpdesk_team of the helpdesk_ticket
form = Form(helpdesk_ticket)
form.team_id = new_helpdesk_team
form.save()
self.assertEqual(timesheet_valid.project_id, initial_project,
"Validated timesheet should keep the same project when the helpdesk_ticket's helpdesk_team is changed.")
self.assertEqual(timesheet_non_valid.project_id, new_project,
"Non-validated timesheet should have the new project set when the helpdesk_ticket's helpdesk_team is changed.")
def test_timesheet_check_warning_when_helpdesk_team_change(self):
"""
1) Test that when a ticket changes its project (helpdesk_team), if the ticket contains at least one non-validated timesheet AND
the new project has no timesheet feature available, a warning notification should be raised.
2) The tickets's helpdesk_team and non-validated timesheets should be changed despite the warning raised.
3) Checks that no warning is raised when the timesheets are validated.
"""
self.env.user.tz = 'UTC'
# (1) create tickets and timesheets
helpdesk_ticket_non_valid, helpdesk_ticket_valid = self.env['helpdesk.ticket'].create([{
'name': 'Test Ticket non valid',
'team_id': self.helpdesk_team.id,
'partner_id': self.partner.id,
}, {
'name': 'Test Ticket only valid',
'team_id': self.helpdesk_team.id,
'partner_id': self.partner.id,
}])
dummy, timesheet_validated = self.env['account.analytic.line'].create([{
'name': 'non valid timesheet test',
'helpdesk_ticket_id': helpdesk_ticket_non_valid.id,
'employee_id': self.empl_employee.id,
}, {
'name': 'validated timesheet test',
'helpdesk_ticket_id': helpdesk_ticket_valid.id,
'employee_id': self.empl_employee.id,
}])
# create helpdesk_team without timesheet feature
no_timesheet_helpdesk_team = self.env['helpdesk.team'].create({
'name': 'Test Team with no timesheet',
'use_helpdesk_timesheet': False,
})
expected_warning = {
'title': "Warning",
'message': "Moving this task to a helpdesk team without timesheet support will retain timesheet drafts in the original helpdesk team. "
"Although they won't be visible here, you can still edit them using the Timesheets app.",
'type': "notification",
}
with patch.object(logging.getLogger('odoo.tests.form').getChild('onchange'), 'warning') as mock_warning:
# change the helpdesk_team of the ticket containing non-validated timesheet, to the helpdesk_team without timesheet feature
helpdesk_ticket_form = Form(helpdesk_ticket_non_valid, view="helpdesk_timesheet.helpdesk_ticket_view_form_inherit_helpdesk_timesheet")
helpdesk_ticket_form.team_id = no_timesheet_helpdesk_team
mock_warning.assert_called_once_with("%(title)s %(message)s", expected_warning)
helpdesk_ticket_form.save()
# (2) verify that after the warning, the helpdesk_team is changed
self.assertEqual(helpdesk_ticket_non_valid.team_id, no_timesheet_helpdesk_team,
"The ticket's helpdesk_team should be changed despite the warning raised.")
# (3) Create ticket including only validated timesheet
timesheet_validated.action_validate_timesheet()
self.assertTrue(timesheet_validated.validated, "The timesheet should be validated.")
# change the helpdesk_team of the ticket only containing validated timesheet, to the helpdesk_team without timesheet feature
helpdesk_ticket_valid.write({'team_id': no_timesheet_helpdesk_team.id})
warning = helpdesk_ticket_valid._onchange_team_id()
self.assertFalse(warning, "No warning should be raised when the ticket's timesheets are validated.")
def test_default_company_id_for_timesheet(self):
""" This test ensures that the default company_id used when a timesheet is created from the ticket form view is the company_id of its project. """
new_company = self.env['res.company'].create({'name': "Do the extra miles"})
company_second = self.env['res.company'].create({'name': "Second company"})
self.env['hr.employee'].create({'user_id': self.env.uid, 'company_id': new_company.id})
self.env['hr.employee'].create({'user_id': self.env.uid, 'company_id': company_second.id})
project = self.env['project.project'].create({
'name': 'Project',
'allow_timesheets': True,
'partner_id': self.partner.id,
})
helpdesk_team = self.env['helpdesk.team'].create({
'name': 'Test Team new company',
'use_helpdesk_timesheet': True,
'project_id': project.id,
'company_id': new_company.id,
})
helpdesk_ticket = self.env['helpdesk.ticket'].create({
'name': 'Test Ticket',
'team_id': helpdesk_team.id,
'partner_id': self.partner.id,
})
# Use the default values sent by the form view
vals = {'date': '2023-11-03', 'user_id': False, 'employee_id': self.env['hr.employee'].create({'user_id': self.env.uid}).id, 'name': False,
'unit_amount': 0, 'project_id': self.project.id, 'task_id': False, 'helpdesk_ticket_id': helpdesk_ticket.id}
timesheet = self.env['account.analytic.line'].create([vals])
self.assertEqual(timesheet.company_id, new_company, 'The expected company of the timesheet is the company from the project of its ticket')
# Test to ensure provided company_id is not overridden
vals.update({'company_id': new_company.id, 'date': '2023-11-04', 'user_id': self.env.uid})
vals.pop('employee_id')
timesheet = self.env['account.analytic.line'].create([vals])
self.assertEqual(timesheet.company_id, new_company, 'The expected company of the timesheet is the company from the project of its ticket')
def test_launch_the_timer_for_a_ticket(self):
""" Test if starting and stopping the timer on a timesheet entry linked to a helpdesk ticket creates a new entry
with the ticket_id set
"""
yesterday = date.today() - timedelta(days=1)
timesheet = self.env['account.analytic.line'].with_user(self.user_employee).create({
'name': 'Yesterday Timesheet',
'project_id': self.project.id,
'date': yesterday,
'employee_id': self.empl_employee.id,
'helpdesk_ticket_id': self.helpdesk_ticket.id,
})
timesheet.with_user(self.user_employee).action_timer_start()
timesheet.with_user(self.user_employee).action_timer_stop()
timesheet_count = self.env['account.analytic.line'].search_count([
('date', '=', date.today()),
('helpdesk_ticket_id', '=', self.helpdesk_ticket.id)
], limit=1)
self.assertEqual(timesheet_count, 1, "The new timesheet entry's ticket_id should be set correctly.")
def test_create_separate_timesheet_entries_depending_on_ticket_id(self):
ticket_copy = self.helpdesk_ticket.sudo().copy()
timesheet_1, timesheet_2, timesheet_3 = self.env['account.analytic.line'].with_user(self.user_employee).create([{
'name': '/',
'project_id': self.project.id,
'date': date.today(),
'helpdesk_ticket_id': self.helpdesk_ticket.id,
}, {
'name': '/',
'project_id': self.project.id,
'date': date.today(),
'helpdesk_ticket_id': ticket_copy.id,
}, {
'name': '/',
'project_id': self.project.id,
'date': date.today(),
'helpdesk_ticket_id': ticket_copy.id,
}])
timesheet_1.sudo()._add_timesheet_time(15, True)
timesheet_2.sudo()._add_timesheet_time(15, True)
timesheet_3.sudo()._add_timesheet_time(15, True)
# timesheets 2 and 3 should be merged since they belong to the same ticket, timesheet 1 must be kept separate
self.assertTrue(timesheet_1.exists(), 'Timesheet 1 should not have been merged')
self.assertFalse(timesheet_2.exists(), 'Timesheet 2 should have been merged into 3')
self.assertTrue(timesheet_3.exists(), 'Timesheet 2 should have been merged into 3')
self.assertAlmostEqual(timesheet_1.unit_amount, 0.25, 2)
self.assertAlmostEqual(timesheet_3.unit_amount, 0.5, 2)
def test_change_helpdesk_team_on_ticket_with_timer_running(self):
""" Test that changing the helpdesk team of a ticket with a running timer does not stop the timer """
timesheet = self.env['account.analytic.line'].with_user(self.user_employee).create({
'name': '/',
'project_id': self.project.id,
'date': date.today(),
'helpdesk_ticket_id': self.helpdesk_ticket.id,
})
timesheet.action_timer_start()
helpdesk_team_with_timesheet, helpdesk_team_without_timesheet = self.env['helpdesk.team'].create([{
'name': 'Helpdesk with timesheet',
'use_helpdesk_timesheet': True,
'project_id': self.project.id,
}, {
'name': 'Helpdesk without timesheet',
'use_helpdesk_timesheet': False,
}])
self.helpdesk_ticket.team_id = helpdesk_team_with_timesheet
self.assertTrue(timesheet.is_timer_running, 'The timer should still be running after changing the helpdesk team')
self.assertEqual(timesheet.user_timer_id.parent_res_model, 'helpdesk.ticket', 'The timer should still be linked to the ticket')
self.assertEqual(timesheet.user_timer_id.parent_res_id, self.helpdesk_ticket.id, 'The timer should still be linked to the ticket')
ticket = self.helpdesk_ticket.with_user(self.user_employee)
self.assertTrue(ticket.is_timer_running)
ticket.team_id = helpdesk_team_without_timesheet
self.assertFalse(ticket.is_timer_running, 'The timer should have been stopped after changing the helpdesk team since the timesheet feature is disabled on that team')
self.assertFalse(timesheet.exists(), 'The timesheet should have been deleted after the timer was stopped since the unit_amount is 0')
def test_total_hours_spent(self):
""" Test the total_hours_spent field is correctly computed """
# create 5 tickets with a timesheet each of 10 minutes
tickets = self.env['helpdesk.ticket'].create([{
'name': f'Ticket {i}',
'team_id': self.helpdesk_team.id,
'partner_id': self.partner.id,
} for i in range(5)])
for ticket in tickets:
self.env['account.analytic.line'].create({
'name': 'Test Timesheet',
'unit_amount': 1 / 6, # 10 minutes in hours
'project_id': self.project.id,
'helpdesk_ticket_id': ticket.id,
'employee_id': self.empl_employee.id,
})
# check sum of total_hours_spent is 5/6 hours
total_hours_spent = sum(ticket.total_hours_spent for ticket in tickets)
self.assertAlmostEqual(total_hours_spent, 5 / 6, places=2,
msg="The total hours spent across all tickets should be 5/6 hours (50 minutes).")
def test_timesheet_value_conversion_hours_days(self):
"""
Ensure that the timesheet value is correctly converted when changing
the encoding unit from hours to days and vice versa.
"""
helpdesk_ticket = self.helpdesk_ticket
self.env['account.analytic.line'].create({
'name': 'Test Timesheet',
'unit_amount': 8,
'helpdesk_ticket_id': helpdesk_ticket.id,
'employee_id': self.empl_employee.id,
})
self.env.company.timesheet_encode_uom_id = self.env.ref('uom.product_uom_day')
self.assertEqual(helpdesk_ticket.team_id.total_timesheet_time, 1, "Total timesheet time should be 1 day")