Merge branch 'develop' of github.com:frappe/erpnext into call-summary-dialog
diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py
new file mode 100644
index 0000000..26cb298
--- /dev/null
+++ b/erpnext/crm/doctype/utils.py
@@ -0,0 +1,81 @@
+import frappe
+from frappe import _
+import json
+
+@frappe.whitelist()
+def get_document_with_phone_number(number):
+	# finds contacts and leads
+	if not number: return
+	number = number[-10:]
+	number_filter = {
+		'phone': ['like', '%{}'.format(number)],
+		'mobile_no': ['like', '%{}'.format(number)]
+	}
+	contacts = frappe.get_all('Contact', or_filters=number_filter, limit=1)
+
+	if contacts:
+		return frappe.get_doc('Contact', contacts[0].name)
+
+	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 = ''
+		query_condition = ''
+		for link in reference_doc.links:
+			link = frappe._dict(link)
+			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)
+
+		if query_condition:
+			query_condition = query_condition[:-2]
+
+		last_communication = frappe.db.sql("""
+			SELECT `name`, `content`
+			FROM `tabCommunication`
+			WHERE {}
+			ORDER BY `modified`
+			LIMIT 1
+			""".format(query_condition))
+
+		if customer_name:
+			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', {
+			'reference_doctype': reference_doc.doctype,
+			'reference_name': reference_doc.name,
+			'sent_or_received': 'Received'
+		}, fields=['name', 'content'], limit=1)
+
+	return {
+		'last_communication': last_communication[0] if last_communication else None,
+		'last_issue': last_issue[0] if last_issue else None
+	}
+
+@frappe.whitelist()
+def add_call_summary(docname, summary):
+	communication = frappe.get_doc('Communication', docname)
+	content = _('Call Summary by {0}: {1}').format(
+		frappe.utils.get_fullname(frappe.session.user), summary)
+	if not communication.content:
+		communication.content = content
+	else:
+		communication.content += '\n' + content
+	communication.save(ignore_permissions=True)
+
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js
new file mode 100644
index 0000000..bfed491
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Exotel Settings', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
new file mode 100644
index 0000000..72f47b5
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
@@ -0,0 +1,61 @@
+{
+ "creation": "2019-05-21 07:41:53.536536",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "enabled",
+  "section_break_2",
+  "account_sid",
+  "api_key",
+  "api_token"
+ ],
+ "fields": [
+  {
+   "fieldname": "enabled",
+   "fieldtype": "Check",
+   "label": "Enabled"
+  },
+  {
+   "depends_on": "enabled",
+   "fieldname": "section_break_2",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "account_sid",
+   "fieldtype": "Data",
+   "label": "Account SID"
+  },
+  {
+   "fieldname": "api_token",
+   "fieldtype": "Data",
+   "label": "API Token"
+  },
+  {
+   "fieldname": "api_key",
+   "fieldtype": "Data",
+   "label": "API Key"
+  }
+ ],
+ "issingle": 1,
+ "modified": "2019-05-22 06:25:18.026997",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "Exotel Settings",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py
new file mode 100644
index 0000000..77de84c
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+import requests
+import frappe
+from frappe import _
+
+class ExotelSettings(Document):
+	def validate(self):
+		self.verify_credentials()
+
+	def verify_credentials(self):
+		if self.enabled:
+			response = requests.get('https://api.exotel.com/v1/Accounts/{sid}'
+				.format(sid = self.account_sid), auth=(self.api_key, self.api_token))
+			if response.status_code != 200:
+				frappe.throw(_("Invalid credentials"))
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py
new file mode 100644
index 0000000..5d85615
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/test_exotel_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestExotelSettings(unittest.TestCase):
+	pass
diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py
new file mode 100644
index 0000000..c70b094
--- /dev/null
+++ b/erpnext/erpnext_integrations/exotel_integration.py
@@ -0,0 +1,122 @@
+import frappe
+from erpnext.crm.doctype.utils import get_document_with_phone_number
+import requests
+
+# api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call
+
+@frappe.whitelist(allow_guest=True)
+def handle_incoming_call(*args, **kwargs):
+	exotel_settings = get_exotel_settings()
+	if not exotel_settings.enabled: return
+
+	employee_email = kwargs.get('AgentEmail')
+	status = kwargs.get('Status')
+
+	if status == 'free':
+		# call disconnected for agent
+		# "and get_call_status(kwargs.get('CallSid')) in ['in-progress']" - additional check to ensure if the call was redirected
+		frappe.publish_realtime('call_disconnected', user=employee_email)
+		return
+
+	call_log = get_call_log(kwargs)
+
+	data = frappe._dict({
+		'call_from': kwargs.get('CallFrom'),
+		'agent_email': kwargs.get('AgentEmail'),
+		'call_type': kwargs.get('Direction'),
+		'call_log': call_log,
+		'call_status_method': 'erpnext.erpnext_integrations.exotel_integration.get_call_status'
+	})
+
+	frappe.publish_realtime('show_call_popup', data, user=data.agent_email)
+
+@frappe.whitelist(allow_guest=True)
+def handle_end_call(*args, **kwargs):
+	close_call_log(kwargs)
+
+@frappe.whitelist(allow_guest=True)
+def handle_missed_call(*args, **kwargs):
+	close_call_log(kwargs)
+
+def close_call_log(call_payload):
+	call_log = get_call_log(call_payload)
+	if call_log:
+		call_log.status = 'Closed'
+		call_log.save(ignore_permissions=True)
+		frappe.db.commit()
+
+
+def get_call_log(call_payload, create_new_if_not_found=True):
+	communication = frappe.get_all('Communication', {
+		'communication_medium': 'Phone',
+		'call_id': call_payload.get('CallSid'),
+	}, limit=1)
+
+	if communication:
+		communication = frappe.get_doc('Communication', communication[0].name)
+		return communication
+	elif create_new_if_not_found:
+		communication = frappe.new_doc('Communication')
+		communication.subject = frappe._('Call from {}').format(call_payload.get("CallFrom"))
+		communication.communication_medium = 'Phone'
+		communication.phone_no = call_payload.get("CallFrom")
+		communication.comment_type = 'Info'
+		communication.communication_type = 'Communication'
+		communication.sent_or_received = 'Received'
+		communication.communication_date = call_payload.get('StartTime')
+		communication.call_id = call_payload.get('CallSid')
+		communication.status = 'Open'
+		communication.content = frappe._('Call from {}').format(call_payload.get("CallFrom"))
+		communication.save(ignore_permissions=True)
+		frappe.db.commit()
+		return communication
+
+@frappe.whitelist()
+def get_call_status(call_id):
+	print(call_id)
+	settings = get_exotel_settings()
+	response = requests.get('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/erpnext/Calls/{call_id}.json'.format(
+		api_key=settings.api_key,
+		api_token=settings.api_token,
+		call_id=call_id
+	))
+	status = response.json().get('Call', {}).get('Status')
+	return status
+
+@frappe.whitelist()
+def make_a_call(from_number, to_number, caller_id):
+	settings = get_exotel_settings()
+	response = requests.post('https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/Calls/connect.json?details=true'.format(
+		api_key=settings.api_key,
+		api_token=settings.api_token,
+		sid=settings.account_sid
+	), data={
+		'From': from_number,
+		'To': to_number,
+		'CallerId': caller_id
+	})
+
+	return response.json()
+
+def get_exotel_settings():
+	return frappe.get_single('Exotel Settings')
+
+@frappe.whitelist(allow_guest=True)
+def get_phone_numbers():
+	numbers = 'some number'
+	whitelist_numbers(numbers, 'for number')
+	return numbers
+
+def whitelist_numbers(numbers, caller_id):
+	settings = get_exotel_settings()
+	query = 'https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/CustomerWhitelist'.format(
+		api_key=settings.api_key,
+		api_token=settings.api_token,
+		sid=settings.account_sid
+	)
+	response = requests.post(query, data={
+		'VirtualNumber': caller_id,
+		'Number': numbers,
+	})
+
+	return response
\ No newline at end of file
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 45de6eb..818f336 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -1,7 +1,8 @@
 {
     "css/erpnext.css": [
         "public/less/erpnext.less",
-        "public/less/hub.less"
+        "public/less/hub.less",
+        "public/less/call_popup.less"
     ],
     "css/marketplace.css": [
         "public/less/hub.less"
@@ -48,7 +49,8 @@
 		"public/js/utils/customer_quick_entry.js",
         "public/js/education/student_button.html",
         "public/js/education/assessment_result_tool.html",
-        "public/js/hub/hub_factory.js"
+        "public/js/hub/hub_factory.js",
+        "public/js/call_popup/call_popup.js"
     ],
     "js/item-dashboard.min.js": [
         "stock/dashboard/item_dashboard.html",
diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js
new file mode 100644
index 0000000..5693ff0
--- /dev/null
+++ b/erpnext/public/js/call_popup/call_popup.js
@@ -0,0 +1,199 @@
+class CallPopup {
+	constructor({ call_from, call_log, call_status_method }) {
+		this.caller_number = call_from;
+		this.call_log = call_log;
+		this.call_status_method = call_status_method;
+		this.make();
+	}
+
+	make() {
+		this.dialog = new frappe.ui.Dialog({
+			'static': true,
+			'minimizable': true,
+			'fields': [{
+				'fieldname': 'caller_info',
+				'fieldtype': 'HTML'
+			}, {
+				'fielname': 'last_interaction',
+				'fieldtype': 'Section Break',
+			}, {
+				'fieldtype': 'Small Text',
+				'label': "Last Communication",
+				'fieldname': 'last_communication',
+				'read_only': true
+			}, {
+				'fieldname': 'last_communication_link',
+				'fieldtype': 'HTML',
+			}, {
+				'fieldtype': 'Small Text',
+				'label': "Last Issue",
+				'fieldname': 'last_issue',
+				'read_only': true
+			}, {
+				'fieldname': 'last_issue_link',
+				'fieldtype': 'HTML',
+			}, {
+				'fieldtype': 'Column Break',
+			}, {
+				'fieldtype': 'Small Text',
+				'label': 'Call Summary',
+				'fieldname': 'call_summary',
+			}, {
+				'fieldtype': 'Button',
+				'label': 'Submit',
+				'click': () => {
+					const values = this.dialog.get_values();
+					if (!values.call_summary) return
+					frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', {
+						'docname': this.call_log.name,
+						'summary': values.call_summary,
+					}).then(() => {
+						this.dialog.set_value('call_summary', '');
+					});
+				}
+			}],
+			on_minimize_toggle: () => {
+				this.set_call_status();
+			}
+		});
+		this.set_call_status(this.call_log.call_status);
+		this.make_caller_info_section();
+		this.dialog.get_close_btn().show();
+		this.setup_call_status_updater();
+		this.dialog.$body.addClass('call-popup');
+		this.dialog.set_secondary_action(() => {
+			clearInterval(this.updater);
+			delete erpnext.call_popup;
+			this.dialog.hide();
+		});
+		this.dialog.show();
+	}
+
+	make_caller_info_section() {
+		const wrapper = this.dialog.fields_dict['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) {
+				wrapper.append(`
+					<div class="caller-info">
+						<div>Unknown Number: <b>${this.caller_number}</b></div>
+						<a class="contact-link text-medium" href="#Form/Contact/New Contact?phone=${this.caller_number}">
+							${__('Create New Contact')}
+						</a>
+					</div>
+				`);
+			} else {
+				const link = contact.links ? contact.links[0] : null;
+				const contact_link = link ? frappe.utils.get_form_link(link.link_doctype, link.link_name, true): '';
+				wrapper.append(`
+					<div class="caller-info flex">
+						<img src="${contact.image}">
+						<div class='flex-column'>
+							<span>${contact.first_name} ${contact.last_name}</span>
+							<span>${contact.mobile_no}</span>
+							${contact_link}
+						</div>
+					</div>
+				`);
+				this.set_call_status();
+				this.make_last_interaction_section();
+			}
+		});
+	}
+
+	set_indicator(color, blink=false) {
+		this.dialog.header.find('.indicator').removeClass('hidden').toggleClass('blink', blink).addClass(color);
+	}
+
+	set_call_status(call_status) {
+		let title = '';
+		call_status = call_status || this.call_log.call_status;
+		if (['busy', 'completed'].includes(call_status) || !call_status) {
+			title = __('Incoming call from {0}',
+				[this.contact ? `${this.contact.first_name} ${this.contact.last_name}` : this.caller_number]);
+			this.set_indicator('blue', true);
+		} else if (call_status === 'in-progress') {
+			title = __('Call Connected');
+			this.set_indicator('yellow');
+		} else if (call_status === 'missed') {
+			this.set_indicator('red');
+			title = __('Call Missed');
+		} else if (call_status === 'disconnected') {
+			this.set_indicator('red');
+			title = __('Call Disconnected');
+		} else {
+			this.set_indicator('blue');
+			title = call_status;
+		}
+		this.dialog.set_title(title);
+	}
+
+	update(data) {
+		this.call_log = data.call_log;
+		this.set_call_status();
+	}
+
+	setup_call_status_updater() {
+		this.updater = setInterval(this.get_call_status.bind(this), 20000);
+	}
+
+	get_call_status() {
+		frappe.xcall(this.call_status_method, {
+			'call_id': this.call_log.call_id
+		}).then((call_status) => {
+			if (call_status === 'completed') {
+				clearInterval(this.updater);
+			}
+		});
+	}
+
+	disconnect_call() {
+		this.set_call_status('disconnected');
+		clearInterval(this.updater);
+	}
+
+	make_last_interaction_section() {
+		frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
+			'number': this.caller_number,
+			'reference_doc': this.contact
+		}).then(data => {
+			if (data.last_communication) {
+				const comm = data.last_communication;
+				// this.dialog.set_df_property('last_interaction', 'hidden', false);
+				const comm_field = this.dialog.fields_dict["last_communication"];
+				comm_field.set_value(comm.content);
+				comm_field.$wrapper.append(frappe.utils.get_form_link('Communication', comm.name));
+			}
+
+			if (data.last_issue) {
+				const issue = data.last_issue;
+				// this.dialog.set_df_property('last_interaction', 'hidden', false);
+				const issue_field = this.dialog.fields_dict["last_issue"];
+				issue_field.set_value(issue.subject);
+				issue_field.$wrapper
+					.append(`<a class="text-medium" href="#List/Issue/List?customer=${issue.customer}">View all issues from ${issue.customer}</a>`);
+			}
+		});
+	}
+}
+
+$(document).on('app_ready', function () {
+	frappe.realtime.on('show_call_popup', data => {
+		if (!erpnext.call_popup) {
+			erpnext.call_popup = new CallPopup(data);
+		} else {
+			erpnext.call_popup.update(data);
+			erpnext.call_popup.dialog.show();
+		}
+	});
+
+	frappe.realtime.on('call_disconnected', () => {
+		if (erpnext.call_popup) {
+			erpnext.call_popup.disconnect_call();
+		}
+	});
+});
diff --git a/erpnext/public/less/call_popup.less b/erpnext/public/less/call_popup.less
new file mode 100644
index 0000000..3f4ffef
--- /dev/null
+++ b/erpnext/public/less/call_popup.less
@@ -0,0 +1,13 @@
+.call-popup {
+	.caller-info {
+		padding: 0 15px;
+	}
+	img {
+		width: auto;
+		height: 100px;
+		margin-right: 15px;
+	}
+	a:hover {
+		text-decoration: underline;
+	}
+}
\ No newline at end of file