Files
enterprise-addons/voip/models/voip_call.py
T

301 lines
12 KiB
Python

from typing import Optional
from odoo import api, fields, models
from odoo.fields import Domain
from odoo.tools import SQL
from odoo.addons.mail.tools.discuss import Store
class VoipCall(models.Model):
_name = "voip.call"
_inherit = ["mail.thread.main.attachment", "voip.country.code.mixin"]
_description = "Phone call"
phone_number = fields.Char(required=True, readonly=True)
direction = fields.Selection(
[
("incoming", "Incoming"),
("outgoing", "Outgoing"),
],
default="outgoing",
readonly=True,
)
state = fields.Selection(
[
("aborted", "Aborted"),
("calling", "Calling"),
("missed", "Missed"),
("ongoing", "Ongoing"),
("rejected", "Rejected"),
("terminated", "Terminated"),
],
default="calling",
index=True,
readonly=True,
)
end_date = fields.Datetime(readonly=True)
start_date = fields.Datetime(readonly=True)
duration = fields.Float(compute="_compute_duration", readonly=True)
is_within_same_company = fields.Boolean(compute="_compute_is_within_same_company", store=True)
# Since activities are deleted from the database once marked as done, the
# activity name is saved here in order to be preserved.
activity_name = fields.Char(help="The name of the activity related to this phone call, if any.")
partner_id = fields.Many2one("res.partner", "Contact", index=True)
user_id = fields.Many2one("res.users", "Responsible", default=lambda self: self.env.uid, index=True)
country_id = fields.Many2one("res.country", compute="_compute_country_id", store=True)
country_flag_url = fields.Char(related="country_id.image_url", string="Country Flag")
call_count = fields.Integer(compute="_compute_call_count", help="The total number of calls made to the same phone number.")
image_1920 = fields.Binary(related="partner_id.image_1920")
avatar_128 = fields.Binary(related="partner_id.avatar_128")
@api.depends("partner_id", "phone_number")
def _compute_call_count(self):
if not self.ids:
self.call_count = 0
return
query = SQL(
"""
SELECT
call_1.id,
COUNT(DISTINCT call_2.id) AS count
FROM
voip_call AS call_1
JOIN voip_call AS call_2 ON (
(
call_1.phone_number = call_2.phone_number
OR call_1.partner_id = call_2.partner_id
)
)
WHERE
call_1.id IN %(ids)s
GROUP BY
call_1.id
ORDER BY
call_1.id;
""",
ids=tuple(self.ids),
)
self.env.cr.execute(query)
count_by_call_id = {res["id"]: res["count"] for res in self.env.cr.dictfetchall()}
for call in self:
call.call_count = count_by_call_id.get(call.id)
@api.depends("state", "partner_id.name")
def _compute_display_name(self):
def get_name(call):
if call.activity_name:
return call.activity_name
if call.state == "aborted":
return self.env._("Aborted call to %(phone_number)s", phone_number=call.phone_number)
if call.state == "missed":
return self.env._("Missed call from %(phone_number)s", phone_number=call.phone_number)
if call.state == "rejected":
if call.direction == "incoming":
return self.env._("Rejected call from %(phone_number)s", phone_number=call.phone_number)
return self.env._("Rejected call to %(phone_number)s", phone_number=call.phone_number)
if call.partner_id:
if call.direction == "incoming":
return self.env._("Call from %(correspondent)s", correspondent=call.partner_id.name)
return self.env._("Call to %(correspondent)s", correspondent=call.partner_id.name)
if call.direction == "incoming":
return self.env._("Call from %(phone_number)s", phone_number=call.phone_number)
return self.env._("Call to %(phone_number)s", phone_number=call.phone_number)
for call in self:
call.display_name = get_name(call)
@api.depends("start_date", "end_date")
def _compute_duration(self):
for call in self:
if call.start_date and call.end_date:
call.duration = (call.end_date - call.start_date).total_seconds() / 3600
else:
call.duration = 0
@api.depends("partner_id.commercial_partner_id", "user_id.partner_id.commercial_partner_id")
def _compute_is_within_same_company(self):
for call in self:
user_company = call.user_id.partner_id.commercial_partner_id
partner_company = call.partner_id.commercial_partner_id
call.is_within_same_company = user_company and user_company == partner_company
@api.depends("country_code_from_phone")
def _compute_country_id(self):
country_codes = set()
for call in self:
code = call.country_code_from_phone
if code:
country_codes.add(code.upper())
countries = self.env["res.country"].search_read(
[("code", "in", list(country_codes))],
fields=["id", "code"],
)
country_id_by_iso_code = {country["code"].lower(): country["id"] for country in countries}
for call in self:
call.country_id = country_id_by_iso_code.get(call.country_code_from_phone, False)
@api.ondelete(at_uninstall=False)
def _unlink_send_notification(self):
for partner, calls in self.grouped(lambda c: c.user_id.partner_id).items():
self.env["bus.bus"]._sendone(
partner,
"voip.call/delete",
{"ids": calls.ids},
)
def action_open_calls(self):
self.ensure_one()
domain = Domain("phone_number", "=", self.phone_number)
if self.partner_id:
domain |= Domain("partner_id", "=", self.partner_id)
return {
"type": "ir.actions.act_window",
"name": self.env._("Calls"),
"res_model": "voip.call",
"view_mode": "list,form",
"domain": domain,
}
@api.model
def create_and_format(
self,
phone_number: Optional[str] = None,
partner_id: Optional[int] = None,
direction: str = "outgoing",
res_id: Optional[int] = None,
res_model: Optional[str] = None,
) -> dict:
"""Creates a call from the provided values and returns it formatted for
use in JavaScript. If a record is provided via its id and model,
introspects it for a recipient.
:param phone_number: The phone number to call
:param partner_id: ID of the partner related to this call
:param direction: Direction of the call ('incoming' or 'outgoing')
:param res_id: Optional record ID to extract partner from
:param res_model: Optional model name to extract partner from
:return: Dictionary with created call IDs and formatted data
"""
self.check_access("read")
values = {
"phone_number": phone_number,
"partner_id": partner_id,
"direction": direction,
"state": "calling",
"user_id": self.env.uid,
}
if res_id and res_model:
related_record = self.env[res_model].browse(res_id)
related_record.check_access("read")
values["partner_id"] = next(
iter(related_record._mail_get_partners(introspect_fields=True)[related_record.id]),
self.env["res.partner"],
).id
calls = self.sudo().create(values).sudo(False)
return {
"ids": [call.id for call in calls],
"store_data": Store().add(calls, calls._get_voip_store_fields()).get_result(),
}
@api.model
def get_recent_phone_calls(
self, search_terms: Optional[str] = None, offset: int = 0, limit: Optional[int] = None
):
domain = Domain("user_id", "=", self.env.uid)
if search_terms:
search_fields = ["phone_number", "partner_id.name", "activity_name"]
domain &= Domain.OR([Domain(field, "ilike", search_terms) for field in search_fields])
calls = self.search(domain, offset=offset, limit=limit, order="create_date DESC")
return Store().add(calls, calls._get_voip_store_fields()).get_result()
@api.model
def _get_number_of_missed_calls(self) -> int:
domain = [("user_id", "=", self.env.uid), ("state", "=", "missed")]
last_seen_phone_call = self.env.user.last_seen_phone_call
if last_seen_phone_call:
domain += [("id", ">", last_seen_phone_call.id)]
return self.search_count(domain)
def abort_call(self):
self.check_access("read")
self.sudo().state = "aborted"
return Store().add(self, self._get_voip_store_fields()).get_result()
def start_call(self):
self.check_access("read")
calls_sudo = self.sudo()
calls_sudo.start_date = fields.Datetime.now()
calls_sudo.state = "ongoing"
return Store().add(self, self._get_voip_store_fields()).get_result()
def end_call(self, activity_name: Optional[str] = None):
self.check_access("read")
calls_sudo = self.sudo()
calls_sudo.end_date = fields.Datetime.now()
calls_sudo.state = "terminated"
if activity_name:
calls_sudo.activity_name = activity_name
return Store().add(self, self._get_voip_store_fields()).get_result()
def reject_call(self):
self.check_access("read")
self.sudo().state = "rejected"
return Store().add(self, self._get_voip_store_fields()).get_result()
def miss_call(self):
self.check_access("read")
self.sudo().state = "missed"
return Store().add(self, self._get_voip_store_fields()).get_result()
def get_contact_info(self):
self.ensure_one()
number = self.phone_number
# Internal extensions could theoretically be one or two digits long.
# phone_mobile_search doesn't handle numbers that short: do a regular
# search for the exact match:
if len(number) < 3:
domain = [("phone", "=", number)]
# 00 and + both denote an international prefix. phone_mobile_search will
# match both indifferently.
elif number.startswith(("+", "00")):
domain = [("phone_mobile_search", "=", number)]
# USA: Calls between different area codes are usually prefixed with 1.
# Conveniently, the country code for the USA also happens to be 1, so we
# just need to add the + symbol to format it like an international call
# and match what's supposed to be stored in the database.
elif number.startswith("1"):
domain = [("phone_mobile_search", "=", f"+{number}")]
else:
domain = [("phone_mobile_search", "=", number)]
partner = self.env["res.partner"].search(domain, limit=1)
if not partner:
partner = (
self.env["res.users.settings"]
.sudo()
.search([("voip_username", "=", number)], limit=1)
.user_id.partner_id.sudo(False)
)
if not partner:
return False
self.check_access("read")
self.sudo().partner_id = partner
return Store().add(self, self._get_voip_store_fields()).get_result()
def _get_voip_store_fields(self):
return [
"create_date",
"direction",
"display_name",
"end_date",
Store.One("partner_id", self.partner_id._voip_get_store_fields()),
Store.One("phone_country_id", self.phone_country_id._voip_get_store_fields()),
"phone_number",
"start_date",
"state",
]
def _phone_get_number_fields(self):
return ["phone_number"]