feat: improved call log doctype

* Added links and some more fields into Call Log Doctype
* Display call info in the call log link pages
diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py
index 885ef05..4ccd9bd 100644
--- a/erpnext/crm/doctype/utils.py
+++ b/erpnext/crm/doctype/utils.py
@@ -81,4 +81,4 @@
 	# strip 0 from the start of the number for proper number comparisions
 	# eg. 07888383332 should match with 7888383332
 	number = number.lstrip('0')
-	return number
\ No newline at end of file
+	return number
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 5430221..c7efbba 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -272,12 +272,9 @@
 	},
 	"Contact": {
 		"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
-		"after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information",
+		"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
 		"validate": "erpnext.crm.utils.update_lead_phone_numbers"
 	},
-	"Lead": {
-		"after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information"
-	},
 	"Email Unsubscribe": {
 		"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
 	},
@@ -582,3 +579,7 @@
 		{'doctype': 'Hotel Room Type', 'index': 4}
 	]
 }
+
+additional_timeline_content = {
+	'*': ['erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs']
+}
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index b4a1cf8..b289785 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -42,7 +42,8 @@
 		"public/js/hub/hub_factory.js",
 		"public/js/call_popup/call_popup.js",
 		"public/js/utils/dimension_tree_filter.js",
-		"public/js/telephony.js"
+		"public/js/telephony.js",
+		"public/js/templates/call_link.html"
 	],
 	"js/item-dashboard.min.js": [
 		"stock/dashboard/item_dashboard.html",
diff --git a/erpnext/public/js/templates/call_link.html b/erpnext/public/js/templates/call_link.html
new file mode 100644
index 0000000..08bdf14
--- /dev/null
+++ b/erpnext/public/js/templates/call_link.html
@@ -0,0 +1,43 @@
+<div class="call-detail-wrapper">
+	<div class="left-arrow"></div>
+	<div class="head text-muted">
+		<span>
+			<i class="fa fa-phone"> </i>
+		<span>
+		<span> {{ type }} Call</span>
+		-
+		<span> {{ frappe.format(duration, { fieldtype: "Duration" }) }}</span>
+		-
+		<span> {{ comment_when(creation) }}</span>
+		-
+		<!-- <span> {{ status }}</span>
+		- -->
+		<a class="text-muted" href="#Form/Call Log/{{name}}">Details</a>
+		{% if (show_call_button) { %}
+			<a class="pull-right">Callback</a>
+		{% } %}
+	</div>
+	<div class="body padding">
+		{% if (type === "Incoming") { %}
+			<span> Incoming call from {{ from }}, received by {{ to }}</span>
+		{% } else { %}
+			<span> Outgoing Call made by {{ from }} to {{ to }}</span>
+		{% } %}
+		<hr>
+		<div class="summary">
+		{% if (summary) { %}
+			<span>{{ summary }}</span>
+		{% } else { %}
+			<i class="text-muted">{{ __("No Summary") }}</i>
+		{% } %}
+		</div>
+		{% if (recording_url) { %}
+		<div class="margin-top">
+				<audio
+					controls
+					src="{{ recording_url }}">
+				</audio>
+		</div>
+		{% } %}
+	</div>
+</div>
diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json
index 55ad2ba..1ecd884 100644
--- a/erpnext/telephony/doctype/call_log/call_log.json
+++ b/erpnext/telephony/doctype/call_log/call_log.json
@@ -8,20 +8,22 @@
   "id",
   "from",
   "to",
-  "column_break_3",
-  "received_by",
   "medium",
-  "caller_information",
-  "contact",
-  "contact_name",
-  "column_break_10",
+  "start_time",
+  "end_time",
+  "column_break_4",
+  "type",
   "customer",
-  "lead",
-  "lead_name",
-  "section_break_5",
   "status",
   "duration",
-  "recording_url"
+  "recording_url",
+  "recording_html",
+  "section_break_11",
+  "summary",
+  "section_break_19",
+  "links",
+  "column_break_3",
+  "section_break_5"
  ],
  "fields": [
   {
@@ -50,6 +52,7 @@
   {
    "fieldname": "to",
    "fieldtype": "Data",
+   "in_list_view": 1,
    "label": "To",
    "read_only": 1
   },
@@ -58,13 +61,13 @@
    "fieldtype": "Select",
    "in_list_view": 1,
    "label": "Status",
-   "options": "Ringing\nIn Progress\nCompleted\nMissed",
+   "options": "Ringing\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled",
    "read_only": 1
   },
   {
    "description": "Call Duration in seconds",
    "fieldname": "duration",
-   "fieldtype": "Int",
+   "fieldtype": "Duration",
    "in_list_view": 1,
    "label": "Duration",
    "read_only": 1
@@ -72,8 +75,7 @@
   {
    "fieldname": "recording_url",
    "fieldtype": "Data",
-   "label": "Recording URL",
-   "read_only": 1
+   "label": "Recording URL"
   },
   {
    "fieldname": "medium",
@@ -82,51 +84,52 @@
    "read_only": 1
   },
   {
-   "fieldname": "received_by",
-   "fieldtype": "Link",
-   "label": "Received By",
-   "options": "Employee",
+   "fieldname": "type",
+   "fieldtype": "Select",
+   "label": "Type",
+   "options": "Incoming\nOutgoing",
    "read_only": 1
   },
   {
-   "fieldname": "caller_information",
+   "fieldname": "recording_html",
+   "fieldtype": "HTML",
+   "label": "Recording HTML"
+  },
+  {
+   "fieldname": "section_break_19",
    "fieldtype": "Section Break",
-   "label": "Caller Information"
+   "label": "Reference"
   },
   {
-   "fieldname": "contact",
-   "fieldtype": "Link",
-   "label": "Contact",
-   "options": "Contact",
-   "read_only": 1
+   "fieldname": "links",
+   "fieldtype": "Table",
+   "label": "Links",
+   "options": "Dynamic Link"
   },
   {
-   "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",
+   "fieldname": "column_break_4",
    "fieldtype": "Column Break"
   },
   {
-   "fetch_from": "lead.lead_name",
-   "fieldname": "lead_name",
-   "fieldtype": "Data",
-   "hidden": 1,
-   "in_list_view": 1,
-   "label": "Lead Name",
+   "fieldname": "summary",
+   "fieldtype": "Small Text",
+   "label": "Call Summary"
+  },
+  {
+   "fieldname": "section_break_11",
+   "fieldtype": "Section Break",
+   "hide_border": 1
+  },
+  {
+   "fieldname": "start_time",
+   "fieldtype": "Datetime",
+   "label": "Start Time",
+   "read_only": 1
+  },
+  {
+   "fieldname": "end_time",
+   "fieldtype": "Datetime",
+   "label": "End Time",
    "read_only": 1
   },
   {
@@ -137,9 +140,10 @@
    "read_only": 1
   }
  ],
+ "in_create": 1,
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2020-11-25 14:32:44.407815",
+ "modified": "2021-01-13 12:28:20.288985",
  "modified_by": "Administrator",
  "module": "Telephony",
  "name": "Call Log",
@@ -162,8 +166,8 @@
    "role": "Employee"
   }
  ],
- "sort_field": "modified",
- "sort_order": "ASC",
+ "sort_field": "creation",
+ "sort_order": "DESC",
  "title_field": "from",
  "track_changes": 1,
  "track_views": 1
diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 296473e..a277a5f 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -8,40 +8,83 @@
 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)
+			# 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
+			if frappe.conf.developer_mode:
+				self.add_comment(text=f"""
+					Scheduled Employees: {scheduled_employees}
+					Matching Employee: {employee_emails}
+					Show Popup To: {emails}
+				""")
 
-		for email in emails:
-			frappe.publish_realtime('show_call_popup', self, user=email)
+			if employee_emails and not emails:
+				self.add_comment(text=_("No employee was scheduled for call popup"))
+
+			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 @@
 
 	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
+		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
+				)
+			)
 
-		filters = frappe._dict({
-			'from': ['like', '%{}'.format(number)],
-			fieldname: ''
+			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)