# -*- 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 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', 'groups_id': [(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', 'groups_id': [(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) # 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" )