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",
"to",
"column_break_3",
"received_by",
"medium",
"caller_information",
"contact",
"contact_name",
"column_break_10",
"lead",
"lead_name",
"section_break_5",
"status",
"duration",
"recording_url",
"summary"
"recording_url"
],
"fields": [
{
@ -60,12 +66,6 @@
"label": "Duration",
"read_only": 1
},
{
"fieldname": "summary",
"fieldtype": "Data",
"label": "Summary",
"read_only": 1
},
{
"fieldname": "recording_url",
"fieldtype": "Data",
@ -77,10 +77,58 @@
"fieldtype": "Data",
"label": "Medium",
"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,
"modified": "2019-07-01 09:09:48.516722",
"modified": "2019-08-06 05:46:53.144683",
"modified_by": "Administrator",
"module": "Communication",
"name": "Call Log",
@ -97,10 +145,15 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Employee"
}
],
"sort_field": "modified",
"sort_order": "ASC",
"title_field": "from",
"track_changes": 1
"track_changes": 1,
"track_views": 1
}

View File

@ -4,16 +4,83 @@
from __future__ import unicode_literals
import frappe
from frappe import _
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):
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):
employee_emails = get_employee_emails_for_popup(self.medium)
for email in employee_emails:
frappe.publish_realtime('show_call_popup', self, user=email)
self.trigger_call_popup()
def on_update(self):
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)
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)
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
@frappe.whitelist()
def get_document_with_phone_number(number):
# 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)
def get_last_interaction(contact=None, lead=None):
if contacts:
return frappe.get_doc('Contact', contacts[0].name)
if not contact and not lead: return
leads = frappe.get_all('Lead', or_filters=number_filter, limit=1)
if leads:
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 = ''
last_communication = None
last_issue = None
if contact:
query_condition = ''
for link in reference_doc.links:
link = frappe._dict(link)
values = []
contact = frappe.get_doc('Contact', contact)
for link in contact.links:
if link.link_doctype == 'Customer':
customer_name = link.link_name
query_condition += "(`reference_doctype`='{}' AND `reference_name`='{}') OR".format(link.link_doctype, link.link_name)
last_issue = get_last_issue_from_customer(link.link_name)
query_condition += "(`reference_doctype`=%s AND `reference_name`=%s) OR"
values += [link_link_doctype, link_link_name]
if query_condition:
# remove extra appended 'OR'
query_condition = query_condition[:-2]
last_communication = frappe.db.sql("""
SELECT `name`, `content`
FROM `tabCommunication`
WHERE {}
WHERE `sent_or_received`='Received'
AND ({})
ORDER BY `modified`
LIMIT 1
""".format(query_condition)) # nosec
""".format(query_condition), values, as_dict=1) # nosec
if customer_name:
last_issue = frappe.get_all('Issue', {
'customer': customer_name
}, ['name', 'subject', 'customer'], limit=1)
elif reference_doc.doctype == 'Lead':
if lead:
last_communication = frappe.get_all('Communication', filters={
'reference_doctype': reference_doc.doctype,
'reference_name': reference_doc.name,
'reference_doctype': 'Lead',
'reference_name': lead,
'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 {
'last_communication': last_communication[0] if last_communication else None,
'last_issue': last_issue[0] if last_issue else None
'last_communication': last_communication,
'last_issue': last_issue
}
@frappe.whitelist()
def add_call_summary(docname, summary):
call_log = frappe.get_doc('Call Log', docname)
summary = _('Call Summary by {0}: {1}').format(
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_last_issue_from_customer(customer_name):
issues = frappe.get_all('Issue', {
'customer': customer_name
}, ['name', 'subject', 'customer'], order_by='`creation` DESC', limit=1)
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()
weekday = frappe.utils.get_weekday()

View File

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

View File

@ -231,8 +231,12 @@ doc_events = {
('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): {
'validate': 'erpnext.regional.india.utils.set_place_of_supply'
},
"Contact":{
"on_trash": "erpnext.support.doctype.issue.issue.update_issue"
"Contact": {
"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": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"

View File

@ -76,6 +76,7 @@ class Employee(NestedSet):
if self.user_id:
self.update_user()
self.update_user_permissions()
self.reset_employee_emails_cache()
def update_user_permissions(self):
if not self.create_user_permission: return
@ -214,6 +215,15 @@ class Employee(NestedSet):
doc.validate_employee_creation()
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):
'''Return timeline for attendance'''
return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*)

View File

@ -11,12 +11,51 @@ class CallPopup {
'static': true,
'minimizable': true,
'fields': [{
'fieldname': 'caller_info',
'fieldtype': 'HTML'
'fieldname': 'name',
'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',
'fieldtype': 'Section Break',
'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',
'label': __('Last Communication'),
@ -24,13 +63,7 @@ class CallPopup {
'read_only': true,
'default': `<i class="text-muted">${__('No communication found.')}<i>`
}, {
'fieldtype': 'Small Text',
'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': 'Section Break',
}, {
'fieldtype': 'Small Text',
'label': __('Call Summary'),
@ -41,13 +74,21 @@ class CallPopup {
'click': () => {
const call_summary = this.dialog.get_value('call_summary');
if (!call_summary) return;
frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', {
'docname': this.call_log.id,
frappe.xcall('erpnext.communication.doctype.call_log.call_log.add_call_summary', {
'call_log': this.call_log.name,
'summary': call_summary,
}).then(() => {
this.close_modal();
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'
});
});
@ -55,71 +96,14 @@ class CallPopup {
}],
});
this.set_call_status();
this.make_caller_info_section();
this.dialog.get_close_btn().show();
this.make_last_interaction_section();
this.dialog.$body.addClass('call-popup');
this.dialog.set_secondary_action(this.close_modal.bind(this));
frappe.utils.play_sound('incoming-call');
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) {
let classes = `indicator ${color} ${blink ? 'blink': ''}`;
this.dialog.header.find('.indicator').attr('class', classes);
@ -129,7 +113,7 @@ class CallPopup {
let title = '';
call_status = call_status || this.call_log.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);
} else if (call_status === 'In Progress') {
title = __('Call Connected');
@ -164,13 +148,13 @@ class CallPopup {
if (!this.dialog.get_value('call_summary')) {
this.close_modal();
}
}, 10000);
}, 30000);
}
make_last_interaction_section() {
frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
'number': this.caller_number,
'reference_doc': this.contact
'contact': this.call_log.contact,
'lead': this.call_log.lead
}).then(data => {
const comm_field = this.dialog.get_field('last_communication');
if (data.last_communication) {
@ -182,15 +166,20 @@ class CallPopup {
const issue = data.last_issue;
const issue_field = this.dialog.get_field("last_issue");
issue_field.set_value(issue.subject);
issue_field.$wrapper.append(`<a class="text-medium" href="#List/Issue?customer=${issue.customer}">
${__('View all issues from {0}', [issue.customer])}
</a>`);
issue_field.$wrapper.append(`
<a class="text-medium" href="#List/Issue?customer=${issue.customer}">
${__('View all issues from {0}', [issue.customer])}
</a>
`);
}
});
}
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() {
frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => {
this.call_disconnected(call_log);