301 lines
12 KiB
Python
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"]
|