Merge pull request #18353 from surajshetty3416/refactor-call-popup

fix(call popup): Multiple changes
This commit is contained in:
Faris Ansari 2019-08-12 15:56:59 +05:30 committed by GitHub
commit 9002a6c195
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 267 additions and 154 deletions

View File

@ -8,12 +8,18 @@
"from", "from",
"to", "to",
"column_break_3", "column_break_3",
"received_by",
"medium", "medium",
"caller_information",
"contact",
"contact_name",
"column_break_10",
"lead",
"lead_name",
"section_break_5", "section_break_5",
"status", "status",
"duration", "duration",
"recording_url", "recording_url"
"summary"
], ],
"fields": [ "fields": [
{ {
@ -60,12 +66,6 @@
"label": "Duration", "label": "Duration",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "summary",
"fieldtype": "Data",
"label": "Summary",
"read_only": 1
},
{ {
"fieldname": "recording_url", "fieldname": "recording_url",
"fieldtype": "Data", "fieldtype": "Data",
@ -77,10 +77,58 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Medium", "label": "Medium",
"read_only": 1 "read_only": 1
},
{
"fieldname": "received_by",
"fieldtype": "Link",
"label": "Received By",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "caller_information",
"fieldtype": "Section Break",
"label": "Caller Information"
},
{
"fieldname": "contact",
"fieldtype": "Link",
"label": "Contact",
"options": "Contact",
"read_only": 1
},
{
"fieldname": "lead",
"fieldtype": "Link",
"label": "Lead ",
"options": "Lead",
"read_only": 1
},
{
"fetch_from": "contact.name",
"fieldname": "contact_name",
"fieldtype": "Data",
"hidden": 1,
"in_list_view": 1,
"label": "Contact Name",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fetch_from": "lead.lead_name",
"fieldname": "lead_name",
"fieldtype": "Data",
"hidden": 1,
"in_list_view": 1,
"label": "Lead Name",
"read_only": 1
} }
], ],
"in_create": 1, "in_create": 1,
"modified": "2019-07-01 09:09:48.516722", "modified": "2019-08-06 05:46:53.144683",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Communication", "module": "Communication",
"name": "Call Log", "name": "Call Log",
@ -97,10 +145,15 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"read": 1,
"role": "Employee"
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"title_field": "from", "title_field": "from",
"track_changes": 1 "track_changes": 1,
"track_views": 1
} }

View File

@ -4,16 +4,83 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.crm.doctype.utils import get_employee_emails_for_popup from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup
from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number
from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number
class CallLog(Document): class CallLog(Document):
def before_insert(self):
# strip 0 from the start of the number for proper number comparisions
# eg. 07888383332 should match with 7888383332
number = self.get('from').lstrip('0')
self.contact = get_contact_with_phone_number(number)
self.lead = get_lead_with_phone_number(number)
def after_insert(self): def after_insert(self):
employee_emails = get_employee_emails_for_popup(self.medium) self.trigger_call_popup()
for email in employee_emails:
frappe.publish_realtime('show_call_popup', self, user=email)
def on_update(self): def on_update(self):
doc_before_save = self.get_doc_before_save() doc_before_save = self.get_doc_before_save()
if doc_before_save and doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']: 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) frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self)
elif doc_before_save.to != self.to:
self.trigger_call_popup()
def trigger_call_popup(self):
scheduled_employees = get_scheduled_employees_for_popup(self.to)
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 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)
@frappe.whitelist()
def add_call_summary(call_log, summary):
doc = frappe.get_doc('Call Log', call_log)
doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '<br><br>' + summary)
def get_employees_with_number(number):
if not number: return []
employee_emails = frappe.cache().hget('employees_with_number', number)
if employee_emails: return employee_emails
employees = frappe.get_all('Employee', filters={
'cell_number': ['like', '%{}'.format(number.lstrip('0'))],
'user_id': ['!=', '']
}, fields=['user_id'])
employee_emails = [employee.user_id for employee in employees]
frappe.cache().hset('employees_with_number', number, employee_emails)
return employee
def set_caller_information(doc, state):
'''Called from hoooks on creation of Lead or Contact'''
if doc.doctype not in ['Lead', 'Contact']: return
numbers = [doc.get('phone'), doc.get('mobile_no')]
for_doc = doc.doctype.lower()
for number in numbers:
if not number: continue
print(number)
filters = frappe._dict({
'from': ['like', '%{}'.format(number.lstrip('0'))],
for_doc: ''
})
logs = frappe.get_all('Call Log', filters=filters)
for log in logs:
call_log = frappe.get_doc('Call Log', log.name)
call_log.set(for_doc, doc.name)
call_log.save(ignore_permissions=True)

