19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:39 +01:00
parent 5df8c07b59
commit daa394e8b0
2114 changed files with 564841 additions and 299642 deletions

View file

@ -1,3 +1,2 @@
# -*- coding: utf-8 -*-
from . import im_livechat_report_channel
from . import im_livechat_report_operator

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Added in stable, its usage should always be covered by an existence check -->
<record id="action_report_livechat_conversation" model="ir.actions.report">
<field name="name">Live Chat Conversation</field>
<field name="model">discuss.channel</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">im_livechat.report_livechat_conversation</field>
<field name="report_file">im_livechat.report_livechat_conversation</field>
</record>
<!-- Added in stable, its usage should always be covered by an existence check -->
<template id="report_livechat_conversation">
<t t-call="web.basic_layout">
<t t-foreach="docs" t-as="doc">
<t t-set="channel" t-value="doc"/>
<t t-call="im_livechat.livechat_email_template"/>
</t>
</t>
</template>
</odoo>

View file

@ -1,100 +1,325 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools
from odoo import api, fields, models
from odoo.tools import get_lang, Query, SQL
class ImLivechatReportChannel(models.Model):
class Im_LivechatReportChannel(models.Model):
""" Livechat Support Report on the Channels """
_name = "im_livechat.report.channel"
_name = 'im_livechat.report.channel'
_description = "Livechat Support Channel Report"
_order = 'start_date, technical_name'
_order = 'start_date, livechat_channel_id, channel_id'
_auto = False
uuid = fields.Char('UUID', readonly=True)
channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True)
channel_id = fields.Many2one('discuss.channel', 'Conversation', readonly=True)
channel_name = fields.Char('Channel Name', readonly=True)
technical_name = fields.Char('Code', readonly=True)
livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True)
start_date = fields.Datetime('Start Date of session', readonly=True)
start_hour = fields.Char('Start Hour of session', readonly=True)
day_number = fields.Char('Day Number', readonly=True, help="1 is Monday, 7 is Sunday")
time_to_answer = fields.Float('Time to answer (sec)', digits=(16, 2), readonly=True, group_operator="avg", help="Average time in seconds to give the first answer to the visitor")
start_date_minutes = fields.Char("Start Date of session, truncated to minutes", readonly=True)
day_number = fields.Selection(
selection=[
("0", "Sunday"),
("1", "Monday"),
("2", "Tuesday"),
("3", "Wednesday"),
("4", "Thursday"),
("5", "Friday"),
("6", "Saturday"),
],
string="Day of the Week",
readonly=True,
)
time_to_answer = fields.Float("Response Time", digits=(16, 6), readonly=True, aggregator="avg", help="Average time in hours to give the first answer to the visitor")
start_date_hour = fields.Char('Hour of start Date of session', readonly=True)
duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)")
nbr_speaker = fields.Integer('# of speakers', readonly=True, group_operator="avg", help="Number of different speakers")
nbr_message = fields.Integer('Average message', readonly=True, group_operator="avg", help="Number of message in the conversation")
is_without_answer = fields.Integer('Session(s) without answer', readonly=True, group_operator="sum",
help="""A session is without answer if the operator did not answer.
If the visitor is also the operator, the session will always be answered.""")
days_of_activity = fields.Integer('Days of activity', group_operator="max", readonly=True, help="Number of days since the first session of the operator")
is_anonymous = fields.Integer('Is visitor anonymous', readonly=True)
duration = fields.Float("Duration (min)", digits=(16, 2), readonly=True, aggregator="avg", help="Duration of the conversation (in minutes)")
nbr_message = fields.Integer("Messages per Session", readonly=True, aggregator="avg", help="Number of message in the conversation")
country_id = fields.Many2one('res.country', 'Country of the visitor', readonly=True)
is_happy = fields.Integer('Visitor is Happy', readonly=True)
rating = fields.Integer('Rating', group_operator="avg", readonly=True)
lang_id = fields.Many2one("res.lang", related="channel_id.livechat_lang_id", string="Language", readonly=True)
rating = fields.Integer('Rating', aggregator="avg", readonly=True)
# TODO DBE : Use Selection field - Need : Pie chart must show labels, not keys.
rating_text = fields.Char('Satisfaction Rate', readonly=True)
is_unrated = fields.Integer('Session not rated', readonly=True)
partner_id = fields.Many2one('res.partner', 'Operator', readonly=True)
partner_id = fields.Many2one("res.partner", "Agent", readonly=True)
handled_by_bot = fields.Integer("Handled by Bot", readonly=True, aggregator="sum")
handled_by_agent = fields.Integer("Handled by Agent", readonly=True, aggregator="sum")
visitor_partner_id = fields.Many2one("res.partner", string="Customer", readonly=True)
call_duration_hour = fields.Float("Call Duration", digits=(16, 2), readonly=True, aggregator="avg")
has_call = fields.Float("Whether the session had a call", readonly=True)
number_of_calls = fields.Float("# of Sessions with calls", readonly=True, related="has_call", aggregator="sum")
percentage_of_calls = fields.Float("Session with Calls (%)", readonly=True, related="has_call", aggregator="avg")
session_outcome = fields.Selection(
selection=[
("no_answer", "Never Answered"),
("no_agent", "No one Available"),
("no_failure", "Success"),
("escalated", "Escalated"),
],
string="Session Outcome",
readonly=True,
)
chatbot_script_id = fields.Many2one("chatbot.script", "Chatbot", readonly=True)
chatbot_answers_path = fields.Char("Chatbot Answers", readonly=True)
chatbot_answers_path_str = fields.Char("Chatbot Answers (String)", readonly=True)
session_expertises = fields.Char("Expertises used in this session (String)", readonly=True)
session_expertise_ids = fields.Many2many(
"im_livechat.expertise",
readonly=True,
related="channel_id.livechat_expertise_ids",
string="Expertises used in this session",
)
conversation_tag_ids = fields.Many2many(
"im_livechat.conversation.tag",
readonly=True,
related="channel_id.livechat_conversation_tag_ids",
string="Tags used in this conversation",
)
agent_requesting_help_history = fields.Many2one(
"im_livechat.channel.member.history",
related="channel_id.livechat_agent_requesting_help_history",
readonly=True,
)
agent_providing_help_history = fields.Many2one(
"im_livechat.channel.member.history",
related="channel_id.livechat_agent_providing_help_history",
readonly=True,
)
def init(self):
# Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view !
tools.drop_view_if_exists(self.env.cr, 'im_livechat_report_channel')
self.env.cr.execute("""
CREATE OR REPLACE VIEW im_livechat_report_channel AS (
SELECT
C.id as id,
C.uuid as uuid,
C.id as channel_id,
C.name as channel_name,
CONCAT(L.name, ' / ', C.id) as technical_name,
C.livechat_channel_id as livechat_channel_id,
C.create_date as start_date,
to_char(date_trunc('hour', C.create_date), 'YYYY-MM-DD HH24:MI:SS') as start_date_hour,
to_char(date_trunc('hour', C.create_date), 'HH24') as start_hour,
extract(dow from C.create_date) as day_number,
EXTRACT('epoch' FROM MAX(M.create_date) - MIN(M.create_date)) AS duration,
EXTRACT('epoch' FROM MIN(MO.create_date) - MIN(M.create_date)) AS time_to_answer,
count(distinct C.livechat_operator_id) as nbr_speaker,
count(distinct M.id) as nbr_message,
CASE
WHEN EXISTS (select distinct M.author_id FROM mail_message M
WHERE M.author_id=C.livechat_operator_id
AND M.res_id = C.id
AND M.model = 'mail.channel'
AND C.livechat_operator_id = M.author_id)
THEN 0
ELSE 1
END as is_without_answer,
(DATE_PART('day', date_trunc('day', now()) - date_trunc('day', C.create_date)) + 1) as days_of_activity,
CASE
WHEN C.anonymous_name IS NULL THEN 0
ELSE 1
END as is_anonymous,
C.country_id,
CASE
WHEN rate.rating = 5 THEN 1
ELSE 0
END as is_happy,
Rate.rating as rating,
CASE
WHEN Rate.rating = 1 THEN 'Unhappy'
WHEN Rate.rating = 5 THEN 'Happy'
WHEN Rate.rating = 3 THEN 'Neutral'
ELSE null
END as rating_text,
CASE
WHEN rate.rating > 0 THEN 0
ELSE 1
END as is_unrated,
C.livechat_operator_id as partner_id
FROM mail_channel C
JOIN mail_message M ON (M.res_id = C.id AND M.model = 'mail.channel')
JOIN im_livechat_channel L ON (L.id = C.livechat_channel_id)
LEFT JOIN mail_message MO ON (MO.res_id = C.id AND MO.model = 'mail.channel' AND MO.author_id = C.livechat_operator_id)
LEFT JOIN rating_rating Rate ON (Rate.res_id = C.id and Rate.res_model = 'mail.channel' and Rate.parent_res_model = 'im_livechat.channel')
WHERE C.livechat_operator_id is not null
GROUP BY C.livechat_operator_id, C.id, C.name, C.livechat_channel_id, L.name, C.create_date, C.uuid, Rate.rating
@property
def _unknown_chatbot_answer_name(self):
return self.env._("Unknown")
@property
def _table_query(self):
return SQL("%s %s %s", self._select(), self._from(), self._where())
def _select(self) -> SQL:
return SQL(
"""
SELECT
C.id as id,
C.uuid as uuid,
C.id as channel_id,
C.name as channel_name,
C.livechat_channel_id as livechat_channel_id,
C.create_date as start_date,
channel_member_history.visitor_partner_id AS visitor_partner_id,
to_char(date_trunc('hour', C.create_date), 'YYYY-MM-DD HH24:MI:SS') as start_date_hour,
to_char(date_trunc('hour', C.create_date), 'HH24') as start_hour,
to_char(date_trunc('minute', C.create_date), 'YYYY-MM-DD HH:MI:SS') AS start_date_minutes,
EXTRACT(dow from C.create_date)::text AS day_number,
EXTRACT('epoch' FROM COALESCE(C.livechat_end_dt, NOW() AT TIME ZONE 'utc') - C.create_date)/60 AS duration,
CASE
WHEN C.livechat_end_dt IS NOT NULL
AND channel_member_history.has_agent
AND message_vals.first_agent_message_dt > C.livechat_end_dt THEN NULL
WHEN C.livechat_end_dt IS NOT NULL
AND NOT channel_member_history.has_agent
AND message_vals.first_agent_message_dt_legacy > C.livechat_end_dt THEN NULL
WHEN channel_member_history.has_agent AND channel_member_history.has_bot THEN
EXTRACT('epoch' FROM message_vals.first_agent_message_dt - message_vals.last_bot_message_dt)
WHEN channel_member_history.has_agent THEN
EXTRACT('epoch' FROM message_vals.first_agent_message_dt - c.create_date)
ELSE
EXTRACT('epoch' FROM message_vals.first_agent_message_dt_legacy - c.create_date)
END/3600 AS time_to_answer,
message_vals.message_count as nbr_message,
CASE
WHEN C.livechat_is_escalated THEN 'escalated'
ELSE C.livechat_failure
END AS session_outcome,
C.country_id,
NULLIF(C.rating_last_value, 0) AS rating,
CASE
WHEN C.rating_last_value = 1 THEN 'Unhappy'
WHEN C.rating_last_value = 5 THEN 'Happy'
WHEN C.rating_last_value = 3 THEN 'Neutral'
ELSE null
END as rating_text,
C.livechat_operator_id as partner_id,
CASE WHEN channel_member_history.has_agent THEN 1 ELSE 0 END as handled_by_agent,
CASE WHEN channel_member_history.has_bot and not channel_member_history.has_agent THEN 1 ELSE 0 END as handled_by_bot,
CASE WHEN channel_member_history.chatbot_script_id IS NOT NULL AND NOT channel_member_history.has_agent THEN channel_member_history.chatbot_script_id ELSE NULL END AS chatbot_script_id,
CASE WHEN call_history_data.call_duration_hour IS NOT NULL THEN 1 ELSE 0 END AS has_call,
call_history_data.call_duration_hour,
chatbot_answer_history.chatbot_answers_path,
chatbot_answer_history.chatbot_answers_path_str,
expertise_history.expertises session_expertises
""",
)
def _from(self) -> SQL:
return SQL(
"""
FROM discuss_channel C
LEFT JOIN LATERAL (
SELECT BOOL_OR(livechat_member_type = 'agent') AS has_agent,
BOOL_OR(livechat_member_type = 'bot') AS has_bot,
MIN(CASE WHEN livechat_member_type = 'visitor' THEN partner_id END) AS visitor_partner_id,
MIN(chatbot_script_id) AS chatbot_script_id
FROM im_livechat_channel_member_history
WHERE channel_id = C.id
) AS channel_member_history ON TRUE
LEFT JOIN LATERAL
(
SELECT SUM(
CASE
WHEN discuss_call_history.end_dt IS NOT NULL
THEN EXTRACT(EPOCH FROM discuss_call_history.end_dt - discuss_call_history.start_dt) / 3600
END
) AS call_duration_hour
FROM discuss_call_history
WHERE discuss_call_history.channel_id = C.id
) AS call_history_data ON TRUE
LEFT JOIN LATERAL
(
SELECT STRING_AGG(chatbot_message.user_raw_script_answer_id::TEXT, ' - ' ORDER BY chatbot_message.id) AS chatbot_answers_path,
STRING_AGG(
COALESCE(
chatbot_script_answer.name->>%s,
chatbot_script_answer.name->>'en_US',
fallback.value,
%s
),
' - ' ORDER BY chatbot_message.id
) AS chatbot_answers_path_str
FROM chatbot_message
LEFT JOIN chatbot_script_answer
ON chatbot_message.user_raw_script_answer_id = chatbot_script_answer.id
LEFT JOIN LATERAL
(
SELECT value
FROM jsonb_each_text(chatbot_script_answer.name)
LIMIT 1
) AS fallback ON TRUE
WHERE chatbot_message.user_raw_script_answer_id IS NOT NULL
AND chatbot_message.discuss_channel_id = C.id
) AS chatbot_answer_history ON TRUE
LEFT JOIN LATERAL
(
SELECT STRING_AGG(
COALESCE(
im_livechat_expertise.name->>%s,
im_livechat_expertise.name->>'en_US',
fallback.value
),
' - ' ORDER BY im_livechat_expertise.id
) AS expertises
FROM im_livechat_channel_member_history_im_livechat_expertise_rel REL
JOIN im_livechat_expertise
ON im_livechat_expertise.id = REL.im_livechat_expertise_id
JOIN im_livechat_channel_member_history
ON im_livechat_channel_member_history.id = REL.im_livechat_channel_member_history_id
JOIN LATERAL
(
SELECT value
FROM jsonb_each_text(im_livechat_expertise.name)
LIMIT 1
) AS fallback ON TRUE
WHERE im_livechat_channel_member_history.channel_id = C.id
) AS expertise_history ON TRUE
LEFT JOIN LATERAL
(
SELECT COUNT(DISTINCT M.id) AS message_count,
MIN(CASE WHEN H.livechat_member_type = 'agent' THEN M.create_date END) AS first_agent_message_dt,
MAX(CASE WHEN H.livechat_member_type = 'bot' THEN M.create_date END) AS last_bot_message_dt,
MIN(CASE WHEN M.author_id = C.livechat_operator_id THEN M.create_date END) AS first_agent_message_dt_legacy
FROM mail_message M
LEFT JOIN im_livechat_channel_member_history H on H.channel_id = M.res_id AND (M.author_id = H.partner_id OR M.author_guest_id = H.guest_id)
WHERE M.res_id = C.id and M.model = 'discuss.channel'
) AS message_vals ON TRUE
""",
self.env.lang,
self._unknown_chatbot_answer_name,
self.env.lang,
)
def _where(self) -> SQL:
return SQL("WHERE C.channel_type = 'livechat'")
@api.model
def formatted_read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None):
# Update chatbot_answers_path label: ids are used for grouping but names
# should be displayed.
result = super().formatted_read_group(
domain, groupby, aggregates, having=having, offset=offset, limit=limit, order=order
)
answer_ids = {
int(answer_id.strip())
for entry in result
if entry.get("chatbot_answers_path")
for answer_id in entry["chatbot_answers_path"].split("-")
}
answer_name_by_id = {
answer.id: answer.name
for answer in self.env["chatbot.script.answer"].search_fetch(
[("id", "in", answer_ids)],
["name"],
)
""")
}
for entry in result:
if not (path := entry.get("chatbot_answers_path")):
continue
id_list = [int(answer_id.strip()) for answer_id in path.split("-")]
entry["chatbot_answers_path"] = " - ".join(
answer_name_by_id.get(answer_id, self._unknown_chatbot_answer_name)
for answer_id in id_list
)
return result
def _read_group_orderby(self, order: str, groupby_terms: dict[str, SQL], query: Query) -> SQL:
if "day_number" not in groupby_terms:
return super()._read_group_orderby(order, groupby_terms, query)
if not order:
order = ",".join(groupby_terms)
order_parts = [part.strip() for part in order.split(",")]
day_number_part = next((part for part in order_parts if part.startswith("day_number")), None)
other_parts = [part for part in order_parts if not part.startswith("day_number")]
other_groupby_terms = {k: v for k, v in groupby_terms.items() if k != "day_number"}
other_order = ",".join(other_parts) if other_parts else None
other_orderby = None
if other_order or other_groupby_terms:
other_orderby = super()._read_group_orderby(other_order, other_groupby_terms, query)
groupby_terms.update(other_groupby_terms)
if query._order_groupby:
groupby_terms["day_number"] = SQL(", ").join([groupby_terms["day_number"], *query._order_groupby])
query._order_groupby.clear()
if not day_number_part:
return other_orderby
parts = [p.upper() for p in day_number_part.split()]
direction = next((p for p in parts if p in ("ASC", "DESC")), "ASC")
nulls_parts = [p for p in parts if p in ("NULLS", "FIRST", "LAST")]
sql_direction = SQL("ASC") if direction == "ASC" else SQL("DESC")
if "NULLS" in nulls_parts and "FIRST" in nulls_parts:
sql_nulls = SQL("NULLS FIRST")
elif "NULLS" in nulls_parts and "LAST" in nulls_parts:
sql_nulls = SQL("NULLS LAST")
else:
sql_nulls = SQL()
first_week_day = int(get_lang(self.env).week_start)
day_number_orderby = SQL(
"mod(7 - %s + (%s)::int, 7) %s %s",
first_week_day,
groupby_terms["day_number"],
sql_direction,
sql_nulls
)
return SQL(", ").join([day_number_orderby, other_orderby]) if other_orderby else day_number_orderby
@api.model
def action_open_discuss_channel_view(self, domain=()):
discuss_channels = self.search_fetch(domain, ["channel_id"]).channel_id
action = self.env["ir.actions.act_window"]._for_xml_id("im_livechat.discuss_channel_action")
if len(discuss_channels) == 1:
action["res_id"] = discuss_channels.id
action["view_mode"] = "form"
action["views"] = [view for view in action["views"] if view[1] == "form"]
return action
action["context"] = {}
action["domain"] = [("id", "in", discuss_channels.ids)]
action["mobile_view_mode"] = "list"
action["view_mode"] = "list"
action["views"] = [view for view in action["views"] if view[1] in ("list", "form")]
return action

