Files
enterprise-addons/helpdesk/tests/test_helpdesk_sla.py
T

329 lines
17 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from contextlib import contextmanager
from unittest.mock import patch
from dateutil.relativedelta import relativedelta
from datetime import datetime
from freezegun import freeze_time
from odoo import fields, Command
from odoo.tests.common import TransactionCase
from odoo.tests import Form
NOW = datetime(2018, 10, 10, 9, 18)
NOW2 = datetime(2019, 1, 8, 9, 0)
class HelpdeskSLA(TransactionCase):
@classmethod
def setUpClass(cls):
super(HelpdeskSLA, cls).setUpClass()
cls.env.company.resource_calendar_id.tz = "Europe/Brussels"
# we create a helpdesk user and a manager
Users = cls.env['res.users'].with_context(tracking_disable=True)
cls.main_company_id = cls.env.ref('base.main_company').id
cls.helpdesk_manager = Users.create({
'company_id': cls.main_company_id,
'name': 'Helpdesk Manager',
'login': 'hm',
'email': 'hm@example.com',
'group_ids': [(6, 0, [cls.env.ref('helpdesk.group_helpdesk_manager').id])]
})
cls.helpdesk_user = Users.create({
'company_id': cls.main_company_id,
'name': 'Helpdesk User',
'login': 'hu',
'email': 'hu@example.com',
'group_ids': [(6, 0, [cls.env.ref('helpdesk.group_helpdesk_user').id])]
})
# the manager defines three teams for our tests (the .sudo() at the end is to avoid potential uid problems)
teams = cls.env['helpdesk.team'].with_user(cls.helpdesk_manager).create([
{
'name': 'Test Team SLA Reached',
'use_sla': True,
},
{
'name': 'Test Team SLA Late',
'use_sla': True,
},
{
'name': 'Test Team No Tickets',
'use_sla': True,
},
]).sudo()
cls.test_team_reached = teams[0]
cls.test_team_late = teams[1]
cls.test_team_no_tickets = teams[2]
# He then defines the stages
stage_as_manager = cls.env['helpdesk.stage'].with_user(cls.helpdesk_manager)
cls.stage_new = stage_as_manager.create({
'name': 'New',
'sequence': 10,
'team_ids': [(6, 0, (cls.test_team_reached.id, cls.test_team_late.id))],
})
cls.stage_progress = stage_as_manager.create({
'name': 'In Progress',
'sequence': 20,
'team_ids': [(6, 0, (cls.test_team_reached.id, cls.test_team_late.id))],
})
cls.stage_wait = stage_as_manager.create({
'name': 'Waiting',
'sequence': 25,
'team_ids': [(6, 0, (cls.test_team_reached.id, cls.test_team_late.id))],
})
cls.stage_done = stage_as_manager.create({
'name': 'Done',
'sequence': 30,
'team_ids': [(6, 0, (cls.test_team_reached.id, cls.test_team_late.id))],
'fold': True,
})
cls.stage_cancel = stage_as_manager.create({
'name': 'Cancelled',
'sequence': 40,
'team_ids': [(6, 0, (cls.test_team_reached.id, cls.test_team_late.id))],
'fold': True,
})
cls.tag_vip = cls.env['helpdesk.tag'].with_user(cls.helpdesk_manager).create({'name': 'VIP'})
cls.tag_urgent = cls.env['helpdesk.tag'].with_user(cls.helpdesk_manager).create({'name': 'Urgent'})
cls.tag_freeze = cls.env['helpdesk.tag'].with_user(cls.helpdesk_manager).create({'name': 'Freeze'})
cls.sla = cls.env['helpdesk.sla'].create({
'name': 'SLA',
'team_id': cls.test_team_reached.id,
'time': 32,
'stage_id': cls.stage_progress.id,
'priority': '1',
})
cls.sla_2 = cls.env['helpdesk.sla'].create({
'name': 'SLA done stage with freeze time',
'team_id': cls.test_team_reached.id,
'time': 10.033333333333333,
'tag_ids': [(4, cls.tag_freeze.id)],
'exclude_stage_ids': cls.stage_wait.ids,
'stage_id': cls.stage_done.id,
'priority': '1',
})
cls.sla_3 = cls.env['helpdesk.sla'].create({
'name': 'SLA Team 2',
'team_id': cls.test_team_late.id,
'time': 16,
'stage_id': cls.stage_progress.id,
'priority': '1',
})
@contextmanager
def _ticket_patch_now(self, datetime):
with freeze_time(datetime), patch.object(self.env.cr, 'now', lambda: datetime):
yield
self.env.flush_all()
def create_ticket(self, team, *arg, **kwargs):
default_values = {
'name': "Help me",
'team_id': team.id,
'tag_ids': [(4, self.tag_urgent.id)],
'stage_id': self.stage_new.id,
'priority': '1',
}
if 'tag_ids' in kwargs:
# from recordset to ORM command
kwargs['tag_ids'] = [(6, False, [tag.id for tag in kwargs['tag_ids']])]
values = dict(default_values, **kwargs)
return self.env['helpdesk.ticket'].create(values)
def test_sla_no_tag(self):
""" SLA without tag should apply to all tickets """
self.sla.tag_ids = [(5,)]
ticket = self.create_ticket(tag_ids=self.tag_urgent, team=self.test_team_reached)
self.assertEqual(ticket.sla_status_ids.sla_id, self.sla, "SLA should have been applied")
def test_sla_single_tag(self):
self.sla.tag_ids = [(4, self.tag_urgent.id)]
ticket = self.create_ticket(tag_ids=self.tag_urgent, team=self.test_team_reached)
self.assertEqual(ticket.sla_status_ids.sla_id, self.sla, "SLA should have been applied")
def test_sla_multiple_tags(self):
self.sla.tag_ids = [(6, False, (self.tag_urgent | self.tag_vip).ids)]
ticket = self.create_ticket(tag_ids=self.tag_urgent, team=self.test_team_reached)
self.assertEqual(ticket.sla_status_ids.sla_id, self.sla, "SLA should have been applied when atleast one tag set on ticket from sla policy")
ticket.tag_ids = [(4, self.tag_vip.id)]
self.assertEqual(ticket.sla_status_ids.sla_id, self.sla, "SLA should have been applied")
def test_sla_tag(self):
self.sla.tag_ids = [(6, False, self.tag_urgent.ids)]
ticket = self.create_ticket(tag_ids=self.tag_urgent, team=self.test_team_reached)
self.assertEqual(ticket.sla_status_ids.sla_id, self.sla, "SLA should have been applied")
def test_sla_remove_tag(self):
self.sla.tag_ids = [(6, False, (self.tag_urgent | self.tag_vip).ids)]
ticket = self.create_ticket(tag_ids=self.tag_urgent | self.tag_vip, team=self.test_team_reached)
self.assertEqual(ticket.sla_status_ids.sla_id, self.sla, "SLA should have been applied")
ticket.tag_ids = [(5,)] # Remove all tags
self.assertFalse(ticket.sla_status_ids, "SLA should no longer apply")
def test_sla_waiting(self):
with self._ticket_patch_now(NOW2):
ticket = self.create_ticket(tag_ids=self.tag_freeze, team=self.test_team_reached)
status = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_2.id)
self.assertEqual(status.deadline, datetime(2019, 1, 9, 12, 2, 0), 'No waiting time, deadline = creation date + 1 day + 2 hours + 2 minutes')
with self._ticket_patch_now('2019-01-08 11:09:50'):
ticket.write({'stage_id': self.stage_progress.id})
initial_values = {ticket.id: {'stage_id': self.stage_new}}
ticket._message_track(['stage_id'], initial_values)
self.assertEqual(status.deadline, datetime(2019, 1, 9, 12, 2, 0), 'No waiting time, deadline = creation date + 1 day + 2 hours + 2 minutes')
# We are in waiting stage, they are no more deadline.
with self._ticket_patch_now('2019-01-08 12:15:00'):
ticket.write({'stage_id': self.stage_wait.id})
initial_values = {ticket.id: {'stage_id': self.stage_progress}}
ticket._message_track(['stage_id'], initial_values)
self.assertFalse(status.deadline, 'In waiting stage: no more deadline')
# We have a response of our customer, the ticket switch to in progress stage (outside working hours)
with self._ticket_patch_now('2019-01-12 10:35:58'):
ticket.write({'stage_id': self.stage_progress.id})
initial_values = {ticket.id: {'stage_id': self.stage_wait}}
ticket._message_track(['stage_id'], initial_values)
# waiting time = 3 full working days 9 - 10 - 11 January (12 doesn't count as it's Saturday)
# + (8 January) 12:15:00 -> 16:00:00 (end of working day) 3,75 hours
# Old deadline = '2019-01-09 12:02:00'
# New: '2019-01-09 12:02:00' + 3 days (waiting) + 2 days (weekend) + 3.75 hours (waiting) = '2019-01-14 15:47:00'
self.assertEqual(status.deadline, datetime(2019, 1, 14, 15, 47), 'We have waiting time: deadline = old_deadline + 3 full working days (waiting) + 3.75 hours (waiting) + 2 days (weekend)')
with self._ticket_patch_now('2019-01-14 15:30:00'):
ticket.write({'stage_id': self.stage_wait.id})
initial_values = {ticket.id: {'stage_id': self.stage_progress}}
ticket._message_track(['stage_id'], initial_values)
self.assertFalse(status.deadline, 'In waiting stage: no more deadline')
# We need to patch now with a new value as it will be used to compute freezed time.
with self._ticket_patch_now('2019-01-16 15:00:00'):
ticket.write({'stage_id': self.stage_done.id})
initial_values = {ticket.id: {'stage_id': self.stage_wait}}
ticket._message_track(['stage_id'], initial_values)
self.assertEqual(status.deadline, datetime(2019, 1, 16, 15, 17), 'We have waiting time: deadline = old_deadline + 7.5 hours (waiting)')
def test_failed_tickets(self):
with self._ticket_patch_now(NOW):
self.sla.time = 3
# Failed ticket
self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id, create_date=NOW - relativedelta(hours=3, minutes=2))
# Not failed ticket
self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id, create_date=NOW - relativedelta(hours=2, minutes=2))
data = self.env['helpdesk.team'].retrieve_dashboard()
self.assertEqual(data['my_all']['count'], 2, "There should be 2 tickets")
self.assertEqual(data['my_all']['failed'], 1, "There should be 1 failed ticket")
def test_deadlines_after_work(self):
with self._ticket_patch_now(NOW + relativedelta(hour=20, minute=0)):
self.sla.time = 3
# Set the calendar tz to UTC in order to ease test comprehension
self.sla.company_id.resource_calendar_id.tz = 'UTC'
ticket = self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id)
# We set ticket create date to 20:00 which is out of the working calendar => The first possible time to work
# on the ticket is the next day at 08:00
self.assertEqual(ticket.sla_deadline, fields.Datetime.now() + relativedelta(days=1, hour=11), "Day0:20h + 3h = Day1:8h + 3h = Day1:11h")
self.sla.exclude_stage_ids = [Command.link(self.stage_wait.id)] # same test as above, but the sla has excluded stages
ticket = self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id)
self.assertEqual(ticket.sla_deadline, fields.Datetime.now() + relativedelta(days=1, hour=11), "Day0:20h + 3h = Day1:8h + 3h = Day1:11h")
self.sla.exclude_stage_ids = [Command.clear()]
self.sla.time = 11
ticket = self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id)
self.assertEqual(ticket.sla_deadline, fields.Datetime.now() + relativedelta(days=2, hour=11), "Day0:20h + 11h = Day0:20h + 1day:3h = Day1:8h + 1day:3h = Day2:8h + 3h = Day2:11h")
def test_deadlines_during_work(self):
with self._ticket_patch_now(NOW + relativedelta(hour=8, minute=0)):
self.sla.time = 3
# Set the calendar tz to UTC in order to ease test comprehension
self.sla.company_id.resource_calendar_id.tz = 'UTC'
ticket = self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id)
# We set ticket create date to 20:00 which is out of the working calendar => The first possible time to work
# on the ticket is the next day at 08:00
self.assertEqual(ticket.sla_deadline, fields.Datetime.now() + relativedelta(days=0, hour=11), "Day0:8h + 3h = Day0:11h")
self.sla.time = 11
ticket = self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id)
self.assertEqual(ticket.sla_deadline, fields.Datetime.now() + relativedelta(days=1, hour=11), "Day0:8h + 11h = Day0:8h + 1day:3h = Day1:8h + 3h = Day1:11h")
def test_teams_success_rate(self):
# Create 6 tickets, 3 on-time according to SLA, 3 late.
with self._ticket_patch_now(NOW):
tickets_reached = self.env['helpdesk.ticket'].concat(*[self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id) for _ in range(3)])
tickets_late = self.env['helpdesk.ticket'].concat(*[self.create_ticket(team=self.test_team_late, user_id=self.env.user.id) for _ in range(3)])
tickets = tickets_reached + tickets_late
# Move tickets in-progress 5 days after creation date.
with self._ticket_patch_now(NOW + relativedelta(days=5)):
tickets.write({'stage_id': self.stage_progress.id})
initial_values = {ticket.id: {'stage_id': self.stage_new} for ticket in tickets}
tickets._message_track(['stage_id'], initial_values)
# Set tickets to done and check teams success rates.
with self._ticket_patch_now(NOW + relativedelta(days=6)):
tickets.write({'stage_id': self.stage_done.id})
initial_values = {ticket.id: {'stage_id': self.stage_progress} for ticket in tickets}
tickets._message_track(['stage_id'], initial_values)
tickets._compute_sla_success()
for ticket in tickets_reached:
self.assertTrue(ticket.sla_success, "Tickets Reached should have sla_success True")
for ticket in tickets_late:
self.assertFalse(ticket.sla_success, "Tickets (late) should have sla_success False")
# Sentinel check for no ticket team.
self.assertEqual(self.test_team_no_tickets.success_rate, -1.0, "Teams without tickets should have -1.0 sentinel success rate")
# Success rate checks
self.assertEqual(self.test_team_reached.success_rate, 100.0, "Team without late tickets should have 100.0 success rate")
self.assertEqual(self.test_team_late.success_rate, 0.0, "Team with only late tickets should have 0.0 success rate")
# Check that an SLA team that has closed tickets before, but hasn't
# closed any in the past 7 days has a -1 success rate. This test makes
# sure that the success rate is correctly only meaningful for the past
# 7 days.
with self._ticket_patch_now(NOW + relativedelta(days=14)):
# Need to manually recompute the field because the value is stored
# in memory and not recomputed automatically.
self.test_team_reached._compute_success_rate()
self.assertEqual(self.test_team_reached.success_rate, -1,
"Team with no tickets closed in the past 7 days should have a -1 success rate"
)
def test_move_ticket_to_done_and_cancel_with_disabled_sla_and_no_calendar(self):
""" Test moving a ticket does not cause an exception when SLAs are disabled when the calendar is empty. """
# Create a ticket on a team with SLAs enabled
ticket = self.create_ticket(team=self.test_team_reached)
# remove calendar from team
self.test_team_reached.write({
'resource_calendar_id': False,
'use_sla': False,
})
# Move the ticket to the 'Done' stage and verify it does not crash
with Form(ticket) as ticket_form:
ticket_form.stage_id = self.stage_done
self.assertEqual(ticket.stage_id, self.stage_done)
# Move the ticket to the 'Cancelled' stage and verify it does not crash
with Form(ticket) as ticket_form:
ticket_form.stage_id = self.stage_cancel
self.assertEqual(ticket.stage_id, self.stage_cancel)
def test_sla_reached(self):
""" Ensure sla_reached is computed corectly"""
with self._ticket_patch_now(NOW):
ticket = self.create_ticket(team=self.test_team_reached, user_id=self.env.user.id)
self.assertFalse(ticket.sla_reached, "Newly created ticket should not be sla_reached")
with self._ticket_patch_now(NOW + relativedelta(days=10)):
ticket.write({'stage_id': self.stage_progress.id})
initial_values = {ticket.id: {'stage_id': self.stage_new}}
ticket._message_track(['stage_id'], initial_values)
self.assertTrue(ticket.sla_reached, "Ticket that reached target stage late should still be sla_reached")
self.assertTrue(ticket.sla_reached_late, "Should also be flagged late")