|
|
@ -8,40 +8,83 @@ from frappe import _ |
|
|
|
from frappe.model.document import Document |
|
|
|
from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number |
|
|
|
from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number |
|
|
|
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links |
|
|
|
|
|
|
|
from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number |
|
|
|
|
|
|
|
END_CALL_STATUSES = ['No Answer', 'Completed', 'Busy', 'Failed'] |
|
|
|
ONGOING_CALL_STATUSES = ['Ringing', 'In Progress'] |
|
|
|
|
|
|
|
|
|
|
|
class CallLog(Document): |
|
|
|
def validate(self): |
|
|
|
deduplicate_dynamic_links(self) |
|
|
|
|
|
|
|
def before_insert(self): |
|
|
|
number = strip_number(self.get('from')) |
|
|
|
self.contact = get_contact_with_phone_number(number) |
|
|
|
self.lead = get_lead_with_phone_number(number) |
|
|
|
if self.contact: |
|
|
|
contact = frappe.get_doc("Contact", self.contact) |
|
|
|
self.customer = contact.get_link_for("Customer") |
|
|
|
"""Add lead(third party person) links to the document. |
|
|
|
""" |
|
|
|
lead_number = self.get('from') if self.is_incoming_call() else self.get('to') |
|
|
|
lead_number = strip_number(lead_number) |
|
|
|
|
|
|
|
contact = get_contact_with_phone_number(strip_number(lead_number)) |
|
|
|
if contact: |
|
|
|
self.add_link(link_type='Contact', link_name=contact) |
|
|
|
|
|
|
|
lead = get_lead_with_phone_number(lead_number) |
|
|
|
if lead: |
|
|
|
self.add_link(link_type='Lead', link_name=lead) |
|
|
|
|
|
|
|
def after_insert(self): |
|
|
|
self.trigger_call_popup() |
|
|
|
|
|
|
|
def on_update(self): |
|
|
|
def _is_call_missed(doc_before_save, doc_after_save): |
|
|
|
# FIXME: This works for Exotel but not for all telepony providers |
|
|
|
return doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES |
|
|
|
|
|
|
|
def _is_call_ended(doc_before_save, doc_after_save): |
|
|
|
return doc_before_save.status not in END_CALL_STATUSES and self.status in END_CALL_STATUSES |
|
|
|
|
|
|
|
doc_before_save = self.get_doc_before_save() |
|
|
|
if not doc_before_save: return |
|
|
|
if doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']: |
|
|
|
frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self) |
|
|
|
elif doc_before_save.to != self.to: |
|
|
|
|
|
|
|
if _is_call_missed(doc_before_save, self): |
|
|
|
frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self) |
|
|
|
self.trigger_call_popup() |
|
|
|
|
|
|
|
if _is_call_ended(doc_before_save, self): |
|
|
|
frappe.publish_realtime('call_{id}_ended'.format(id=self.id), self) |
|
|
|
|
|
|
|
def is_incoming_call(self): |
|
|
|
return self.type == 'Incoming' |
|
|
|
|
|
|
|
def add_link(self, link_type, link_name): |
|
|
|
self.append('links', { |
|
|
|
'link_doctype': link_type, |
|
|
|
'link_name': link_name |
|
|
|
}) |
|
|
|
|
|
|
|
def trigger_call_popup(self): |
|
|
|
scheduled_employees = get_scheduled_employees_for_popup(self.medium) |
|
|
|
employee_emails = get_employees_with_number(self.to) |
|
|
|
if self.is_incoming_call(): |
|
|
|
scheduled_employees = get_scheduled_employees_for_popup(self.medium) |
|
|
|
employee_emails = get_employees_with_number(self.to) |
|
|
|
|
|
|
|
# check if employees with matched number are scheduled to receive popup |
|
|
|
emails = set(scheduled_employees).intersection(employee_emails) |
|
|
|
|
|
|
|
if frappe.conf.developer_mode: |
|
|
|
self.add_comment(text=f""" |
|
|
|
Scheduled Employees: {scheduled_employees} |
|
|
|
Matching Employee: {employee_emails} |
|
|
|
Show Popup To: {emails} |
|
|
|
""") |
|
|
|
|
|
|
|
# check if employees with matched number are scheduled to receive popup |
|
|
|
emails = set(scheduled_employees).intersection(employee_emails) |
|
|
|
if employee_emails and not emails: |
|
|
|
self.add_comment(text=_("No employee was scheduled for call popup")) |
|
|
|
|
|
|
|
# # if no employee found with matching phone number then show popup to scheduled employees |
|
|
|
# emails = emails or scheduled_employees if employee_emails |
|
|
|
for email in emails: |
|
|
|
frappe.publish_realtime('show_call_popup', self, user=email) |
|
|
|
|
|
|
|
for email in emails: |
|
|
|
frappe.publish_realtime('show_call_popup', self, user=email) |
|
|
|
|
|
|
|
@frappe.whitelist() |
|
|
|
def add_call_summary(call_log, summary): |
|
|
@ -65,34 +108,67 @@ def get_employees_with_number(number): |
|
|
|
|
|
|
|
return employee_emails |
|
|
|
|
|
|
|
def set_caller_information(doc, state): |
|
|
|
'''Called from hooks on creation of Lead or Contact''' |
|
|
|
if doc.doctype not in ['Lead', 'Contact']: return |
|
|
|
|
|
|
|
numbers = [doc.get('phone'), doc.get('mobile_no')] |
|
|
|
# contact for Contact and lead for Lead |
|
|
|
fieldname = doc.doctype.lower() |
|
|
|
|
|
|
|
# contact_name or lead_name |
|
|
|
display_name_field = '{}_name'.format(fieldname) |
|
|
|
|
|
|
|
# Contact now has all the nos saved in child table |
|
|
|
if doc.doctype == 'Contact': |
|
|
|
def link_existing_conversations(doc, state): |
|
|
|
""" |
|
|
|
Called from hooks on creation of Contact or Lead to link all the existing conversations. |
|
|
|
""" |
|
|
|
if doc.doctype != 'Contact': return |
|
|
|
try: |
|
|
|
numbers = [d.phone for d in doc.phone_nos] |
|
|
|
|
|
|
|
for number in numbers: |
|
|
|
number = strip_number(number) |
|
|
|
if not number: continue |
|
|
|
|
|
|
|
filters = frappe._dict({ |
|
|
|
'from': ['like', '%{}'.format(number)], |
|
|
|
fieldname: '' |
|
|
|
for number in numbers: |
|
|
|
number = strip_number(number) |
|
|
|
if not number: continue |
|
|
|
logs = frappe.db.sql_list(""" |
|
|
|
SELECT cl.name FROM `tabCall Log` cl |
|
|
|
LEFT JOIN `tabDynamic Link` dl |
|
|
|
ON cl.name = dl.parent |
|
|
|
WHERE (cl.`from` like %(phone_number)s or cl.`to` like %(phone_number)s) |
|
|
|
GROUP BY cl.name |
|
|
|
HAVING SUM( |
|
|
|
CASE |
|
|
|
WHEN dl.link_doctype = %(doctype)s AND dl.link_name = %(docname)s |
|
|
|
THEN 1 |
|
|
|
ELSE 0 |
|
|
|
END |
|
|
|
)=0 |
|
|
|
""", dict( |
|
|
|
phone_number='%{}'.format(number), |
|
|
|
docname=doc.name, |
|
|
|
doctype = doc.doctype |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
for log in logs: |
|
|
|
call_log = frappe.get_doc('Call Log', log) |
|
|
|
call_log.add_link(link_type=doc.doctype, link_name=doc.name) |
|
|
|
call_log.save() |
|
|
|
frappe.db.commit() |
|
|
|
except Exception: |
|
|
|
frappe.log_error(title=_('Error during caller information update')) |
|
|
|
|
|
|
|
def get_linked_call_logs(doctype, docname): |
|
|
|
# content will be shown in timeline |
|
|
|
logs = frappe.get_all('Dynamic Link', fields=['parent'], filters={ |
|
|
|
'parenttype': 'Call Log', |
|
|
|
'link_doctype': doctype, |
|
|
|
'link_name': docname |
|
|
|
}) |
|
|
|
|
|
|
|
logs = set([log.parent for log in logs]) |
|
|
|
|
|
|
|
logs = frappe.get_all('Call Log', fields=['*'], filters={ |
|
|
|
'name': ['in', logs] |
|
|
|
}) |
|
|
|
|
|
|
|
timeline_contents = [] |
|
|
|
for log in logs: |
|
|
|
log.show_call_button = 0 |
|
|
|
timeline_contents.append({ |
|
|
|
'creation': log.creation, |
|
|
|
'template': 'call_link', |
|
|
|
'template_data': log |
|
|
|
}) |
|
|
|
|
|
|
|
logs = frappe.get_all('Call Log', filters=filters) |
|
|
|
return timeline_contents |
|
|
|
|
|
|
|
for log in logs: |
|
|
|
frappe.db.set_value('Call Log', log.name, { |
|
|
|
fieldname: doc.name, |
|
|
|
display_name_field: doc.get_title() |
|
|
|
}, update_modified=False) |
|
|
|