View File

@ -241,3 +241,15 @@ def make_lead_from_communication(communication, ignore_communication_links=False
link_communication_to_document(doc, "Lead", lead_name, ignore_communication_links) link_communication_to_document(doc, "Lead", lead_name, ignore_communication_links)
return lead_name return lead_name
def get_lead_with_phone_number(number):
if not number: return
leads = frappe.get_all('Lead', or_filters={
'phone': ['like', '%{}'.format(number)],
'mobile_no': ['like', '%{}'.format(number)]
}, limit=1)
lead = leads[0].name if leads else None
return lead

View File

@ -3,82 +3,57 @@ from frappe import _
import json import json
@frappe.whitelist() @frappe.whitelist()
def get_document_with_phone_number(number): def get_last_interaction(contact=None, lead=None):
# finds contacts and leads
if not number: return
number = number.lstrip('0')
number_filter = {
'phone': ['like', '%{}'.format(number)],
'mobile_no': ['like', '%{}'.format(number)]
}
contacts = frappe.get_all('Contact', or_filters=number_filter, limit=1)
if contacts: if not contact and not lead: return
return frappe.get_doc('Contact', contacts[0].name)
leads = frappe.get_all('Lead', or_filters=number_filter, limit=1) last_communication = None
last_issue = None
if leads: if contact:
return frappe.get_doc('Lead', leads[0].name)
@frappe.whitelist()
def get_last_interaction(number, reference_doc):
reference_doc = json.loads(reference_doc) if reference_doc else get_document_with_phone_number(number)
if not reference_doc: return
reference_doc = frappe._dict(reference_doc)
last_communication = {}
last_issue = {}
if reference_doc.doctype == 'Contact':
customer_name = ''
query_condition = '' query_condition = ''
for link in reference_doc.links: values = []
link = frappe._dict(link) contact = frappe.get_doc('Contact', contact)
for link in contact.links:
if link.link_doctype == 'Customer': if link.link_doctype == 'Customer':
customer_name = link.link_name last_issue = get_last_issue_from_customer(link.link_name)
query_condition += "(`reference_doctype`='{}' AND `reference_name`='{}') OR".format(link.link_doctype, link.link_name) query_condition += "(`reference_doctype`=%s AND `reference_name`=%s) OR"
values += [link_link_doctype, link_link_name]
if query_condition: if query_condition:
# remove extra appended 'OR'
query_condition = query_condition[:-2] query_condition = query_condition[:-2]
last_communication = frappe.db.sql(""" last_communication = frappe.db.sql("""
SELECT `name`, `content` SELECT `name`, `content`
FROM `tabCommunication` FROM `tabCommunication`
WHERE {} WHERE `sent_or_received`='Received'
AND ({})
ORDER BY `modified` ORDER BY `modified`
LIMIT 1 LIMIT 1
""".format(query_condition)) # nosec """.format(query_condition), values, as_dict=1) # nosec
if customer_name: if lead:
last_issue = frappe.get_all('Issue', {
'customer': customer_name
}, ['name', 'subject', 'customer'], limit=1)
elif reference_doc.doctype == 'Lead':
last_communication = frappe.get_all('Communication', filters={ last_communication = frappe.get_all('Communication', filters={
'reference_doctype': reference_doc.doctype, 'reference_doctype': 'Lead',
'reference_name': reference_doc.name, 'reference_name': lead,
'sent_or_received': 'Received' 'sent_or_received': 'Received'
}, fields=['name', 'content'], limit=1) }, fields=['name', 'content'], order_by='`creation` DESC', limit=1)
last_communication = last_communication[0] if last_communication else None
return { return {
'last_communication': last_communication[0] if last_communication else None, 'last_communication': last_communication,
'last_issue': last_issue[0] if last_issue else None 'last_issue': last_issue
} }
@frappe.whitelist() def get_last_issue_from_customer(customer_name):
def add_call_summary(docname, summary): issues = frappe.get_all('Issue', {
call_log = frappe.get_doc('Call Log', docname) 'customer': customer_name
summary = _('Call Summary by {0}: {1}').format( }, ['name', 'subject', 'customer'], order_by='`creation` DESC', limit=1)
frappe.utils.get_fullname(frappe.session.user), summary)
if not call_log.summary:
call_log.summary = summary
else:
call_log.summary += '<br>' + summary
call_log.save(ignore_permissions=True)
def get_employee_emails_for_popup(communication_medium): return issues[0] if issues else None
def get_scheduled_employees_for_popup(communication_medium):
now_time = frappe.utils.nowtime() now_time = frappe.utils.nowtime()
weekday = frappe.utils.get_weekday() weekday = frappe.utils.get_weekday()

View File

@ -18,6 +18,8 @@ def handle_incoming_call(**kwargs):
call_log = get_call_log(call_payload) call_log = get_call_log(call_payload)
if not call_log: if not call_log:
create_call_log(call_payload) create_call_log(call_payload)
else:
update_call_log(call_payload, call_log=call_log)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def handle_end_call(**kwargs): def handle_end_call(**kwargs):
@ -27,10 +29,11 @@ def handle_end_call(**kwargs):
def handle_missed_call(**kwargs): def handle_missed_call(**kwargs):
update_call_log(kwargs, 'Missed') update_call_log(kwargs, 'Missed')
def update_call_log(call_payload, status): def update_call_log(call_payload, status='Ringing', call_log=None):
call_log = get_call_log(call_payload) call_log = call_log or get_call_log(call_payload)
if call_log: if call_log:
call_log.status = status call_log.status = status
call_log.to = call_payload.get('DialWhomNumber')
call_log.duration = call_payload.get('DialCallDuration') or 0 call_log.duration = call_payload.get('DialCallDuration') or 0
call_log.recording_url = call_payload.get('RecordingUrl') call_log.recording_url = call_payload.get('RecordingUrl')
call_log.save(ignore_permissions=True) call_log.save(ignore_permissions=True)
@ -48,7 +51,7 @@ def get_call_log(call_payload):
def create_call_log(call_payload): def create_call_log(call_payload):
call_log = frappe.new_doc('Call Log') call_log = frappe.new_doc('Call Log')
call_log.id = call_payload.get('CallSid') call_log.id = call_payload.get('CallSid')
call_log.to = call_payload.get('CallTo') call_log.to = call_payload.get('DialWhomNumber')
call_log.medium = call_payload.get('To') call_log.medium = call_payload.get('To')
call_log.status = 'Ringing' call_log.status = 'Ringing'
setattr(call_log, 'from', call_payload.get('CallFrom')) setattr(call_log, 'from', call_payload.get('CallFrom'))

View File

@ -231,8 +231,12 @@ doc_events = {
('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): { ('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): {
'validate': 'erpnext.regional.india.utils.set_place_of_supply' 'validate': 'erpnext.regional.india.utils.set_place_of_supply'
}, },
"Contact":{ "Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue" "on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information"
},
"Lead": {
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information"
}, },
"Email Unsubscribe": { "Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"

View File

@ -76,6 +76,7 @@ class Employee(NestedSet):
if self.user_id: if self.user_id:
self.update_user() self.update_user()
self.update_user_permissions() self.update_user_permissions()
self.reset_employee_emails_cache()
def update_user_permissions(self): def update_user_permissions(self):
if not self.create_user_permission: return if not self.create_user_permission: return
@ -214,6 +215,15 @@ class Employee(NestedSet):
doc.validate_employee_creation() doc.validate_employee_creation()
doc.db_set("employee", self.name) doc.db_set("employee", self.name)
def reset_employee_emails_cache(self):
prev_doc = self.get_doc_before_save() or {}
cell_number = self.get('cell_number')
prev_number = prev_doc.get('cell_number')
if (cell_number != prev_number or
self.get('user_id') != prev_doc.get('user_id')):
frappe.cache().hdel('employees_with_number', cell_number)
frappe.cache().hdel('employees_with_number', prev_number)
def get_timeline_data(doctype, name): def get_timeline_data(doctype, name):
'''Return timeline for attendance''' '''Return timeline for attendance'''
return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*) return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*)