View file

@ -6,21 +6,61 @@
<field name="name">im_livechat.report.channel.pivot</field>
<field name="model">im_livechat.report.channel</field>
<field name="arch" type="xml">
<pivot string="Livechat Support Statistics" disable_linking="1" sample="1">
<field name="technical_name" type="row"/>
<pivot js_class="im_livechat.report_channel_pivot" string="Livechat Support Statistics" sample="1" display_quantity="1">
<field name="has_call" type="measure" invisible="1"/>
<field name="call_duration_hour" type="measure" widget="float_time" options="{'displaySeconds': True}"/>
<field name="partner_id" type="row"/>
<field name="percentage_of_calls" type="measure" widget="percentage"/>
<field name="time_to_answer" string="Response Time (hh:mm:ss)" type="measure" widget="float_time" options="{'displaySeconds': True}" />
<field name="duration" type="measure"/>
<field name="nbr_message" type="measure"/>
<field name="rating" string="Rating (%)" type="measure" widget="im_livechat.rating_percentage"/>
<field name="number_of_calls" string="# of calls" type="measure" widget="integer"/>
</pivot>
</field>
</record>
<record id="im_livechat_report_channel_view_list" model="ir.ui.view">
<field name="name">im_livechat.report.channel.list</field>
<field name="model">im_livechat.report.channel</field>
<field name="arch" type="xml">
<list create="false">
<field name="start_date" string="Session Date"/>
<field name="channel_name" string="Participants"/>
<field name="country_id"/>
<field name="lang_id"/>
<field name="session_expertise_ids" string="Expertises" widget="many2many_tags"/>
<field name="duration" widget="float_time"/>
<field name="nbr_message" string="# Messages"/>
<field name="rating_text" string="Rating Text"/>
</list>
</field>
</record>
<record id="im_livechat_report_channel_view_form" model="ir.ui.view">
<field name="name">im_livechat.report.channel.form</field>
<field name="model">im_livechat.report.channel</field>
<field name="arch" type="xml">
<form string="Channel Rule" class="o_livechat_rules_form" js_class="livechat_session_form">
<sheet>
<group>
<field name="channel_name"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="im_livechat_report_channel_view_graph" model="ir.ui.view">
<field name="name">im_livechat.report.channel.graph</field>
<field name="model">im_livechat.report.channel</field>
<field name="arch" type="xml">
<graph string="Livechat Support Statistics" sample="1" disable_linking="1">
<field name="technical_name"/>
<field name="nbr_message" type="measure"/>
<graph js_class="im_livechat.channel_report_graph_views" string="Livechat Support Statistics" type="line" stacked="1" sample="1">
<field name="call_duration_hour" type="measure" widget="float_time"/>
<field name="has_call" invisible="1"/>
<field name="percentage_of_calls" type="measure" widget="percentage"/>
<field name="start_date" interval="day"/>
<field name="rating_text"/>
<field name="rating" string="Rating (%)" type="measure" widget="im_livechat.rating_percentage"/>
</graph>
</field>
</record>
@ -30,54 +70,88 @@
<field name="model">im_livechat.report.channel</field>
<field name="arch" type="xml">
<search string="Search report">
<field name="channel_name"/>
<filter name="missed_session" string="Missed sessions" domain="[('nbr_speaker','&lt;=', 1)]"/>
<filter name="treated_session" string="Treated sessions" domain="[('nbr_speaker','&gt;', 1)]"/>
<filter name="last_24h" string="Last 24h" domain="[('start_date','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') )]"/>
<filter name="start_date_filter" string="This Week" domain="[
('start_date', '>=', (datetime.datetime.combine(context_today() + relativedelta(weeks=-1,days=1,weekday=0), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S')),
('start_date', '&lt;', (datetime.datetime.combine(context_today() + relativedelta(days=1,weekday=0), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))]"/>
<field name="partner_id"/>
<field name="agent_requesting_help_history"/>
<field name="agent_requesting_help_history"/>
<field name="livechat_channel_id" string="Channel"/>
<field name="country_id" string="Country"/>
<field name="chatbot_script_id"/>
<field name="chatbot_answers_path_str" string="Chatbot Answers"/>
<field name="session_expertises" string="Expertise"/>
<field name="conversation_tag_ids" string="Tags"/>
<field name="visitor_partner_id"/>
<filter name="my_session" domain="[('partner_id.user_ids', '=', uid)]" string="My Sessions"/>
<separator/>
<filter name="filter_start_date" date="start_date"/>
<group expand="0" string="Group By...">
<filter name="group_by_session" string="Code" domain="[]" context="{'group_by':'technical_name'}"/>
<filter name="group_by_channel" string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter name="group_by_operator" string="Operator" domain="[('partner_id','!=', False)]" context="{'group_by':'partner_id'}"/>
<filter name="escalated" string="Escalated" domain="[('session_outcome', '=', 'escalated')]"/>
<filter name="no_answer" string="Not Answered" domain="[('session_outcome', '=', 'no_answer')]"/>
<filter name="no_agent" string="No one Available" domain="[('session_outcome', '=', 'no_agent')]"/>
<separator/>
<filter name="rating_happy" string="Happy" domain="[('rating_text','=', 'Happy')]"/>
<filter name="rating_neutral" string="Neutral" domain="[('rating_text','=', 'Neutral')]"/>
<filter name="rating_unhappy" string="Unhappy" domain="[('rating_text','=', 'Unhappy')]"/>
<separator />
<filter name="filter_start_date" string="Date" date="start_date"/>
<filter name="filter_date_last_month" invisible="1" string="Date: Last month"
domain="[('start_date', '&gt;=', 'today -1m')]"/>
<filter name="filter_date_last_week" invisible="1" string="Date: Last week"
domain="[('start_date', '&gt;=', 'today -1w')]"/>
<group>
<filter name="group_by_channel" string="Channel" domain="[]" context="{'group_by':'livechat_channel_id'}"/>
<filter name="group_by_operator" string="Agent" domain="[]" context="{'group_by': 'partner_id'}"/>
<filter name="group_by_agent_requesting_help" domain="[]" context="{'group_by': 'agent_requesting_help_history'}"/>
<filter name="group_by_agent_providing_help" domain="[]" context="{'group_by': 'agent_providing_help_history'}"/>
<filter name="group_by_rating" string="Rating" domain="[]" context="{'group_by':'rating_text'}"/>
<filter name="group_by_country" string="Country" domain="[]" context="{'group_by':'country_id'}"/>
<filter name="group_by_outcome" string="Status" domain="[]" context="{'group_by':'session_outcome'}"/>
<filter name="group_by_chatbot" string="Chatbot" domain="[]" context="{'group_by':'chatbot_script_id'}"/>
<filter name="group_by_chatbot_answers" domain="[]" context="{'group_by':'chatbot_answers_path'}"/>
<filter name="group_by_expertise" string="Expertise" domain="[]" context="{'group_by':'session_expertises'}"/>
<filter name="group_by_conversation_tag" string="Tags" domain="[]" context="{'group_by':'conversation_tag_ids'}"/>
<filter name="group_by_customer" domain="[]" context="{'group_by':'visitor_partner_id'}"/>
<separator orientation="vertical" />
<filter name="group_by_hour" string="Creation date (hour)" domain="[]" context="{'group_by':'start_date_hour'}"/>
<filter name="group_by_month" string="Creation date" domain="[]" context="{'group_by':'start_date:month'}" />
<filter name="group_by_hour" string="Hour of Day" domain="[]" context="{'group_by':'start_hour'}"/>
<filter name="group_by_day_of_week" string="Day of Week" domain="[]" context="{'group_by':'day_number'}"/>
<filter name="group_by_month" string="Date" domain="[]" context="{'group_by':'start_date:month'}" />
</group>
</search>
</field>
</record>
<record id="im_livechat_report_channel_action" model="ir.actions.act_window">
<field name="name">Session Statistics</field>
<field name="name">Sessions</field>
<field name="res_model">im_livechat.report.channel</field>
<field name="view_mode">graph,pivot</field>
<field name="context">{"search_default_last_week":1}</field>
<field name="help">Livechat Support Channel Statistics allows you to easily check and analyse your company livechat session performance. Extract information about the missed sessions, the audience, the duration of a session, etc.</field>
<field name="context">
{
"search_default_filter_date_last_month": 1,
"pivot_measures": ["__count", "time_to_answer", "duration", "rating", "number_of_calls"],
"graph_measure": "__count__",
"im_livechat_hide_partner_company": True,
}
</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">No data yet!</p>
<p>Track and improve live chat performance with insights on session activity, response times, customer ratings, and call interactions.</p>
</field>
</record>
<record id="im_livechat_report_channel_time_to_answer_action" model="ir.actions.act_window">
<field name="name">Session Statistics</field>
<field name="name">Sessions</field>
<field name="res_model">im_livechat.report.channel</field>
<field name="view_mode">graph,pivot</field>
<field name="context">{"graph_measure": "time_to_answer", "search_default_last_week":1}</field>
<field name="context">{"graph_measure": "time_to_answer", "search_default_filter_date_last_week":1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No data yet!
</p>
<p class="o_view_nocontent_smiling_face">No data yet!</p>
<p>Track and improve live chat performance with insights on session activity, response times, customer ratings, and call interactions.</p>
</field>
</record>
<menuitem
id="menu_reporting_livechat_channel"
name="Session Statistics"
name="Sessions"
parent="menu_reporting_livechat"
sequence="10"
sequence="20"
action="im_livechat_report_channel_action"/>
</data>
</odoo>

View file

@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools
class ImLivechatReportOperator(models.Model):
""" Livechat Support Report on the Operator """
_name = "im_livechat.report.operator"
_description = "Livechat Support Operator Report"
_order = 'livechat_channel_id, partner_id'
_auto = False
partner_id = fields.Many2one('res.partner', 'Operator', readonly=True)
livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True)
nbr_channel = fields.Integer('# of Sessions', readonly=True, group_operator="sum")
channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True)
start_date = fields.Datetime('Start Date of session', readonly=True)
time_to_answer = fields.Float('Time to answer', digits=(16, 2), readonly=True, group_operator="avg", help="Average time to give the first answer to the visitor")
duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)")
def init(self):
# Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view !
tools.drop_view_if_exists(self.env.cr, 'im_livechat_report_operator')
self.env.cr.execute("""
CREATE OR REPLACE VIEW im_livechat_report_operator AS (
SELECT
row_number() OVER () AS id,
C.livechat_operator_id AS partner_id,
C.livechat_channel_id AS livechat_channel_id,
COUNT(DISTINCT C.id) AS nbr_channel,
C.id AS channel_id,
C.create_date AS start_date,
EXTRACT('epoch' FROM MAX(M.create_date) - MIN(M.create_date)) AS duration,
EXTRACT('epoch' FROM MIN(MO.create_date) - MIN(M.create_date)) AS time_to_answer
FROM mail_channel C
JOIN mail_message M ON M.res_id = C.id AND M.model = 'mail.channel'
LEFT JOIN mail_message MO ON (MO.res_id = C.id AND MO.model = 'mail.channel' AND MO.author_id = C.livechat_operator_id)
WHERE C.livechat_channel_id IS NOT NULL
GROUP BY C.id, C.livechat_operator_id
)
""")

View file

@ -1,66 +0,0 @@
<?xml version="1.0"?>
<odoo>
<data>
<record id="im_livechat_report_operator_view_pivot" model="ir.ui.view">
<field name="name">im_livechat.report.operator.pivot</field>
<field name="model">im_livechat.report.operator</field>
<field name="arch" type="xml">
<pivot string="Livechat Support Statistics" disable_linking="1" sample="1">
<field name="partner_id" type="row"/>
<field name="duration" type="measure"/>
<field name="nbr_channel" type="measure"/>
</pivot>
</field>
</record>
<record id="im_livechat_report_operator_view_graph" model="ir.ui.view">
<field name="name">im_livechat.report.operator.graph</field>
<field name="model">im_livechat.report.operator</field>
<field name="arch" type="xml">
<graph string="Livechat Support Statistics" sample="1" disable_linking="1">
<field name="partner_id"/>
<field name="nbr_channel" type="measure"/>
</graph>
</field>
</record>
<record id="im_livechat_report_operator_view_search" model="ir.ui.view">
<field name="name">im_livechat.report.operator.search</field>
<field name="model">im_livechat.report.operator</field>
<field name="arch" type="xml">
<search string="Search report">
<field name="partner_id"/>
<filter name="last_24h" string="Last 24h" domain="[('start_date','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') )]"/>
<filter name="start_date_filter" string="This Week" domain="[
('start_date', '>=', (datetime.datetime.combine(context_today() + relativedelta(weeks=-1,days=1,weekday=0), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S')),
('start_date', '&lt;', (datetime.datetime.combine(context_today() + relativedelta(days=1,weekday=0), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))]"/>
<separator/>
<filter name="filter_start_date" date="start_date"/>
<group expand="0" string="Group By...">
<filter name="group_by_channel" string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter name="group_by_operator" string="Operator" domain="[('partner_id','!=', False)]" context="{'group_by':'partner_id'}"/>
<separator orientation="vertical" />
<filter name="group_by_month" string="Creation date" domain="[]" context="{'group_by':'start_date:month'}" />
</group>
</search>
</field>
</record>
<record id="im_livechat_report_operator_action" model="ir.actions.act_window">
<field name="name">Operator Analysis</field>
<field name="res_model">im_livechat.report.operator</field>
<field name="view_mode">graph,pivot</field>
<field name="context">{"search_default_last_week":1}</field>
<field name="help">Livechat Support Channel Statistics allows you to easily check and analyse your company livechat session performance. Extract information about the missed sessions, the audience, the duration of a session, etc.</field>
</record>
<menuitem
id="menu_reporting_livechat_operator"
name="Operator Analysis"
parent="menu_reporting_livechat"
sequence="10"
action="im_livechat_report_operator_action"/>
</data>
</odoo>