diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index 110030d3de..cfc08eb084 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -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 } \ No newline at end of file diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py index 66f1064e58..c9fdfbe447 100644 --- a/erpnext/communication/doctype/call_log/call_log.py +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -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')) + '

' + 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) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 347f727c50..9292b3a53a 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -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 \ No newline at end of file diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 9cfab15995..55532761c2 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -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 += '
' + 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() diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index c04cedce31..09c399e6aa 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -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')) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 47d1a68efc..be9a4fb264 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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" diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index cf418b0e8f..3fc330e2d2 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -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(*) diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 89657a1837..5278b322a4 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -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': `${__('No issue has been raised by the caller.')}` }, { 'fieldtype': 'Small Text', 'label': __('Last Communication'), @@ -24,13 +63,7 @@ class CallPopup { 'read_only': true, 'default': `${__('No communication found.')}` }, { - 'fieldtype': 'Small Text', - 'label': __('Last Issue'), - 'fieldname': 'last_issue', - 'read_only': true, - 'default': `${__('No issue raised by the customer.')}` - }, { - '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')}
${__('View call log')}`, + message: ` + ${__('Call Summary Saved')} +
+ + ${__('View call log')} + + `, 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(`
${__("Loading...")}
`); - 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(` -
- ${__('Unknown Number')}: ${this.caller_number} - -
- `).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 += `
${link.link_doctype}: ${frappe.utils.get_form_link(link.link_doctype, link.link_name, true)}
`; - }); - wrapper.append(` -
- ${frappe.avatar(null, 'avatar-xl', contact.name, contact.image || '')} -
-
${contact_name}
-
${contact.mobile_no || ''}
-
${contact.phone_no || ''}
- ${contact_links} -
-
- `); - } - 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(` - ${__('View all issues from {0}', [issue.customer])} - `); + issue_field.$wrapper.append(` + + ${__('View all issues from {0}', [issue.customer])} + + `); } }); } + 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);