odoo18/addons_extensions/helpdesk/models/helpdesk_sla_status.py

144 lines
8.0 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import math
from odoo import fields, models, api
from odoo.osv import expression
class HelpdeskSLAStatus(models.Model):
_name = 'helpdesk.sla.status'
_description = "Ticket SLA Status"
_table = 'helpdesk_sla_status'
_order = 'deadline ASC, sla_stage_id'
_rec_name = 'sla_id'
ticket_id = fields.Many2one('helpdesk.ticket', string='Ticket', required=True, ondelete='cascade', index=True)
sla_id = fields.Many2one('helpdesk.sla', required=True, ondelete='cascade')
sla_stage_id = fields.Many2one('helpdesk.stage', related='sla_id.stage_id', store=True, export_string_translation=False) # need to be stored for the search in `_sla_reach`
deadline = fields.Datetime("Deadline", compute='_compute_deadline', compute_sudo=True, store=True)
reached_datetime = fields.Datetime("Reached Date", help="Datetime at which the SLA stage was reached for the first time")
status = fields.Selection([('failed', 'Failed'), ('reached', 'Reached'), ('ongoing', 'Ongoing')], string="Status", compute='_compute_status', compute_sudo=True, search='_search_status')
color = fields.Integer("Color Index", compute='_compute_color')
exceeded_hours = fields.Float("Exceeded Working Hours", compute='_compute_exceeded_hours', compute_sudo=True, store=True, help="Working hours exceeded for reached SLAs compared with deadline. Positive number means the SLA was reached after the deadline.")
@api.depends('ticket_id.create_date', 'sla_id', 'ticket_id.stage_id')
def _compute_deadline(self):
for status in self:
if (status.deadline and status.reached_datetime) or (status.deadline and not status.sla_id.exclude_stage_ids) or (status.status == 'failed'):
continue
deadline = status.ticket_id.create_date
working_calendar = status.ticket_id.team_id.resource_calendar_id
if not working_calendar:
# Normally, having a working_calendar is mandatory
status.deadline = deadline
continue
if status.sla_id.exclude_stage_ids:
if status.ticket_id.stage_id in status.sla_id.exclude_stage_ids:
# We are in the freezed time stage: No deadline
status.deadline = False
continue
avg_hour = working_calendar.hours_per_day or 8 # default to 8 working hours/day
time_days = math.floor(status.sla_id.time / avg_hour)
if time_days > 0:
deadline = working_calendar.plan_days(time_days + 1, deadline, compute_leaves=True)
# We should also depend on ticket creation time, otherwise for 1 day SLA, all tickets
# created on monday will have their deadline filled with tuesday 8:00
create_dt = working_calendar.plan_hours(0, status.ticket_id.create_date)
deadline = deadline and deadline.replace(hour=create_dt.hour, minute=create_dt.minute, second=create_dt.second, microsecond=create_dt.microsecond)
sla_hours = status.sla_id.time % avg_hour
if status.sla_id.exclude_stage_ids:
sla_hours += status._get_freezed_hours(working_calendar)
# Except if ticket creation time is later than the end time of the working day
deadline_for_working_cal = working_calendar.plan_hours(0, deadline)
if deadline_for_working_cal and deadline.day < deadline_for_working_cal.day and time_days > 0:
deadline = deadline.replace(hour=0, minute=0, second=0, microsecond=0)
# We should execute the function plan_hours in any case because, in a 1 day SLA environment,
# if I create a ticket knowing that I'm not working the day after at the same time, ticket
# deadline will be set at time I don't work (ticket creation time might not be in working calendar).
status.deadline = deadline and working_calendar.plan_hours(sla_hours, deadline, compute_leaves=True)
@api.depends('deadline', 'reached_datetime')
def _compute_status(self):
for status in self:
if status.reached_datetime and status.deadline: # if reached_datetime, SLA is finished: either failed or succeeded
status.status = 'reached' if status.reached_datetime < status.deadline else 'failed'
else: # if not finished, deadline should be compared to now()
status.status = 'ongoing' if not status.deadline or status.deadline > fields.Datetime.now() else 'failed'
@api.model
def _search_status(self, operator, value):
""" Supported operators: '=', 'in' and their negative form. """
# constants
datetime_now = fields.Datetime.now()
positive_domain = {
'failed': ['|', '&', ('reached_datetime', '=', True), ('deadline', '<=', 'reached_datetime'), '&', ('reached_datetime', '=', False), ('deadline', '<=', fields.Datetime.to_string(datetime_now))],
'reached': ['&', ('reached_datetime', '=', True), ('reached_datetime', '<', 'deadline')],
'ongoing': ['|', ('deadline', '=', False), '&', ('reached_datetime', '=', False), ('deadline', '>', fields.Datetime.to_string(datetime_now))]
}
# in/not in case: we treat value as a list of selection item
if not isinstance(value, list):
value = [value]
# transform domains
if operator in expression.NEGATIVE_TERM_OPERATORS:
# "('status', 'not in', [A, B])" tranformed into "('status', '=', C) OR ('status', '=', D)"
domains_to_keep = [dom for key, dom in positive_domain if key not in value]
return expression.OR(domains_to_keep)
else:
return expression.OR(positive_domain[value_item] for value_item in value)
@api.depends('status')
def _compute_color(self):
for status in self:
if status.status == 'failed':
status.color = 1
elif status.status == 'reached':
status.color = 10
else:
status.color = 0
@api.depends('deadline', 'reached_datetime')
def _compute_exceeded_hours(self):
for status in self:
if status.deadline and status.ticket_id.team_id.resource_calendar_id:
reached_datetime = status.reached_datetime or fields.Datetime.now()
if reached_datetime <= status.deadline:
start_dt = reached_datetime
end_dt = status.deadline
factor = -1
else:
start_dt = status.deadline
end_dt = reached_datetime
factor = 1
duration_data = status.ticket_id.team_id.resource_calendar_id.get_work_duration_data(start_dt, end_dt, compute_leaves=True)
status.exceeded_hours = duration_data['hours'] * factor
else:
status.exceeded_hours = False
def _get_freezed_hours(self, working_calendar):
self.ensure_one()
hours_freezed = 0
field_stage = self.env['ir.model.fields']._get(self.ticket_id._name, "stage_id")
freeze_stages = self.sla_id.exclude_stage_ids.ids
tracking_lines = self.ticket_id.message_ids.tracking_value_ids.filtered(lambda tv: tv.field_id == field_stage).sorted(key="create_date")
if not tracking_lines:
return 0
old_time = self.ticket_id.create_date
for tracking_line in tracking_lines:
if tracking_line.old_value_integer in freeze_stages:
# We must use get_work_hours_count to compute real waiting hours (as the deadline computation is also based on calendar)
hours_freezed += working_calendar.get_work_hours_count(old_time, tracking_line.create_date)
old_time = tracking_line.create_date
if tracking_lines[-1].new_value_integer in freeze_stages:
# the last tracking line is not yet created
hours_freezed += working_calendar.get_work_hours_count(old_time, fields.Datetime.now())
return hours_freezed