View File

@ -11,12 +11,51 @@ class CallPopup {
'static': true, 'static': true,
'minimizable': true, 'minimizable': true,
'fields': [{ 'fields': [{
'fieldname': 'caller_info', 'fieldname': 'name',
'fieldtype': 'HTML' 'label': 'Name',
'default': this.get_caller_name() || __('Unknown Caller'),
'fieldtype': 'Data',
'read_only': 1
}, {
'fieldtype': 'Button',
'label': __('Open Contact'),
'click': () => frappe.set_route('Form', 'Contact', this.call_log.contact),
'depends_on': () => this.call_log.contact
}, {
'fieldtype': 'Button',
'label': __('Open Lead'),
'click': () => frappe.set_route('Form', 'Lead', this.call_log.lead),
'depends_on': () => this.call_log.lead
}, {
'fieldtype': 'Button',
'label': __('Make New Contact'),
'click': () => frappe.new_doc('Contact', { 'mobile_no': this.caller_number }),
'depends_on': () => !this.get_caller_name()
}, {
'fieldtype': 'Button',
'label': __('Make New Lead'),
'click': () => frappe.new_doc('Lead', { 'mobile_no': this.caller_number }),
'depends_on': () => !this.get_caller_name()
}, {
'fieldtype': 'Column Break',
}, {
'fieldname': 'number',
'label': 'Phone Number',
'fieldtype': 'Data',
'default': this.caller_number,
'read_only': 1
}, { }, {
'fielname': 'last_interaction', 'fielname': 'last_interaction',
'fieldtype': 'Section Break', 'fieldtype': 'Section Break',
'label': __('Activity'), 'label': __('Activity'),
'depends_on': () => this.get_caller_name()
}, {
'fieldtype': 'Small Text',
'label': __('Last Issue'),
'fieldname': 'last_issue',
'read_only': true,
'depends_on': () => this.call_log.contact,
'default': `<i class="text-muted">${__('No issue has been raised by the caller.')}<i>`
}, { }, {
'fieldtype': 'Small Text', 'fieldtype': 'Small Text',
'label': __('Last Communication'), 'label': __('Last Communication'),
@ -24,13 +63,7 @@ class CallPopup {
'read_only': true, 'read_only': true,
'default': `<i class="text-muted">${__('No communication found.')}<i>` 'default': `<i class="text-muted">${__('No communication found.')}<i>`
}, { }, {
'fieldtype': 'Small Text', 'fieldtype': 'Section Break',
'label': __('Last Issue'),
'fieldname': 'last_issue',
'read_only': true,
'default': `<i class="text-muted">${__('No issue raised by the customer.')}<i>`
}, {
'fieldtype': 'Column Break',
}, { }, {
'fieldtype': 'Small Text', 'fieldtype': 'Small Text',
'label': __('Call Summary'), 'label': __('Call Summary'),
@ -41,13 +74,21 @@ class CallPopup {
'click': () => { 'click': () => {
const call_summary = this.dialog.get_value('call_summary'); const call_summary = this.dialog.get_value('call_summary');
if (!call_summary) return; if (!call_summary) return;
frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { frappe.xcall('erpnext.communication.doctype.call_log.call_log.add_call_summary', {
'docname': this.call_log.id, 'call_log': this.call_log.name,
'summary': call_summary, 'summary': call_summary,
}).then(() => { }).then(() => {
this.close_modal(); this.close_modal();
frappe.show_alert({ frappe.show_alert({
message: `${__('Call Summary Saved')}<br><a class="text-small text-muted" href="#Form/Call Log/${this.call_log.name}">${__('View call log')}</a>`, message: `
${__('Call Summary Saved')}
<br>
<a
class="text-small text-muted"
href="#Form/Call Log/${this.call_log.name}">
${__('View call log')}
</a>
`,
indicator: 'green' indicator: 'green'
}); });
}); });
@ -55,71 +96,14 @@ class CallPopup {
}], }],
}); });
this.set_call_status(); this.set_call_status();
this.make_caller_info_section();
this.dialog.get_close_btn().show(); this.dialog.get_close_btn().show();
this.make_last_interaction_section();
this.dialog.$body.addClass('call-popup'); this.dialog.$body.addClass('call-popup');
this.dialog.set_secondary_action(this.close_modal.bind(this)); this.dialog.set_secondary_action(this.close_modal.bind(this));
frappe.utils.play_sound('incoming-call'); frappe.utils.play_sound('incoming-call');
this.dialog.show(); this.dialog.show();
} }
make_caller_info_section() {
const wrapper = this.dialog.get_field('caller_info').$wrapper;
wrapper.append(`<div class="text-muted"> ${__("Loading...")} </div>`);
frappe.xcall('erpnext.crm.doctype.utils.get_document_with_phone_number', {
'number': this.caller_number
}).then(contact_doc => {
wrapper.empty();
const contact = this.contact = contact_doc;
if (!contact) {
this.setup_unknown_caller(wrapper);
} else {
this.setup_known_caller(wrapper);
this.set_call_status();
this.make_last_interaction_section();
}
});
}
setup_unknown_caller(wrapper) {
wrapper.append(`
<div class="caller-info">
<b>${__('Unknown Number')}:</b> ${this.caller_number}
<button
class="margin-left btn btn-new btn-default btn-xs"
data-doctype="Contact"
title=${__("Make New Contact")}>
<i class="octicon octicon-plus text-medium"></i>
</button>
</div>
`).find('button').click(
() => frappe.set_route(`Form/Contact/New Contact?phone=${this.caller_number}`)
);
}
setup_known_caller(wrapper) {
const contact = this.contact;
const contact_name = frappe.utils.get_form_link(contact.doctype, contact.name, true, this.get_caller_name());
const links = contact.links ? contact.links : [];
let contact_links = '';
links.forEach(link => {
contact_links += `<div>${link.link_doctype}: ${frappe.utils.get_form_link(link.link_doctype, link.link_name, true)}</div>`;
});
wrapper.append(`
<div class="caller-info flex">
${frappe.avatar(null, 'avatar-xl', contact.name, contact.image || '')}
<div>
<h5>${contact_name}</h5>
<div>${contact.mobile_no || ''}</div>
<div>${contact.phone_no || ''}</div>
${contact_links}
</div>
</div>
`);
}
set_indicator(color, blink=false) { set_indicator(color, blink=false) {
let classes = `indicator ${color} ${blink ? 'blink': ''}`; let classes = `indicator ${color} ${blink ? 'blink': ''}`;
this.dialog.header.find('.indicator').attr('class', classes); this.dialog.header.find('.indicator').attr('class', classes);
@ -129,7 +113,7 @@ class CallPopup {
let title = ''; let title = '';
call_status = call_status || this.call_log.status; call_status = call_status || this.call_log.status;
if (['Ringing'].includes(call_status) || !call_status) { if (['Ringing'].includes(call_status) || !call_status) {
title = __('Incoming call from {0}', [this.get_caller_name()]); title = __('Incoming call from {0}', [this.get_caller_name() || this.caller_number]);
this.set_indicator('blue', true); this.set_indicator('blue', true);
} else if (call_status === 'In Progress') { } else if (call_status === 'In Progress') {
title = __('Call Connected'); title = __('Call Connected');
@ -164,13 +148,13 @@ class CallPopup {
if (!this.dialog.get_value('call_summary')) { if (!this.dialog.get_value('call_summary')) {
this.close_modal(); this.close_modal();
} }
}, 10000); }, 30000);
} }
make_last_interaction_section() { make_last_interaction_section() {
frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', { frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
'number': this.caller_number, 'contact': this.call_log.contact,
'reference_doc': this.contact 'lead': this.call_log.lead
}).then(data => { }).then(data => {
const comm_field = this.dialog.get_field('last_communication'); const comm_field = this.dialog.get_field('last_communication');
if (data.last_communication) { if (data.last_communication) {
@ -182,15 +166,20 @@ class CallPopup {
const issue = data.last_issue; const issue = data.last_issue;
const issue_field = this.dialog.get_field("last_issue"); const issue_field = this.dialog.get_field("last_issue");
issue_field.set_value(issue.subject); issue_field.set_value(issue.subject);
issue_field.$wrapper.append(`<a class="text-medium" href="#List/Issue?customer=${issue.customer}"> issue_field.$wrapper.append(`
${__('View all issues from {0}', [issue.customer])} <a class="text-medium" href="#List/Issue?customer=${issue.customer}">
</a>`); ${__('View all issues from {0}', [issue.customer])}
</a>
`);
} }
}); });
} }
get_caller_name() { get_caller_name() {
return this.contact ? this.contact.lead_name || this.contact.name || '' : this.caller_number; let log = this.call_log;
return log.contact_name || log.lead_name;
} }
setup_listener() { setup_listener() {
frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => { frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => {
this.call_disconnected(call_log); this.call_disconnected(call_log);