Merge branch 'develop' into develop
diff --git a/erpnext/crm/doctype/appointment/__init__.py b/erpnext/crm/doctype/appointment/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/__init__.py
diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js
new file mode 100644
index 0000000..4e41047
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/appointment.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('Appointment', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
new file mode 100644
index 0000000..2d695f31
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -0,0 +1,110 @@
+{
+ "autoname": "format:APMT-{customer_name}-{####}",
+ "creation": "2019-08-27 10:48:27.926283",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "scheduled_time",
+  "status",
+  "customer_details_section",
+  "customer_name",
+  "customer_phone_number",
+  "customer_skype",
+  "customer_details",
+  "lead",
+  "calendar_event"
+ ],
+ "fields": [
+  {
+   "fieldname": "customer_details_section",
+   "fieldtype": "Section Break",
+   "label": "Customer Details"
+  },
+  {
+   "fieldname": "customer_name",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Name",
+   "reqd": 1
+  },
+  {
+   "fieldname": "customer_phone_number",
+   "fieldtype": "Data",
+   "label": "Phone Number",
+   "reqd": 1
+  },
+  {
+   "fieldname": "customer_skype",
+   "fieldtype": "Data",
+   "label": "Skype ID",
+   "reqd": 1
+  },
+  {
+   "fieldname": "customer_details",
+   "fieldtype": "Long Text",
+   "label": "Details"
+  },
+  {
+   "fieldname": "scheduled_time",
+   "fieldtype": "Datetime",
+   "in_list_view": 1,
+   "label": "Scheduled Time",
+   "reqd": 1
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "label": "Status",
+   "options": "Open\nClosed",
+   "reqd": 1
+  },
+  {
+   "fieldname": "lead",
+   "fieldtype": "Link",
+   "label": "Lead",
+   "options": "Lead",
+   "reqd": 1
+  },
+  {
+   "fieldname": "calendar_event",
+   "fieldtype": "Link",
+   "label": "Calendar Event",
+   "options": "Event"
+  }
+ ],
+ "modified": "2019-09-13 15:25:49.362246",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Appointment",
+ "name_case": "UPPER CASE",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Guest",
+   "share": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
new file mode 100644
index 0000000..614a43c
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+from collections import Counter
+from datetime import timedelta
+
+import frappe
+from frappe.model.document import Document
+from frappe.desk.form.assign_to import add as add_assignemnt
+
+
+class Appointment(Document):
+	def validate(self):
+		number_of_appointments_in_same_slot = frappe.db.count('Appointment',filters={'scheduled_time':self.scheduled_time})
+		settings = frappe.get_doc('Appointment Booking Settings')
+		if(number_of_appointments_in_same_slot >= settings.number_of_agents):
+			frappe.throw('Time slot is not available')
+
+	def before_insert(self):
+		appointment_event = frappe.new_doc('Event')
+		appointment_event = frappe.get_doc({
+			'doctype': 'Event',
+			'subject': ' '.join(['Appointment with', self.customer_name]),
+			'starts_on': self.scheduled_time,
+			'status': 'Open',
+			'type': 'Private',
+			'event_participants': [dict(reference_doctype="Lead", reference_docname=self.lead)]
+		})
+		appointment_event.insert(ignore_permissions=True)
+		self.calendar_event = appointment_event.name
+
+	def after_insert(self):
+		available_agents = _get_agents_sorted_by_asc_workload()
+		for agent in available_agents:
+			if(_check_agent_availability(agent, self.scheduled_time)):
+				agent = agent[0]
+				agent = frappe.json.loads(agent)[0]
+				add_assignemnt({
+					'doctype':self.doctype,
+					'name':self.name,
+					'assign_to':agent
+				})
+				employee = _get_employee_from_user(agent)
+				if employee:
+					print(employee)
+					calendar_event = frappe.get_doc('Event', self.calendar_event)
+					calendar_event.append('event_participants', dict(
+						reference_doctype='Employee',
+						reference_docname=employee[0].name))
+					print(calendar_event)
+					calendar_event.save()
+				break
+
+
+def _get_agents_sorted_by_asc_workload():
+	appointments = frappe.db.get_list('Appointment', fields='*')
+	agent_list = _get_agent_list_as_strings()
+	
+	if not appointments:
+		return agent_list
+	
+	appointment_counter = Counter(agent_list)
+	
+	for appointment in appointments:
+		assigned_to = frappe.parse_json(appointment._assign)
+		print(assigned_to)
+		if appointment._assign == '[]' or not appointment._assign:
+			continue
+		if assigned_to[0] in agent_list:
+			appointment_counter[assigned_to[0]] += 1
+	
+	sorted_agent_list = appointment_counter.most_common()
+	sorted_agent_list.reverse()
+	
+	return sorted_agent_list
+
+
+def _get_agent_list_as_strings():
+	agent_list_as_strings = []
+	agent_list = frappe.get_doc('Appointment Booking Settings').agent_list
+	
+	for agent in agent_list:
+		agent_list_as_strings.append(agent.user)
+	
+	return agent_list_as_strings
+
+
+def _check_agent_availability(agent_email,scheduled_time):
+	appointments_at_scheduled_time = frappe.get_list('Appointment', filters={'scheduled_time': scheduled_time})
+	for appointment in appointemnts_at_scheduled_time:
+		if appointment._assign == agent_email:
+			return False
+	return True
+
+
+def _get_employee_from_user(user):
+	return frappe.get_list('Employee', fields='*',filters={'user_id':user})
diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py
new file mode 100644
index 0000000..c1a1c4f
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/test_appointment.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+import datetime
+
+
+def create_appointments(number):
+    for i in range(1, number):
+        frappe.get_doc({
+            'doctype': 'Appointment',
+            'scheduled_time': datetime.datetime.min,
+            'customer_name': 'Test Customer'+str(i),
+            'customer_phone_number': '8088',
+            'customer_skype': 'test'+str(i),
+        })
+
+def delete_appointments():
+        doc_list = frappe.get_list('Appointment',filters={'scheduled_time':datetime.datetime.min,'customer_phone_number':'8088'})
+        for doc in doc_list:
+            doc.delete()
+
+
+class TestAppointment(unittest.TestCase):
+    pass
diff --git a/erpnext/crm/doctype/appointment_booking_settings/__init__.py b/erpnext/crm/doctype/appointment_booking_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/__init__.py
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js
new file mode 100644
index 0000000..2642e6e
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js
@@ -0,0 +1,13 @@
+// frappe.ui.form.on('Availability Of Slots', 'from_time', check_time)
+// frappe.ui.form.on('Availability Of Slots', 'to_time', check_time)
+
+frappe.ui.form.on('Appointment Booking Settings', 'validate',check_times);
+function check_times(frm) {
+	$.each(frm.doc.availability_of_slots || [], function (i, d) {
+		let from_time = Date.parse('01/01/2019 ' + d.from_time);
+		let to_time = Date.parse('01/01/2019 ' + d.to_time);
+		if (from_time > to_time) {
+			frappe.throw(__(`In row ${i + 1} of Availability Of Slots : "To Time" must be later than "From Time"`));
+		}
+	});
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
new file mode 100644
index 0000000..c59a2e4
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
@@ -0,0 +1,96 @@
+{
+ "creation": "2019-08-27 10:56:48.309824",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "availability_of_slots",
+  "number_of_agents",
+  "holiday_list",
+  "appointment_duration",
+  "email_reminders",
+  "advance_booking_days",
+  "agent_list"
+ ],
+ "fields": [
+  {
+   "fieldname": "availability_of_slots",
+   "fieldtype": "Table",
+   "label": "Availability Of Slots",
+   "options": "Availability Of Slots",
+   "reqd": 1
+  },
+  {
+   "default": "1",
+   "fieldname": "number_of_agents",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "No. Of Agents",
+   "reqd": 1
+  },
+  {
+   "fieldname": "holiday_list",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Holiday List",
+   "options": "Holiday List",
+   "reqd": 1
+  },
+  {
+   "default": "60",
+   "fieldname": "appointment_duration",
+   "fieldtype": "Int",
+   "label": "Appointment Duration (In Minutes)",
+   "reqd": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "email_reminders",
+   "fieldtype": "Check",
+   "label": "Email Reminders"
+  },
+  {
+   "default": "7",
+   "fieldname": "advance_booking_days",
+   "fieldtype": "Int",
+   "label": "Number of days appointments can be booked in advance",
+   "reqd": 1
+  },
+  {
+   "fieldname": "agent_list",
+   "fieldtype": "Table MultiSelect",
+   "label": "Agents",
+   "options": "Assignment Rule User",
+   "reqd": 1
+  }
+ ],
+ "issingle": 1,
+ "modified": "2019-09-13 11:31:26.654516",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Appointment Booking Settings",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "Guest",
+   "share": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py
new file mode 100644
index 0000000..8f1fb14
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py
@@ -0,0 +1,30 @@
+# -*- 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 import _
+import datetime
+from frappe.model.document import Document
+
+class AppointmentBookingSettings(Document):
+	def validate(self):
+		# Day of week should not be repeated
+		list_of_days = []
+		date = '01/01/1970 '
+		format_string = "%d/%m/%Y %H:%M:%S"
+		for record in self.availability_of_slots:
+			list_of_days.append(record.day_of_week)
+			# Difference between from_time and to_time is multiple of appointment_duration
+			from_time = datetime.datetime.strptime(date+record.from_time,format_string)
+			to_time = datetime.datetime.strptime(date+record.to_time,format_string)
+			timedelta = to_time-from_time
+			if(from_time>to_time):
+				frappe.throw('From Time cannot be later than To Time for '+record.day_of_week)
+			if timedelta.total_seconds() % (self.appointment_duration*60):
+				frappe.throw('The difference between from time and To Time must be a multiple of Appointment ')
+		set_of_days = set(list_of_days)
+		if len(list_of_days) > len(set_of_days):
+			frappe.throw(_('Days of week must be unique'))
+	
diff --git a/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py
new file mode 100644
index 0000000..3dc3c39
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_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 TestAppointmentBookingSettings(unittest.TestCase):
+	pass
diff --git a/erpnext/crm/doctype/availability_of_slots/__init__.py b/erpnext/crm/doctype/availability_of_slots/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/availability_of_slots/__init__.py
diff --git a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json
new file mode 100644
index 0000000..b54af8d
--- /dev/null
+++ b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json
@@ -0,0 +1,46 @@
+{
+ "creation": "2019-09-10 15:02:05.779434",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "day_of_week",
+  "from_time",
+  "to_time"
+ ],
+ "fields": [
+  {
+   "fieldname": "day_of_week",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Day Of Week",
+   "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
+   "reqd": 1
+  },
+  {
+   "fieldname": "from_time",
+   "fieldtype": "Time",
+   "in_list_view": 1,
+   "label": "From Time",
+   "reqd": 1
+  },
+  {
+   "fieldname": "to_time",
+   "fieldtype": "Time",
+   "in_list_view": 1,
+   "label": "To Time",
+   "reqd": 1
+  }
+ ],
+ "istable": 1,
+ "modified": "2019-09-10 15:05:20.406855",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Availability Of Slots",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py
new file mode 100644
index 0000000..8258471
--- /dev/null
+++ b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py
@@ -0,0 +1,10 @@
+# -*- 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
+
+class AvailabilityOfSlots(Document):
+	pass
diff --git a/erpnext/crm/doctype/availabilty_of_slots/__init__.py b/erpnext/crm/doctype/availabilty_of_slots/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/availabilty_of_slots/__init__.py
diff --git a/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json b/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json
new file mode 100644
index 0000000..d26f7ce
--- /dev/null
+++ b/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json
@@ -0,0 +1,46 @@
+{
+ "creation": "2019-08-27 10:52:54.204677",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "day_of_week",
+  "from_time",
+  "to_time"
+ ],
+ "fields": [
+  {
+   "fieldname": "day_of_week",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Day Of Week",
+   "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
+   "reqd": 1
+  },
+  {
+   "fieldname": "from_time",
+   "fieldtype": "Time",
+   "in_list_view": 1,
+   "label": "From Time ",
+   "reqd": 1
+  },
+  {
+   "fieldname": "to_time",
+   "fieldtype": "Time",
+   "in_list_view": 1,
+   "label": "To Time",
+   "reqd": 1
+  }
+ ],
+ "istable": 1,
+ "modified": "2019-08-27 10:52:54.204677",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Availabilty Of Slots",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py b/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py
new file mode 100644
index 0000000..bd76480
--- /dev/null
+++ b/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py
@@ -0,0 +1,11 @@
+# -*- 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
+
+
+class AvailabiltyOfSlots(Document):
+    pass
diff --git a/erpnext/crm/doctype/timezone/__init__.py b/erpnext/crm/doctype/timezone/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/__init__.py
diff --git a/erpnext/crm/doctype/timezone/test_timezone.py b/erpnext/crm/doctype/timezone/test_timezone.py
new file mode 100644
index 0000000..92a8889
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/test_timezone.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 TestTimezone(unittest.TestCase):
+	pass
diff --git a/erpnext/crm/doctype/timezone/timezone.js b/erpnext/crm/doctype/timezone/timezone.js
new file mode 100644
index 0000000..4dc57db
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/timezone.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('Timezone', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/crm/doctype/timezone/timezone.json b/erpnext/crm/doctype/timezone/timezone.json
new file mode 100644
index 0000000..b998e6c
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/timezone.json
@@ -0,0 +1,61 @@
+{
+ "autoname": "field:timezone_name",
+ "creation": "2019-08-27 11:39:30.328670",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "offset",
+  "timezone_name"
+ ],
+ "fields": [
+  {
+   "fieldname": "offset",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Offset In Minutes",
+   "reqd": 1
+  },
+  {
+   "fieldname": "timezone_name",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Name",
+   "reqd": 1,
+   "unique": 1
+  }
+ ],
+ "modified": "2019-09-03 11:59:27.729561",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Timezone",
+ "name_case": "Title Case",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Guest",
+   "share": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/timezone/timezone.py b/erpnext/crm/doctype/timezone/timezone.py
new file mode 100644
index 0000000..2c77023
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/timezone.py
@@ -0,0 +1,17 @@
+# -*- 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
+
+
+class Timezone(Document):
+    def validate(self):
+        if self.offset > 720 or self.offset < -720:
+            frappe.throw(
+                'Timezone offsets must be between -720 and +720 minutes')
+        if frappe.db.exists({'doctype':'Timezone','offset':self.offset}):
+            frappe.throw(
+                'Timezone offsets need to be unique')
\ No newline at end of file
diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book-appointment/index.css
new file mode 100644
index 0000000..a6e6313
--- /dev/null
+++ b/erpnext/www/book-appointment/index.css
@@ -0,0 +1,38 @@
+.time-slot {
+    margin-bottom: 2em;
+    margin-left: 0.5em;
+    margin-right: 0.5em;
+    border-radius: 0.4em;
+    cursor: pointer;
+    border: 0.5px solid #cccccc;
+    min-height: 75px;
+    padding: 0.5em 1em;
+}
+
+.time-slot:hover {
+    background: #ddd;
+}
+
+.time-slot.unavailable {
+    background: #CBD5E0;
+    cursor: not-allowed;
+    color: #718096
+}
+
+.time-slot.unavailable .text-muted {
+    color: #718096
+}
+
+input[type="radio"] {
+    visibility: hidden;
+    display: none;
+}
+
+.time-slot.selected {
+    color: white;
+    background: #5e64ff;
+}
+
+.time-slot.selected .text-muted {
+    color: #EDF2F7 !important;
+}
diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html
new file mode 100644
index 0000000..2e03213
--- /dev/null
+++ b/erpnext/www/book-appointment/index.html
@@ -0,0 +1,67 @@
+{% extends "templates/web.html" %}
+
+{% block title %}{{ _("Book Appointment") }}{% endblock %}
+
+{% block script %}
+<script src="assets/js/moment-bundle.min.js"></script>
+<script src="book-appointment/index.js"></script>
+{% endblock %}
+
+{% block page_content %}
+<div class="container">
+    <!-- title: Book an appointment -->
+    <div id="select-date-time">
+        <div class="text-center mb-5">
+            <h3>Book an appointment</h3>
+            <p class="lead text-muted" id="lead-text">Select the date and your timezone</p>
+        </div>
+        <div class="row justify-content-center mt-3">
+            <div class="col-md-6 align-self-center ">
+                <div class="row">
+                    <input type="date" oninput="on_date_or_timezone_select()" name="appointment-date"
+                        id="appointment-date" class="form-control mt-3 col-md m-3">
+                    <select name="appointment-timezone" oninput="on_date_or_timezone_select()" id="appointment-timezone"
+                        class="form-control mt-3 col-md">
+                    </select>
+                </div>
+            </div>
+        </div>
+        <div class="row mt-3" id="timeslot-container">
+
+        </div>
+        <div class="row justify-content-center mt-3">
+            <div class="col-md-4">
+                <button class="btn btn-primary form-control" id="next-button">Next</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--Enter Details-->
+<div id="enter-details">
+    <div class="text-center mb-5">
+        <h3>Add details</h3>
+        <p class="lead">Selected date is <span class="date-span"></span> at <span class="time-span">
+            </span></p>
+    </div>
+    <div class="row justify-content-center mt-3">
+        <div class="col-md-4 align-items-center">
+            <input class="form-control mt-3" type="text" name="customer_name" id="customer_name" placeholder="Your Name"
+                required>
+            <input class="form-control mt-3" type="tel" name="customer_number" id="customer_number"
+                placeholder="Contact Number" required>
+            <input class="form-control mt-3" type="text" name="customer_skype" id="customer_skype" placeholder="Skype"
+                required>
+            <input class="form-control mt-3"type="email" name="customer_email" id="customer_email" 
+                placeholder="Email Address">
+            <textarea class="form-control mt-3" name="customer_notes" id="customer_notes" cols="30" rows="10"
+                placeholder="Notes"></textarea>
+            <div class="row mt-3 ">
+                <div class="col-md"><button class="btn btn-dark form-control" onclick="initialise_select_date()">Go back</button></div>
+                <div class="col-md"><button class="btn btn-primary form-control " onclick="submit()" id="submit-button">Submit</button></div>
+            </div>
+        </div>
+    </div>
+</div>
+</div>
+
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js
new file mode 100644
index 0000000..5302d1b
--- /dev/null
+++ b/erpnext/www/book-appointment/index.js
@@ -0,0 +1,213 @@
+
+frappe.ready(() => {
+    initialise_select_date()
+})
+window.holiday_list = [];
+
+async function initialise_select_date() {
+    navigate_to_page(1);
+    await get_global_variables();
+    setup_date_picker();
+    setup_timezone_selector();
+    hide_next_button();
+}
+
+async function get_global_variables() {
+    window.appointment_settings = (await frappe.call({
+        method: 'erpnext.www.book-appointment.index.get_appointment_settings'
+    })).message
+    window.timezones = (await frappe.call({
+        method: 'erpnext.www.book-appointment.index.get_timezones'
+    })).message;
+    window.holiday_list = (await frappe.call({
+        method: 'erpnext.www.book-appointment.index.get_holiday_list',
+        args: {
+            'holiday_list_name': window.appointment_settings.holiday_list
+        }
+    })).message;
+}
+
+function setup_timezone_selector() {
+    let timezones_element = document.getElementById('appointment-timezone');
+    var offset = new Date().getTimezoneOffset();
+    window.timezones.forEach(timezone => {
+        var opt = document.createElement('option');
+        opt.value = timezone.offset;
+        opt.innerHTML = timezone.timezone_name;
+        opt.defaultSelected = (offset == timezone.offset)
+        timezones_element.appendChild(opt)
+    });
+}
+
+function setup_date_picker() {
+    let date_picker = document.getElementById('appointment-date');
+    let today = new Date();
+    date_picker.min = today.toISOString().substr(0, 10);
+    today.setDate(today.getDate() + window.appointment_settings.advance_booking_days);
+    date_picker.max = today.toISOString().substr(0,10);
+}
+
+function hide_next_button(){
+    let next_button = document.getElementById('next-button');
+    next_button.disabled = true;
+    next_button.onclick = ()=>{frappe.msgprint("Please select a date and time")};
+}
+
+function show_next_button(){
+    let next_button = document.getElementById('next-button');
+    next_button.disabled = false;
+    next_button.onclick = setup_details_page;
+}
+
+function on_date_or_timezone_select() {
+    let date_picker = document.getElementById('appointment-date');
+    let timezone = document.getElementById('appointment-timezone');
+    if (date_picker.value === '') {
+        clear_time_slots();
+        hide_next_button();
+        frappe.throw('Please select a date');
+    }
+    window.selected_date = date_picker.value;
+    window.selected_timezone = timezone.value;
+    update_time_slots(date_picker.value, timezone.value);
+    let lead_text = document.getElementById('lead-text');
+    lead_text.innerHTML = "Select Time"
+}
+
+async function get_time_slots(date, timezone) {
+    let slots = (await frappe.call({
+        method: 'erpnext.www.book-appointment.index.get_appointment_slots',
+        args: {
+            date: date,
+            timezone: timezone
+        }
+    })).message;
+    return slots;
+}
+
+async function update_time_slots(selected_date, selected_timezone) {
+    let timeslot_container = document.getElementById('timeslot-container');
+    window.slots = await get_time_slots(selected_date, selected_timezone);
+    clear_time_slots();
+    if (window.slots.length <= 0) {
+        let message_div = document.createElement('p');
+        message_div.innerHTML = "There are no slots available on this date";
+        timeslot_container.appendChild(message_div);
+        return
+    }
+    window.slots.forEach((slot,index) => {
+        if(index%8==0){
+            let break_element = document.createElement('div');
+            break_element.classList.add('w-100');
+            timeslot_container.appendChild(break_element);
+        }
+        let start_time = new Date(slot.time)
+        var timeslot_div = document.createElement('div');
+        timeslot_div.classList.add('time-slot');
+        timeslot_div.classList.add('col-md');
+        if (!slot.availability) {
+            timeslot_div.classList.add('unavailable')
+        }
+        timeslot_div.innerHTML = get_slot_layout(start_time);
+        timeslot_div.id = slot.time.substr(11, 20);
+        timeslot_div.addEventListener('click', select_time);
+        timeslot_container.appendChild(timeslot_div);
+    });
+    set_default_timeslot();
+}
+
+function clear_time_slots() {
+    let timeslot_container = document.getElementById('timeslot-container');
+    while (timeslot_container.firstChild) {
+        timeslot_container.removeChild(timeslot_container.firstChild)
+    }
+}
+
+function get_slot_layout(time) {
+    time = new Date(time)
+    let start_time_string = moment(time).format("LT");
+    let end_time = moment(time).add(window.appointment_settings.appointment_duration,'minutes');
+    let end_time_string = end_time.format("LT");
+    return `<span style="font-size: 1.2em;">${start_time_string}</span><br><span class="text-muted small">to ${end_time_string}</span>`;
+}
+
+function select_time() {
+    if (this.classList.contains("unavailable")) {
+        return
+    }
+    try {
+        selected_element = document.getElementsByClassName('selected')[0]
+    } catch (e) {
+        this.classList.add("selected")
+    }
+    window.selected_time = this.id
+    selected_element.classList.remove("selected");
+    this.classList.add("selected");
+    show_next_button();
+}
+
+function set_default_timeslot() {
+    let timeslots = document.getElementsByClassName('time-slot')
+    for (let i = 0; i < timeslots.length; i++) {
+        const timeslot = timeslots[i];
+        if (!timeslot.classList.contains('unavailable')) {
+            timeslot.classList.add("selected");
+            break;
+        }
+    }
+}
+
+function navigate_to_page(page_number){
+    let page1 = document.getElementById('select-date-time');
+    let page2 = document.getElementById('enter-details');
+    switch(page_number){
+        case 1: 
+            page1.style.display = 'block';
+            page2.style.display = 'none';
+            break;
+        case 2:
+            page1.style.display = 'none';
+            page2.style.display = 'block';
+            break;
+        default:
+            console.log("That's not a valid page")
+    }
+}
+
+function setup_details_page(){
+    navigate_to_page(2)
+    let date_container = document.getElementsByClassName('date-span')[0];
+    let time_container = document.getElementsByClassName('time-span')[0];
+    date_container.innerHTML = moment(window.selected_date).format("MMM Do YYYY");
+    time_container.innerHTML = moment(window.selected_time,"HH:mm:ss").format("LT");
+}
+
+async function submit() {
+    // form validation here
+    form_validation();
+    let appointment = (await frappe.call({
+        method: 'erpnext.www.book-appointment.index.create_appointment',
+        args: {
+            'date': window.selected_date,
+            'time': window.selected_time,
+            'contact': window.contact
+        }
+    })).message;
+    frappe.msgprint(__('Appointment Created Successfully'));
+    let button = document.getElementById('submit-button');
+    button.disabled = true;
+    button.onclick = () => { console.log('This should never have happened') }
+} 
+
+function form_validation(){
+    var date = window.selected_date;
+    var time = window.selected_time;
+    contact = {};
+    contact.name = document.getElementById('customer_name').value;
+    contact.number = document.getElementById('customer_number').value;
+    contact.skype = document.getElementById('customer_skype').value;
+    contact.notes = document.getElementById('customer_notes').value;
+    contact.email = document.getElementById('customer_email').value;
+    window.contact = contact
+    console.log({ date, time, contact });
+}
diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py
new file mode 100644
index 0000000..530445f
--- /dev/null
+++ b/erpnext/www/book-appointment/index.py
@@ -0,0 +1,163 @@
+import frappe
+import datetime
+import json
+
+no_cache = 1
+
+
+@frappe.whitelist(allow_guest=True)
+def get_appointment_settings():
+    settings = frappe.get_doc('Appointment Booking Settings')
+    return settings
+
+
+@frappe.whitelist(allow_guest=True)
+def get_holiday_list(holiday_list_name):
+    holiday_list = frappe.get_doc('Holiday List', holiday_list_name)
+    return holiday_list
+
+
+@frappe.whitelist(allow_guest=True)
+def get_timezones():
+    timezones = frappe.get_list('Timezone', fields='*')
+    return timezones
+
+
+@frappe.whitelist(allow_guest=True)
+def get_appointment_slots(date, timezone):
+    timezone = int(timezone)
+    format_string = '%Y-%m-%d %H:%M:%S'
+    query_start_time = datetime.datetime.strptime(
+        date + ' 00:00:00', format_string)
+    query_end_time = datetime.datetime.strptime(
+        date + ' 23:59:59', format_string)
+    query_start_time = _convert_to_ist(query_start_time, timezone)
+    query_end_time = _convert_to_ist(query_end_time, timezone)
+    # Database queries
+    settings = frappe.get_doc('Appointment Booking Settings')
+    holiday_list = frappe.get_doc('Holiday List', settings.holiday_list)
+    timeslots = get_available_slots_between(
+        query_start_time, query_end_time, settings)
+
+    # Filter timeslots based on date
+    converted_timeslots = []
+    for timeslot in timeslots:
+        # Check if holiday
+        if _is_holiday(timeslot.date(), holiday_list):
+            converted_timeslots.append(
+                dict(time=_convert_to_tz(timeslot, timezone), availability=False))
+            continue
+        # Check availability
+        if check_availabilty(timeslot, settings):
+            converted_timeslots.append(
+                dict(time=_convert_to_tz(timeslot, timezone), availability=True))
+        else:
+            converted_timeslots.append(
+                dict(time=_convert_to_tz(timeslot, timezone), availability=False))
+    date_required = datetime.datetime.strptime(
+        date + ' 00:00:00', format_string).date()
+    converted_timeslots = filter_timeslots(date_required, converted_timeslots)
+    return converted_timeslots
+
+
+def get_available_slots_between(query_start_time, query_end_time, settings):
+    records = _get_records(query_start_time, query_end_time, settings)
+    timeslots = []
+    appointment_duration = datetime.timedelta(
+        minutes=settings.appointment_duration)
+    for record in records:
+        if record.day_of_week == WEEKDAYS[query_start_time.weekday()]:
+            current_time = _deltatime_to_datetime(
+                query_start_time, record.from_time)
+            end_time = _deltatime_to_datetime(
+                query_start_time, record.to_time)
+        else:
+            current_time = _deltatime_to_datetime(
+                query_end_time, record.from_time)
+            end_time = _deltatime_to_datetime(
+                query_end_time, record.to_time)
+        while current_time + appointment_duration <= end_time:
+            timeslots.append(current_time)
+            current_time += appointment_duration
+    return timeslots
+
+
+@frappe.whitelist(allow_guest=True)
+def create_appointment(date, time, contact):
+    appointment = frappe.new_doc('Appointment')
+    format_string = '%Y-%m-%d %H:%M:%S'
+    appointment.scheduled_time = datetime.datetime.strptime(
+        date+" "+time, format_string)
+    contact = json.loads(contact)
+    appointment.customer_name = contact['name']
+    appointment.customer_phone_number = contact['number']
+    appointment.customer_skype = contact['skype']
+    appointment.customer_details = contact['notes']
+    appointment.status = 'Open'
+    appointment.lead = find_lead_by_email(contact['email']).name
+    appointment.insert()
+
+def find_lead_by_email(email):
+    if frappe.db.exists({
+        'doctype':'Lead',
+        'email_id':email
+    }):
+        return frappe.get_list('Lead',filters={'email_id':email})[0]
+    frappe.throw('Email ID not associated with any Lead. Please make sure to use the email address you got this mail on')
+
+# Helper Functions
+def filter_timeslots(date, timeslots):
+    filtered_timeslots = []
+    for timeslot in timeslots:
+        if(timeslot['time'].date() == date):
+            filtered_timeslots.append(timeslot)
+    return filtered_timeslots
+
+
+def check_availabilty(timeslot, settings):
+    return frappe.db.count('Appointment', {'scheduled_time': timeslot}) < settings.number_of_agents
+
+
+def _is_holiday(date, holiday_list):
+    for holiday in holiday_list.holidays:
+        if holiday.holiday_date == date:
+            return True
+    return False
+
+
+def _get_records(start_time, end_time, settings):
+    records = []
+    for record in settings.availability_of_slots:
+        if record.day_of_week == WEEKDAYS[start_time.weekday()] or record.day_of_week == WEEKDAYS[end_time.weekday()]:
+            records.append(record)
+    return records
+
+
+def _deltatime_to_datetime(date, deltatime):
+    time = (datetime.datetime.min + deltatime).time()
+    return datetime.datetime.combine(date.date(), time)
+
+
+def _datetime_to_deltatime(date_time):
+    midnight = datetime.datetime.combine(date_time.date(), datetime.time.min)
+    return (date_time-midnight)
+
+
+def _convert_to_ist(datetime_object, timezone):
+    offset = datetime.timedelta(minutes=timezone)
+    datetime_object = datetime_object + offset
+    offset = datetime.timedelta(minutes=-330)
+    datetime_object = datetime_object - offset
+    return datetime_object
+
+
+def _convert_to_tz(datetime_object, timezone):
+    offset = datetime.timedelta(minutes=timezone)
+    datetime_object = datetime_object - offset
+    offset = datetime.timedelta(minutes=-330)
+    datetime_object = datetime_object + offset
+    return datetime_object
+
+
+WEEKDAYS = ["Monday", "Tuesday", "Wednesday",
+            "Thursday", "Friday", "Saturday", "Sunday"]