diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7bd6d72cb46b414f1e5dd2656ac20a7dbcb13cc7..6e2821df18945454800d7d2905425df6c26d50cf 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json
 from .exceptions import *
 from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template
 
-__version__ = '8.7.11'
+__version__ = '8.8.0'
 __title__ = "Frappe Framework"
 
 local = Local()
diff --git a/frappe/auth.py b/frappe/auth.py
index 8845b3e790141d8f750d45ebeb72091ee89fc745..bd510b9fcd0ddf517d76a9fe8164fa8d0bb2699a 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -16,9 +16,14 @@ from frappe.modules.patch_handler import check_session_stopped
 from frappe.translate import get_lang_code
 from frappe.utils.password import check_password
 from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log
+from frappe.utils.background_jobs import enqueue
+from twofactor import (should_run_2fa, authenticate_for_2factor,
+	confirm_otp_token, get_cached_user_pass)
 
 from six.moves.urllib.parse import quote
 
+import pyotp, base64, os
+
 class HTTPRequest:
 	def __init__(self):
 		# Get Environment variables
@@ -62,6 +67,7 @@ class HTTPRequest:
 
 	def validate_csrf_token(self):
 		if frappe.local.request and frappe.local.request.method=="POST":
+			if not frappe.local.session: return
 			if not frappe.local.session.data.csrf_token \
 				or frappe.local.session.data.device=="mobile" \
 				or frappe.conf.get('ignore_csrf', None):
@@ -88,7 +94,7 @@ class HTTPRequest:
 	def connect(self, ac_name = None):
 		"""connect to db, from ac_name or db_name"""
 		frappe.local.db = frappe.database.Database(user = self.get_db_name(), \
-			password = getattr(conf,'db_password', ''))
+			password = getattr(conf, 'db_password', ''))
 
 class LoginManager:
 	def __init__(self):
@@ -98,7 +104,7 @@ class LoginManager:
 		self.user_type = None
 
 		if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login":
-			self.login()
+			if self.login()==False: return
 			self.resume = False
 
 			# run login triggers
@@ -116,7 +122,12 @@ class LoginManager:
 	def login(self):
 		# clear cache
 		frappe.clear_cache(user = frappe.form_dict.get('usr'))
-		self.authenticate()
+		user, pwd = get_cached_user_pass()
+		self.authenticate(user=user, pwd=pwd)
+		if should_run_2fa(self.user):
+			authenticate_for_2factor(self.user)
+			if not confirm_otp_token(self):
+				return False
 		self.post_login()
 
 	def post_login(self):
@@ -183,7 +194,7 @@ class LoginManager:
 		if not (user and pwd):
 			user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd')
 		if not (user and pwd):
-			self.fail('Incomplete login details', user=user)
+			self.fail(_('Incomplete login details'), user=user)
 
 		if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")):
 			user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user
@@ -205,7 +216,9 @@ class LoginManager:
 		except frappe.AuthenticationError:
 			self.fail('Incorrect password', user=user)
 
-	def fail(self, message, user="NA"):
+	def fail(self, message, user=None):
+		if not user:
+			user = _('Unknown User')
 		frappe.local.response['message'] = message
 		add_authentication_log(message, user, status="Failed")
 		frappe.db.commit()
@@ -302,6 +315,7 @@ class CookieManager:
 		for key in set(self.to_delete):
 			response.set_cookie(key, "", expires=expires)
 
+
 @frappe.whitelist()
 def get_logged_user():
 	return frappe.session.user
@@ -317,4 +331,4 @@ def get_website_user_home_page(user):
 		home_page = frappe.get_attr(home_page_method[-1])(user)
 		return '/' + home_page.strip('/')
 	else:
-		return '/me'
+		return '/me'
\ No newline at end of file
diff --git a/frappe/build.js b/frappe/build.js
index 707708236b2f0ab90255217d7f29a3802a022a3c..45ab9bc9cf88adc0202ce769c17e592ef3b28c01 100644
--- a/frappe/build.js
+++ b/frappe/build.js
@@ -10,6 +10,7 @@ const path_join = path.resolve;
 const app = require('express')();
 const http = require('http').Server(app);
 const io = require('socket.io')(http);
+const touch = require("touch");
 
 // basic setup
 const sites_path = path_join(__dirname, '..', '..', '..', 'sites');
@@ -42,6 +43,7 @@ function build(minify) {
 	for (const output_path in build_map) {
 		pack(output_path, build_map[output_path], minify);
 	}
+	touch(path_join(sites_path, '.build'), {force:true});
 }
 
 let socket_connection = false;
@@ -228,7 +230,7 @@ function watch_less(ondirty) {
 	const less_paths = app_paths.map(path => path_join(path, 'public', 'less'));
 
 	const to_watch = filter_valid_paths(less_paths);
-	chokidar.watch(to_watch).on('change', (filename, stats) => {
+	chokidar.watch(to_watch).on('change', (filename) => {
 		console.log(filename, 'dirty');
 		var last_index = filename.lastIndexOf('/');
 		const less_path = filename.slice(0, last_index);
@@ -236,17 +238,18 @@ function watch_less(ondirty) {
 		filename = filename.split('/').pop();
 
 		compile_less_file(filename, less_path, public_path)
-		.then(css_file_path => {
-			// build the target css file for which this css file is input
-			for (const target in build_map) {
-				const sources = build_map[target];
-				if (sources.includes(css_file_path)) {
-					pack(target, sources);
-					ondirty && ondirty(target);
-					break;
+			.then(css_file_path => {
+				// build the target css file for which this css file is input
+				for (const target in build_map) {
+					const sources = build_map[target];
+					if (sources.includes(css_file_path)) {
+						pack(target, sources);
+						ondirty && ondirty(target);
+						break;
+					}
 				}
-			}
-		})
+			});
+		touch(path_join(sites_path, '.build'), {force:true});
 	});
 }
 
@@ -265,6 +268,7 @@ function watch_js(ondirty) {
 				// break;
 			}
 		}
+		touch(path_join(sites_path, '.build'), {force:true});
 	});
 }
 
diff --git a/frappe/change_log/v8/v8_8_0.md b/frappe/change_log/v8/v8_8_0.md
new file mode 100644
index 0000000000000000000000000000000000000000..e93c0adbeb04ea94b1d81ed82b30ed19c92870bd
--- /dev/null
+++ b/frappe/change_log/v8/v8_8_0.md
@@ -0,0 +1,2 @@
+### Two Factor Authentication
+- Now you can authenticate user with two factor authentication. You can enable the Two Factor Authentication from System Settings.
\ No newline at end of file
diff --git a/frappe/client.py b/frappe/client.py
index 7d9eb7bbf5ac0024dce53d5fb8a18ceb4a0ffc1e..fafa535e0e753425b5383a74b48e228de8aaef2a 100644
--- a/frappe/client.py
+++ b/frappe/client.py
@@ -296,3 +296,8 @@ def get_js(items):
 		out.append(code)
 
 	return out
+
+@frappe.whitelist(allow_guest=True)
+def get_time_zone():
+	'''Returns default time zone'''
+	return {"time_zone": frappe.defaults.get_defaults().get("time_zone")}
diff --git a/frappe/contacts/doctype/address/address.js b/frappe/contacts/doctype/address/address.js
index a8d48601178c05b58d41cda4ea599d8cbee52c26..3cbecfe3274f3fe9e615bae2d8e90f3b435f991a 100644
--- a/frappe/contacts/doctype/address/address.js
+++ b/frappe/contacts/doctype/address/address.js
@@ -22,6 +22,7 @@ frappe.ui.form.on("Address", {
 				}
 			}
 		});
+		frm.refresh_field("links");
 	},
 	validate: function(frm) {
 		// clear linked customer / supplier / sales partner on saving...
diff --git a/frappe/core/doctype/doctype/boilerplate/test_controller.js b/frappe/core/doctype/doctype/boilerplate/test_controller.js
index 6749c53bb07cb3340798f075cbcaac3487dab357..ed27ac02f6a509b7a9e0e90c62614c7c6c7c7d6d 100644
--- a/frappe/core/doctype/doctype/boilerplate/test_controller.js
+++ b/frappe/core/doctype/doctype/boilerplate/test_controller.js
@@ -8,9 +8,9 @@ QUnit.test("test: {doctype}", function (assert) {{
 	// number of asserts
 	assert.expect(1);
 
-	frappe.run_serially('{doctype}', [
+	frappe.run_serially([
 		// insert a new {doctype}
-		() => frappe.tests.make([
+		() => frappe.tests.make('{doctype}', [
 			// values to be set
 			{{key: 'value'}}
 		]),
diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json
index 104ee7d53c591e8e724b482eac395287eb831e90..1eebb71a366a87fa5f2e5a2fc2d861265f1f8b87 100644
--- a/frappe/core/doctype/role/role.json
+++ b/frappe/core/doctype/role/role.json
@@ -105,6 +105,37 @@
    "set_only_once": 0, 
    "unique": 0
   }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "default": "0", 
+   "fieldname": "two_factor_auth", 
+   "fieldtype": "Check", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Two Factor Authentication", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "precision": "", 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
   {
    "allow_bulk_edit": 0, 
    "allow_on_submit": 0, 
@@ -148,7 +179,7 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2017-05-04 11:03:41.533058", 
+ "modified": "2017-07-06 12:42:57.097914", 
  "modified_by": "Administrator", 
  "module": "Core", 
  "name": "Role", 
diff --git a/frappe/core/doctype/sms_parameter/README.md b/frappe/core/doctype/sms_parameter/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..5935a390d27c19fc29a7dafa57f7c85c97d105d3
--- /dev/null
+++ b/frappe/core/doctype/sms_parameter/README.md
@@ -0,0 +1 @@
+SMS query parameter for SMS Settings.
\ No newline at end of file
diff --git a/frappe/core/doctype/sms_parameter/__init__.py b/frappe/core/doctype/sms_parameter/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..baffc4882521df036913c9481fc1fe3fea71223b
--- /dev/null
+++ b/frappe/core/doctype/sms_parameter/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.json b/frappe/core/doctype/sms_parameter/sms_parameter.json
new file mode 100755
index 0000000000000000000000000000000000000000..b5648ade80d40343c6b6c5fd139c2a357a5d47e5
--- /dev/null
+++ b/frappe/core/doctype/sms_parameter/sms_parameter.json
@@ -0,0 +1,98 @@
+{
+ "allow_copy": 0, 
+ "allow_guest_to_view": 0, 
+ "allow_import": 0, 
+ "allow_rename": 0, 
+ "beta": 0, 
+ "creation": "2013-02-22 01:27:58", 
+ "custom": 0, 
+ "docstatus": 0, 
+ "doctype": "DocType", 
+ "editable_grid": 1, 
+ "fields": [
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "parameter", 
+   "fieldtype": "Data", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 1, 
+   "in_standard_filter": 0, 
+   "label": "Parameter", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "print_width": "150px", 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 1, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0, 
+   "width": "150px"
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "value", 
+   "fieldtype": "Data", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 1, 
+   "in_standard_filter": 0, 
+   "label": "Value", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "print_width": "150px", 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 1, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0, 
+   "width": "150px"
+  }
+ ], 
+ "has_web_view": 0, 
+ "hide_heading": 0, 
+ "hide_toolbar": 0, 
+ "idx": 1, 
+ "image_view": 0, 
+ "in_create": 0, 
+ "is_submittable": 0, 
+ "issingle": 0, 
+ "istable": 1, 
+ "max_attachments": 0, 
+ "modified": "2017-07-22 22:52:53.309396", 
+ "modified_by": "chude.osiegbu@manqala.com", 
+ "module": "Core", 
+ "name": "SMS Parameter", 
+ "owner": "Administrator", 
+ "permissions": [], 
+ "quick_entry": 0, 
+ "read_only": 0, 
+ "read_only_onload": 0, 
+ "show_name_in_global_search": 0, 
+ "track_changes": 0, 
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.py b/frappe/core/doctype/sms_parameter/sms_parameter.py
new file mode 100644
index 0000000000000000000000000000000000000000..08b220b61aba2aecef7540780794f90569b2b807
--- /dev/null
+++ b/frappe/core/doctype/sms_parameter/sms_parameter.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+from frappe.model.document import Document
+
+class SMSParameter(Document):
+	pass
\ No newline at end of file
diff --git a/frappe/core/doctype/sms_settings/README.md b/frappe/core/doctype/sms_settings/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4fb49803b3c667a06bae782aca9b31b3dcb07dad
--- /dev/null
+++ b/frappe/core/doctype/sms_settings/README.md
@@ -0,0 +1 @@
+Settings for automatically sending SMS from the system.
\ No newline at end of file
diff --git a/frappe/core/doctype/sms_settings/__init__.py b/frappe/core/doctype/sms_settings/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..baffc4882521df036913c9481fc1fe3fea71223b
--- /dev/null
+++ b/frappe/core/doctype/sms_settings/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/frappe/core/doctype/sms_settings/sms_settings.js b/frappe/core/doctype/sms_settings/sms_settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json
new file mode 100755
index 0000000000000000000000000000000000000000..0898ed389e230d7dc5223d22a526f893c2377320
--- /dev/null
+++ b/frappe/core/doctype/sms_settings/sms_settings.json
@@ -0,0 +1,267 @@
+{
+ "allow_copy": 1, 
+ "allow_guest_to_view": 0, 
+ "allow_import": 0, 
+ "allow_rename": 0, 
+ "beta": 0, 
+ "creation": "2013-01-10 16:34:24", 
+ "custom": 0, 
+ "docstatus": 0, 
+ "doctype": "DocType", 
+ "editable_grid": 0, 
+ "fields": [
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "column_break0", 
+   "fieldtype": "Column Break", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0, 
+   "width": "50%"
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "description": "Eg. smsgateway.com/api/send_sms.cgi", 
+   "fieldname": "sms_gateway_url", 
+   "fieldtype": "Data", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 1, 
+   "in_standard_filter": 0, 
+   "label": "SMS Gateway URL", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 1, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "description": "Enter url parameter for message", 
+   "fieldname": "message_parameter", 
+   "fieldtype": "Data", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 1, 
+   "in_standard_filter": 0, 
+   "label": "Message Parameter", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 1, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "description": "Enter url parameter for receiver nos", 
+   "fieldname": "receiver_parameter", 
+   "fieldtype": "Data", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 1, 
+   "in_standard_filter": 0, 
+   "label": "Receiver Parameter", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 1, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "sms_sender_name", 
+   "fieldtype": "Data", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "SMS Sender Name", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "precision": "", 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "static_parameters_section", 
+   "fieldtype": "Column Break", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0, 
+   "width": "50%"
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)", 
+   "fieldname": "parameters", 
+   "fieldtype": "Table", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Static Parameters", 
+   "length": 0, 
+   "no_copy": 0, 
+   "options": "SMS Parameter", 
+   "permlevel": 0, 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }
+ ], 
+ "has_web_view": 0, 
+ "hide_heading": 0, 
+ "hide_toolbar": 0, 
+ "icon": "fa fa-cog", 
+ "idx": 1, 
+ "image_view": 0, 
+ "in_create": 0, 
+ "is_submittable": 0, 
+ "issingle": 1, 
+ "istable": 0, 
+ "max_attachments": 0, 
+ "modified": "2017-07-22 22:52:16.066981", 
+ "modified_by": "chude.osiegbu@manqala.com", 
+ "module": "Core", 
+ "name": "SMS Settings", 
+ "owner": "Administrator", 
+ "permissions": [
+  {
+   "amend": 0, 
+   "apply_user_permissions": 0, 
+   "cancel": 0, 
+   "create": 1, 
+   "delete": 0, 
+   "email": 0, 
+   "export": 0, 
+   "if_owner": 0, 
+   "import": 0, 
+   "permlevel": 0, 
+   "print": 0, 
+   "read": 1, 
+   "report": 0, 
+   "role": "System Manager", 
+   "set_user_permissions": 0, 
+   "share": 1, 
+   "submit": 0, 
+   "write": 1
+  }
+ ], 
+ "quick_entry": 0, 
+ "read_only": 0, 
+ "read_only_onload": 0, 
+ "show_name_in_global_search": 0, 
+ "track_changes": 0, 
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..a8b59beffa153d33572c02ac80ed57bd62f268e1
--- /dev/null
+++ b/frappe/core/doctype/sms_settings/sms_settings.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+from frappe import _, throw, msgprint
+from frappe.utils import nowdate
+
+from frappe.model.document import Document
+
+class SMSSettings(Document):
+	pass
+
+def validate_receiver_nos(receiver_list):
+	validated_receiver_list = []
+	for d in receiver_list:
+		# remove invalid character
+		for x in [' ', '+', '-', '(', ')']:
+			d = d.replace(x, '')
+
+		validated_receiver_list.append(d)
+
+	if not validated_receiver_list:
+		throw(_("Please enter valid mobile nos"))
+
+	return validated_receiver_list
+
+
+def get_sender_name():
+	"returns name as SMS sender"
+	sender_name = frappe.db.get_single_value('SMS Settings', 'sms_sender_name') or \
+		'ERPNXT'
+	if len(sender_name) > 6 and \
+			frappe.db.get_default("country") == "India":
+		throw("""As per TRAI rule, sender name must be exactly 6 characters.
+			Kindly change sender name in Setup --> Global Defaults.
+			Note: Hyphen, space, numeric digit, special characters are not allowed.""")
+	return sender_name
+
+@frappe.whitelist()
+def get_contact_number(contact_name, ref_doctype, ref_name):
+	"returns mobile number of the contact"
+	number = frappe.db.sql("""select mobile_no, phone from tabContact 
+		where name=%s 
+			and exists(
+				select name from `tabDynamic Link` where link_doctype=%s and link_name=%s
+			)
+	""", (contact_name, ref_doctype, ref_name))
+	
+	return number and (number[0][0] or number[0][1]) or ''
+
+@frappe.whitelist()
+def send_sms(receiver_list, msg, sender_name = '', success_msg = True):
+
+	import json
+	if isinstance(receiver_list, basestring):
+		receiver_list = json.loads(receiver_list)
+		if not isinstance(receiver_list, list):
+			receiver_list = [receiver_list]
+
+	receiver_list = validate_receiver_nos(receiver_list)
+
+	arg = {
+		'receiver_list' : receiver_list,
+		'message'		: unicode(msg).encode('utf-8'),
+		'sender_name'	: sender_name or get_sender_name(),
+		'success_msg'	: success_msg
+	}
+
+	if frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'):
+		send_via_gateway(arg)
+	else:
+		msgprint(_("Please Update SMS Settings"))
+
+def send_via_gateway(arg):
+	ss = frappe.get_doc('SMS Settings', 'SMS Settings')
+	args = {ss.message_parameter: arg.get('message')}
+	for d in ss.get("parameters"):
+		args[d.parameter] = d.value
+
+	success_list = []
+	for d in arg.get('receiver_list'):
+		args[ss.receiver_parameter] = d
+		status = send_request(ss.sms_gateway_url, args)
+
+		if 200 <= status < 300:
+			success_list.append(d)
+
+	if len(success_list) > 0:
+		args.update(arg)
+		create_sms_log(args, success_list)
+		if arg.get('success_msg'):
+			frappe.msgprint(_("SMS sent to following numbers: {0}").format("\n" + "\n".join(success_list)))
+
+
+def send_request(gateway_url, params):
+	import requests
+	response = requests.get(gateway_url, params = params, headers={'Accept': "text/plain, text/html, */*"})
+	response.raise_for_status()
+	return response.status_code
+
+
+# Create SMS Log
+# =========================================================
+def create_sms_log(args, sent_to):
+	sl = frappe.new_doc('SMS Log')
+	sl.sender_name = args['sender_name']
+	sl.sent_on = nowdate()
+	sl.message = args['message'].decode('utf-8')
+	sl.no_of_requested_sms = len(args['receiver_list'])
+	sl.requested_numbers = "\n".join(args['receiver_list'])
+	sl.no_of_sent_sms = len(sent_to)
+	sl.sent_to = "\n".join(sent_to)
+	sl.flags.ignore_permissions = True
+	sl.save()
diff --git a/frappe/core/doctype/sms_settings/test_sms_settings.js b/frappe/core/doctype/sms_settings/test_sms_settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..c090d167f58ecca30d5911c9ae87bcfe6d7fab2f
--- /dev/null
+++ b/frappe/core/doctype/sms_settings/test_sms_settings.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: SMS Settings", function (assert) {
+	let done = assert.async();
+
+	// number of asserts
+	assert.expect(1);
+
+	frappe.run_serially('SMS Settings', [
+		// insert a new SMS Settings
+		() => frappe.tests.make([
+			// values to be set
+			{key: 'value'}
+		]),
+		() => {
+			assert.equal(cur_frm.doc.key, 'value');
+		},
+		() => done()
+	]);
+
+});
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index bbdf75c08549be7c480f744dc6f56d9144c1e0bb..6405a275bfc01c0365f92c336d8805326c5393ba 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -895,6 +895,165 @@
    "set_only_once": 0, 
    "unique": 0
   }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 1, 
+   "columns": 0, 
+   "fieldname": "two_factor_authentication", 
+   "fieldtype": "Section Break", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Two Factor Authentication", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "precision": "", 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "enable_two_factor_auth", 
+   "fieldtype": "Check", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Enable Two Factor Auth", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "precision": "", 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "default": "OTP App", 
+   "depends_on": "", 
+   "description": "Choose authentication method to be used by all users", 
+   "fieldname": "two_factor_method", 
+   "fieldtype": "Select", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Two Factor Authentication method", 
+   "length": 0, 
+   "no_copy": 0, 
+   "options": "OTP App\nSMS\nEmail", 
+   "permlevel": 0, 
+   "precision": "", 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "depends_on": "eval:doc.two_factor_method == \"OTP App\"", 
+   "description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>", 
+   "fieldname": "lifespan_qrcode_image", 
+   "fieldtype": "Int", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Expiry time of QR Code Image Page", 
+   "length": 0, 
+   "no_copy": 0, 
+   "permlevel": 0, 
+   "precision": "", 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "default": "Frappe Framework", 
+   "depends_on": "enable_two_factor_auth", 
+   "fieldname": "otp_issuer_name", 
+   "fieldtype": "Data", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "OTP Issuer Name", 
+   "length": 0, 
+   "no_copy": 0, 
+   "options": "", 
+   "permlevel": 0, 
+   "precision": "", 
+   "print_hide": 0, 
+   "print_hide_if_no_value": 0, 
+   "read_only": 0, 
+   "remember_last_selected_value": 0, 
+   "report_hide": 0, 
+   "reqd": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
   {
    "allow_bulk_edit": 0, 
    "allow_on_submit": 0, 
@@ -1027,7 +1186,7 @@
  "issingle": 1, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2017-07-20 22:57:56.466867", 
+ "modified": "2017-08-07 23:29:18.858797", 
  "modified_by": "Administrator", 
  "module": "Core", 
  "name": "System Settings", 
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index f7ecfc00bb7fe69f12f38d9c5b3ce92028897eb9..cd7edc6a53c486208cb477f35c4ab297f0e70b66 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -9,6 +9,7 @@ from frappe.model import no_value_fields
 from frappe.translate import set_default_language
 from frappe.utils import cint
 from frappe.utils.momentjs import get_all_timezones
+from frappe.twofactor import toggle_two_factor_auth
 
 class SystemSettings(Document):
 	def validate(self):
@@ -25,6 +26,12 @@ class SystemSettings(Document):
 				if len(parts)!=2 or not (cint(parts[0]) or cint(parts[1])):
 					frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm"))
 
+		if self.enable_two_factor_auth:
+			if self.two_factor_method=='SMS':
+				if not frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'):
+					frappe.throw(_('Please setup SMS before setting it as an authentication method, via SMS Settings'))
+			toggle_two_factor_auth(True, roles=['All'])
+
 	def on_update(self):
 		for df in self.meta.get("fields"):
 			if df.fieldtype not in no_value_fields:
diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js
index 87ea09fab7529ba179b85196e998f93a4019c2fc..da28ab5a2b9c916ab5eac870bf316e5dc810d712 100644
--- a/frappe/core/doctype/test_runner/test_runner.js
+++ b/frappe/core/doctype/test_runner/test_runner.js
@@ -23,6 +23,7 @@ frappe.ui.form.on('Test Runner', {
 
 	},
 	run_tests: function(frm, files) {
+		frappe.flags.in_test = true;
 		let require_list = [
 			"assets/frappe/js/lib/jquery/qunit.js",
 			"assets/frappe/js/lib/jquery/qunit.css"
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 49c1f8b43727b0dcc32849052da7e430535a278a..5409b569c739ead77a33b94018264f155cfa0274 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -78,6 +78,15 @@ frappe.ui.form.on('User', {
 				})
 			})
 
+			frm.add_custom_button(__("Reset OTP Secret"), function() {
+				frappe.call({
+					method: "frappe.core.doctype.user.user.reset_otp_secret",
+					args: {
+						"user": frm.doc.name
+					}
+				})
+			})
+
 			frm.trigger('enabled');
 
 			frm.roles_editor && frm.roles_editor.show();
@@ -111,6 +120,7 @@ frappe.ui.form.on('User', {
 			}
 			cur_frm.dirty();
 		}
+
 	},
 	validate: function(frm) {
 		if(frm.roles_editor) {
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 0796ff76fbc45fcb918eb693c6306894e1c17010..31714b7116057725126341fd2c957030b6d57ccd 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -1971,7 +1971,7 @@
  "istable": 0, 
  "max_attachments": 5, 
  "menu_index": 0, 
- "modified": "2017-07-12 19:24:00.824902", 
+ "modified": "2017-07-07 17:18:14.047969", 
  "modified_by": "Administrator", 
  "module": "Core", 
  "name": "User", 
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index c5dfbd0e2a0469d2a31255746022441e8adf471e..e7d24baf2e957be8bcb8bf9e19d352f51e9d1ceb 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -14,6 +14,7 @@ import frappe.share
 import re
 from frappe.limits import get_limits
 from frappe.website.utils import is_signup_enabled
+from frappe.utils.background_jobs import enqueue
 
 STANDARD_USERS = ("Guest", "Administrator")
 
@@ -586,8 +587,8 @@ def get_email_awaiting(user):
 		return waiting
 	else:
 		frappe.db.sql("""update `tabUser Email`
-	    		set awaiting_password =0
-	    		where parent = %(user)s""",{"user":user})
+				set awaiting_password =0
+				where parent = %(user)s""",{"user":user})
 		return False
 
 @frappe.whitelist(allow_guest=False)
@@ -675,7 +676,7 @@ def ask_pass_update():
 	from frappe.utils import set_default
 
 	users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email`
-        WHERE awaiting_password = 1""", as_dict=True)
+		WHERE awaiting_password = 1""", as_dict=True)
 
 	password_list = [ user.get("user") for user in users ]
 	set_default("email_user_password", u','.join(password_list))
@@ -888,4 +889,84 @@ def handle_password_test_fail(result):
 def update_gravatar(name):
 	gravatar = has_gravatar(name)
 	if gravatar:
-		frappe.db.set_value('User', name, 'user_image', gravatar)
\ No newline at end of file
+		frappe.db.set_value('User', name, 'user_image', gravatar)
+
+@frappe.whitelist(allow_guest=True)
+def send_token_via_sms(tmp_id,phone_no=None,user=None):
+	try:
+		from frappe.core.doctype.sms_settings.sms_settings import send_request
+	except:
+		return False
+
+	if not frappe.cache().ttl(tmp_id + '_token'):
+		return False
+	ss = frappe.get_doc('SMS Settings', 'SMS Settings')
+	if not ss.sms_gateway_url:
+		return False
+
+	token = frappe.cache().get(tmp_id + '_token')
+	args = {ss.message_parameter: 'verification code is {}'.format(token)}
+
+	for d in ss.get("parameters"):
+		args[d.parameter] = d.value
+
+	if user:
+		user_phone = frappe.db.get_value('User', user, ['phone','mobile_no'], as_dict=1)
+		usr_phone = user_phone.mobile_no or user_phone.phone
+		if not usr_phone:
+			return False
+	else:
+		if phone_no:
+			usr_phone = phone_no
+		else:
+			return False
+
+	args[ss.receiver_parameter] = usr_phone
+	status = send_request(ss.sms_gateway_url, args)
+
+	if 200 <= status < 300:
+		frappe.cache().delete(tmp_id + '_token')
+		return True
+	else:
+		return False
+
+@frappe.whitelist(allow_guest=True)
+def send_token_via_email(tmp_id,token=None):
+	import pyotp
+
+	user = frappe.cache().get(tmp_id + '_user')
+	count = token or frappe.cache().get(tmp_id + '_token')
+
+	if ((not user) or (user == 'None') or (not count)):
+		return False
+	user_email = frappe.db.get_value('User',user, 'email')
+	if not user_email:
+		return False
+
+	otpsecret = frappe.cache().get(tmp_id + '_otp_secret')
+	hotp = pyotp.HOTP(otpsecret)
+
+	frappe.sendmail(
+		recipients=user_email, sender=None, subject='Verification Code',
+		message='<p>Your verification code is {0}</p>'.format(hotp.at(int(count))),
+		delayed=False, retry=3)
+
+	return True
+	
+@frappe.whitelist(allow_guest=True)
+def reset_otp_secret(user):
+	otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
+	user_email = frappe.db.get_value('User',user, 'email')
+	if frappe.session.user in ["Administrator", user] :
+		frappe.defaults.clear_default(user + '_otplogin')
+		frappe.defaults.clear_default(user + '_otpsecret')
+		email_args = {
+			'recipients':user_email, 'sender':None, 'subject':'OTP Secret Reset - {}'.format(otp_issuer or "Frappe Framework"),
+			'message':'<p>Your OTP secret on {} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.</p>'.format(otp_issuer or "Frappe Framework"),
+			'delayed':False,
+			'retry':3 
+		}
+		enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args)
+		return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login."))
+	else:
+		return frappe.throw(_("OTP secret can only be reset by the Administrator."))
\ No newline at end of file
diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py
index 82f13242aecde8104a9628141a196f9a03513264..be721b327181d2a1121a7869fe2874d6488c70d1 100644
--- a/frappe/core/doctype/version/test_version.py
+++ b/frappe/core/doctype/version/test_version.py
@@ -14,12 +14,13 @@ class TestVersion(unittest.TestCase):
 		new_doc = copy.deepcopy(old_doc)
 
 		old_doc.color = None
+		new_doc.color = '#fafafa'
 
 		diff = get_diff(old_doc, new_doc)['changed']
 
 		self.assertEquals(get_fieldnames(diff)[0], 'color')
 		self.assertTrue(get_old_values(diff)[0] is None)
-		self.assertEquals(get_new_values(diff)[0], 'blue')
+		self.assertEquals(get_new_values(diff)[0], '#fafafa')
 
 		new_doc.starts_on = "2017-07-20"
 
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.js b/frappe/custom/doctype/customize_form/test_customize_form.js
index 5d2be73e0b5e5f74a15a20f388ca1a7954f48aa7..d37afa55809152a8d448d2635656ebbe3e594039 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.js
+++ b/frappe/custom/doctype/customize_form/test_customize_form.js
@@ -7,7 +7,7 @@ QUnit.test("test customize form", function(assert) {
 	let done = assert.async();
 	frappe.run_serially([
 		() => frappe.set_route('Form', 'Customize Form'),
-		() => frappe.timeout(2),
+		() => frappe.timeout(1),
 		() => cur_frm.set_value('doc_type', 'ToDo'),
 		() => frappe.timeout(2),
 		() => {
diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json
index 75e949e90f8fc1385dbe3bcf3fc6f163fd39cdf3..12fcf5d0afa64649ecad9b63a5b2f55ebb3ed75a 100644
--- a/frappe/desk/doctype/event/event.json
+++ b/frappe/desk/doctype/event/event.json
@@ -312,9 +312,9 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
-   "default": "blue", 
+   "default": "", 
    "fieldname": "color", 
-   "fieldtype": "Select", 
+   "fieldtype": "Color", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
    "ignore_xss_filter": 0, 
@@ -325,7 +325,7 @@
    "label": "Color", 
    "length": 0, 
    "no_copy": 0, 
-   "options": "red\ngreen\nblue\nyellow\nskyblue\norange", 
+   "options": "", 
    "permlevel": 0, 
    "precision": "", 
    "print_hide": 0, 
@@ -895,8 +895,8 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2017-07-06 12:37:44.036819", 
- "modified_by": "Administrator", 
+ "modified": "2017-08-03 16:34:54.657796", 
+ "modified_by": "faris@erpnext.com", 
  "module": "Desk", 
  "name": "Event", 
  "owner": "Administrator", 
diff --git a/frappe/desk/doctype/event/test_event.js b/frappe/desk/doctype/event/test_event.js
new file mode 100644
index 0000000000000000000000000000000000000000..50dcd9e9aa41b60d245a4e9866d9ea237c3a52ae
--- /dev/null
+++ b/frappe/desk/doctype/event/test_event.js
@@ -0,0 +1,42 @@
+
+QUnit.test("test: Event", function (assert) {
+	let done = assert.async();
+
+	// number of asserts
+	assert.expect(4);
+
+	const subject = '_Test Event 1';
+	const datetime = frappe.datetime.now_datetime();
+	const hex = '#6be273';
+	const rgb = 'rgb(107, 226, 115)';
+
+	frappe.run_serially([
+		// insert a new Event
+		() => frappe.tests.make('Event', [
+			// values to be set
+			{subject: subject},
+			{starts_on: datetime},
+			{color: hex},
+			{event_type: 'Private'}
+		]),
+		() => {
+			assert.equal(cur_frm.doc.subject, subject, 'Subject correctly set');
+			assert.equal(cur_frm.doc.starts_on, datetime, 'Date correctly set');
+			assert.equal(cur_frm.doc.color, hex, 'Color correctly set');
+
+			// set filters explicitly for list view
+			frappe.route_options = {
+				event_type: 'Private'
+			};
+		},
+		() => frappe.set_route('List', 'Event', 'Calendar'),
+		() => frappe.timeout(2),
+		() => {
+			const bg_color = $(`.result-list:visible .fc-day-grid-event:contains("${subject}")`)
+				.css('background-color');
+			assert.equal(bg_color, rgb, 'Event background color is set correctly');
+		},
+		() => done()
+	]);
+
+});
diff --git a/frappe/desk/doctype/note/note.js b/frappe/desk/doctype/note/note.js
index d905b7ad27fca5e312cd0eb24e66a76222761295..c237998ccfd40a86bd1ea18b10308c3457f5856d 100644
--- a/frappe/desk/doctype/note/note.js
+++ b/frappe/desk/doctype/note/note.js
@@ -10,7 +10,7 @@ frappe.ui.form.on("Note", {
 			// toggle edit
 			frm.add_custom_button("Edit", function() {
 				frm.events.set_editable(frm, !frm.is_note_editable);
-			})
+			});
 			frm.events.set_editable(frm, false);
 		}
 	},
@@ -24,12 +24,12 @@ frappe.ui.form.on("Note", {
 		frm.set_df_property("content", "read_only", editable ? 0: 1);
 
 		// hide all other fields
-		$.each(frm.fields_dict, function(fieldname, field) {
+		$.each(frm.fields_dict, function(fieldname) {
 
 			if(fieldname !== "content") {
 				frm.set_df_property(fieldname, "hidden", editable ? 0: 1);
 			}
-		})
+		});
 
 		// no label, description for content either
 		frm.get_field("content").toggle_label(editable);
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index f76787e0e38d13decf5527395116d6f1179bd8e7..5ea6412977f4a2b739b4b9325d4ccc5e3de1d9da 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -561,7 +561,7 @@ var frappe_slides = [
 				}
 			}
 		},
-	},
+	}
 ];
 
 var utils = {
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index 8e8fef3359d6cae8bfc3da5ae86583ec5a975115..ad3108b67aef8f4901d2278ccb89bb9919a61f18 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -267,3 +267,10 @@ def email_setup_wizard_exception(traceback, args):
 
 def get_language_code(lang):
 	return frappe.db.get_value('Language', {'language_name':lang})
+
+
+def enable_twofactor_all_roles():
+	all_role = frappe.get_doc('Role',{'role_name':'All'})
+	all_role.two_factor_auth = True
+	all_role.save(ignore_permissions=True)
+
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 8140a0b11edbaa06e9b7348e8245d8cf382a492b..073576c43724a1f1a5adfd642088fe1a3d518cde 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -77,7 +77,7 @@ def run(report_name, filters=None, user=None):
 		frappe.msgprint(_("Must have report permission to access this report."),
 			raise_exception=True)
 
-	columns, result, message, chart = [], [], None, None
+	columns, result, message, chart, data_to_be_printed = [], [], None, None, None
 	if report.report_type=="Query Report":
 		if not report.query:
 			frappe.msgprint(_("Must specify a Query to run"), raise_exception=True)
@@ -99,6 +99,8 @@ def run(report_name, filters=None, user=None):
 				message = res[2]
 			if len(res) > 3:
 				chart = res[3]
+			if len(res) > 4:
+				data_to_be_printed = res[4]
 
 	if report.apply_user_permissions and result:
 		result = get_filtered_data(report.ref_doctype, columns, result, user)
@@ -110,7 +112,8 @@ def run(report_name, filters=None, user=None):
 		"result": result,
 		"columns": columns,
 		"message": message,
-		"chart": chart
+		"chart": chart,
+		"data_to_be_printed": data_to_be_printed
 	}
 
 
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 4f4713edbe90ee68295987fca9be72fb2ce6247a..04790de8b684b37d8d49c872b2a6d135d05a394f 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -14,6 +14,7 @@ from frappe.utils.scheduler import log
 from frappe.email.queue import send
 from frappe.email.doctype.email_group.email_group import add_subscribers
 from frappe.utils import parse_addr
+from frappe.utils import validate_email_add
 
 
 class Newsletter(Document):
@@ -23,6 +24,10 @@ class Newsletter(Document):
 				from `tabEmail Queue` where reference_doctype=%s and reference_name=%s
 				group by status""", (self.doctype, self.name))) or None
 
+	def validate(self):
+		if self.send_from:
+			validate_email_add(self.send_from, True)
+
 	def test_send(self, doctype="Lead"):
 		self.recipients = frappe.utils.split_emails(self.test_email_id)
 		self.queue_all()
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 723c6024969fdf1631c1f1b9b99a06b1e372cd43..ae9fca7e7ae7283096d6c850817576a26703d523 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -37,6 +37,9 @@ class SessionStopped(Exception):
 class UnsupportedMediaType(Exception):
 	http_status_code = 415
 
+class RequestToken(Exception):
+	http_status_code = 200
+
 class Redirect(Exception):
 	http_status_code = 301
 
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 2d28b74d91be2939afd73b5c1b799a1e8ea38782..bf990a9f72c02f5457cfbe5c74ca173c8ba934c9 100755
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -128,7 +128,8 @@ scheduler_events = {
 		"frappe.email.doctype.email_account.email_account.pull",
 		"frappe.email.doctype.email_account.email_account.notify_unreplied",
 		"frappe.oauth.delete_oauth2_data",
-		"frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment"
+		"frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment",
+		"frappe.twofactor.delete_all_barcodes_for_users"
 	],
 	"hourly": [
 		"frappe.model.utils.link_count.update_link_count",
@@ -189,3 +190,5 @@ bot_parsers = [
 
 setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception"
 before_write_file = "frappe.limits.validate_space_limit"
+
+otp_methods = ['OTP App','Email','SMS']
diff --git a/frappe/public/build.json b/frappe/public/build.json
index 054421286e19f85e645912823d337d2f41e89014..75dbf5063a73459ba9cde91a088c9b640be3fbcf 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -87,6 +87,7 @@
 		"public/js/frappe/ui/messages.js",
 		"public/js/frappe/ui/keyboard.js",
 		"public/js/frappe/ui/emoji.js",
+		"public/js/frappe/ui/colors.js",
 
 		"public/js/frappe/request.js",
 		"public/js/frappe/socketio_client.js",
diff --git a/frappe/public/css/calendar.css b/frappe/public/css/calendar.css
index a46ab7227f75797911f7dce6621f728ef7b4db5c..df530f7c304ea66d1064baca8f3264a8c6ab3c66 100644
--- a/frappe/public/css/calendar.css
+++ b/frappe/public/css/calendar.css
@@ -73,7 +73,6 @@ th.fc-day-header {
   background: #cfdce5 !important;
 }
 .fc-day-grid-event {
-  background-color: rgba(94, 100, 255, 0.2) !important;
   border: none !important;
   margin: 5px 4px 0 !important;
   padding: 1px 5px !important;
diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css
index 4e9dfbaa6e3e3691f0ed23286ad4bafe79a864e2..ccedde274dcaade461141abeb1bc1706c6dd2f46 100644
--- a/frappe/public/css/email.css
+++ b/frappe/public/css/email.css
@@ -49,7 +49,7 @@ hr {
   border-top: none;
 }
 .email-footer-container {
-  margin-top: 10px;
+  margin-top: 30px;
 }
 .email-footer-container > div:not(:last-child) {
   margin-bottom: 5px;
diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css
index c56811e892805763caa2fd96daff857f88b9e0f1..2dd5aaa1e20672962c7533b674d8fb023bd619c4 100644
--- a/frappe/public/css/form.css
+++ b/frappe/public/css/form.css
@@ -299,17 +299,32 @@ h6.uppercase,
 .timeline-item.user-content .action-btns {
   position: absolute;
   right: 0;
-  padding: 5px 15px 2px 5px;
+  padding: 8px 15px 0 5px;
+}
+.timeline-item.user-content .action-btns .edit-btn-container {
+  margin-right: 13px;
 }
 .timeline-item.user-content .comment-header {
   background-color: #fafbfc;
-  padding: 10px 15px 10px 13px;
+  padding: 10px 15px 8px 13px;
   margin: 0px;
   color: #8D99A6;
   border-bottom: 1px solid #EBEFF2;
 }
 .timeline-item.user-content .comment-header.links-active {
-  padding-right: 60px;
+  padding-right: 77px;
+}
+.timeline-item.user-content .comment-header .asset-details {
+  display: inline-block;
+  width: 100%;
+}
+.timeline-item.user-content .comment-header .asset-details .btn-link {
+  border: 0;
+  border-radius: 0;
+  padding: 0;
+}
+.timeline-item.user-content .comment-header .asset-details .btn-link:hover {
+  text-decoration: none;
 }
 .timeline-item.user-content .comment-header .commented-on-small {
   display: none;
@@ -334,7 +349,8 @@ h6.uppercase,
 .timeline-item.user-content .close-btn-container .close {
   color: inherit;
   opacity: 1;
-  padding: 0 0 0 10px;
+  padding: 0;
+  font-size: 18px;
 }
 .timeline-item.user-content .edit-btn-container {
   padding: 0;
@@ -409,7 +425,8 @@ h6.uppercase,
   top: 5px;
 }
 .timeline-item .reply-link {
-  padding-left: 7px;
+  margin-left: 15px;
+  font-size: 12px;
 }
 .timeline-head {
   background-color: white;
diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css
index 05cb1d07b620dc14eb12c6b5906e74ef28f43525..49ecce16dec0da3fee76f84d60f9a2756f4d02b9 100644
--- a/frappe/public/css/list.css
+++ b/frappe/public/css/list.css
@@ -183,6 +183,25 @@
 .listview-main-section .octicon-heart {
   cursor: pointer;
 }
+.listview-main-section .page-form {
+  padding-left: 17px;
+}
+@media (max-width: 991px) {
+  .listview-main-section .page-form {
+    padding-left: 25px;
+  }
+}
+.listview-main-section .page-form .octicon-search {
+  float: left;
+  padding-top: 7px;
+  margin-left: -4px;
+  margin-right: -4px;
+}
+@media (max-width: 991px) {
+  .listview-main-section .page-form .octicon-search {
+    margin-left: -12px;
+  }
+}
 .like-action.octicon-heart {
   color: #ff5858;
 }
diff --git a/frappe/public/css/mobile.css b/frappe/public/css/mobile.css
index ebcc52084f0ea8b8c3ede41898153efcdec6e692..cc5b926f13bb0c82d97ff88f6f62bc3aef4de35e 100644
--- a/frappe/public/css/mobile.css
+++ b/frappe/public/css/mobile.css
@@ -25,6 +25,9 @@ body {
   body[data-route^="Form"] .page-title h1 {
     margin-top: 12px;
   }
+  body[data-route^="Form"] .page-title h1.editable-title {
+    padding-right: 80px;
+  }
   body[data-route^="Form"] .page-title .indicator {
     display: inline-block;
     margin-top: 12px;
@@ -197,7 +200,7 @@ body {
   }
   body[data-route^="Form"] .page-title .title-text {
     font-size: 16px;
-    width: calc(100% - 30px);
+    width: calc(100% - 90px);
   }
   body[data-route^="Form"] .page-title .indicator {
     float: left;
@@ -356,7 +359,10 @@ body {
     content: none;
   }
   .timeline .timeline-item.user-content .action-btns {
-    padding: 5px 10px 2px 5px;
+    padding: 7px 10px 2px 5px;
+  }
+  .timeline .timeline-item.user-content .action-btns .edit-btn-container {
+    margin-right: 0;
   }
   .timeline .timeline-item.user-content .comment-header {
     padding: 7px 10px;
@@ -364,6 +370,12 @@ body {
   .timeline .timeline-item.user-content .comment-header .links-active {
     padding-right: 10px;
   }
+  .timeline .timeline-item.user-content .comment-header .reply-link {
+    margin-left: 0;
+  }
+  .timeline .timeline-item.user-content .comment-header .asset-details {
+    width: calc(100% - 30px);
+  }
   .timeline .timeline-item.user-content .avatar-medium {
     margin-right: 10px;
   }
diff --git a/frappe/public/css/page.css b/frappe/public/css/page.css
index f5ccdc5a6ab0a88b65e73d5e71a12e347f3e9244..66a7bbd836cff17c7a64e8f650029a984fd6acf7 100644
--- a/frappe/public/css/page.css
+++ b/frappe/public/css/page.css
@@ -44,7 +44,6 @@
   vertical-align: middle;
 }
 .page-title .title-image {
-  display: inline-block;
   width: 46px;
   height: 0;
   padding: 23px 0;
@@ -56,6 +55,7 @@
   text-align: center;
   line-height: 0;
   float: left;
+  margin-right: 10px;
 }
 .editable-title .title-text {
   cursor: pointer;
diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css
index b9b2d733bb0d6d6031c6b89c321f06abf33cbbee..6e33918c6c7526c676c4d83b3f1cb32c82f6eee5 100644
--- a/frappe/public/css/website.css
+++ b/frappe/public/css/website.css
@@ -507,6 +507,7 @@ li {
   border-top: 1px solid #EBEFF2;
 }
 .page_content {
+  padding-top: 30px;
   padding-bottom: 30px;
 }
 .carousel-control .icon {
@@ -554,6 +555,9 @@ li {
 .panel-body {
   padding-left: 15px;
 }
+.page-head {
+  margin-bottom: -30px;
+}
 .page-head h1,
 .page-head h2 {
   margin-top: 0px;
@@ -588,9 +592,14 @@ fieldset {
   width: 100%;
 }
 .page-container {
-  padding: 0px;
+  display: flex;
   max-width: 970px;
-  margin: auto;
+  margin: 0 auto;
+}
+@media (max-width: 767px) {
+  .page-container {
+    flex-direction: column-reverse;
+  }
 }
 .page-max-width {
   max-width: 800px;
@@ -603,30 +612,28 @@ fieldset {
 .web-sidebar {
   position: relative;
 }
-.web-sidebar .sidebar-item {
+.web-sidebar .sidebar-item:not(:last-child) {
   margin: 0px;
   padding-bottom: 12px;
   border: none;
   color: #8D99A6;
-  font-size: 12px;
 }
-.web-sidebar .sidebar-item .badge {
+.web-sidebar .sidebar-item:not(:last-child) .badge {
   font-weight: normal;
 }
 .web-sidebar .sidebar-item a {
-  color: #36414C !important;
+  color: #8D99A6;
 }
 .web-sidebar .sidebar-item a.active {
-  color: #36414C !important;
-  font-weight: 500 !important;
-}
-.web-sidebar .sidebar-items {
-  margin-bottom: 30px;
+  color: #36414C;
 }
 .web-sidebar .sidebar-items .title {
   font-size: 14px;
   font-weight: bold;
 }
+.web-sidebar .sidebar-items ul {
+  margin-bottom: 0;
+}
 .page-footer {
   padding: 15px 0px;
   border-top: 1px solid #EBEFF2;
@@ -712,11 +719,6 @@ textarea {
 .sidebar-navbar-items a:visited {
   border-bottom: 0px;
 }
-@media (max-width: 767px) {
-  .visible-xs {
-    display: inline-block !important;
-  }
-}
 .more-block {
   padding-bottom: 30px;
 }
@@ -790,16 +792,49 @@ a.active {
 .btn-next-wrapper {
   margin-top: 60px;
 }
-.sidebar-block,
+.sidebar-block {
+  flex: 1;
+  font-size: 12px;
+  border-right: 1px solid #d1d8dd;
+  padding: 30px;
+  padding-left: 0px;
+}
+@media (max-width: 767px) {
+  .sidebar-block {
+    font-size: 14px;
+    border-right: none;
+    border-top: 1px solid #d1d8dd;
+    padding-left: 20px;
+  }
+}
 .page-content {
+  flex: 6;
+}
+.page-content h1:first-child {
+  margin-top: 0;
+}
+.page-content.with-sidebar {
+  padding: 30px;
+  padding-left: 40px;
+}
+.page-content.without-sidebar {
   padding-top: 30px;
-  padding-bottom: 50px;
 }
 .your-account-info {
   margin-top: 30px;
 }
-.page-content.with-sidebar {
-  padding-left: 50px;
+@media (max-width: 767px) {
+  .visible-xs {
+    display: inline-block !important;
+  }
+  .sidebar-block {
+    width: 100%;
+  }
+  .page-content.with-sidebar {
+    width: 100%;
+    padding-left: 20px;
+    padding-right: 20px;
+  }
 }
 @media screen and (max-width: 480px) {
   .page-content {
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index e76ef9105e2a358cf3755f75a4658995bf68f58a..7c15d260bdcf08d9f5f50cce66193010aeef400f 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -45,6 +45,7 @@ frappe.Application = Class.extend({
 		this.make_nav_bar();
 		this.set_favicon();
 		this.setup_analytics();
+		this.setup_beforeunload();
 		frappe.ui.keys.setup();
 		this.set_rtl();
 
@@ -480,6 +481,23 @@ frappe.Application = Class.extend({
 		}
 	},
 
+	setup_beforeunload: function() {
+		if (frappe.defaults.get_default('in_selenium')) {
+			return;
+		}
+		window.onbeforeunload = function () {
+			if (frappe.flags.in_test) return null;
+			var unsaved_docs = [];
+			for (doctype in locals) {
+				for (name in locals[doctype]) {
+					var doc = locals[doctype][name];
+					if(doc.__unsaved) { unsaved_docs.push(doc.name); }
+				}
+			}
+			return unsaved_docs.length ? true : null;
+		};
+	},
+
 	show_notes: function() {
 		var me = this;
 		if(frappe.boot.notes.length) {
diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js
index 54bb4e0595c680ff1372fd40638509e2f2b4115c..f979bb2cb50c9d0c53e9a6bba9a0ebf06ddb8987 100755
--- a/frappe/public/js/frappe/form/control.js
+++ b/frappe/public/js/frappe/form/control.js
@@ -688,6 +688,8 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
 	},
 	set_formatted_input: function(value) {
 		this._super(value);
+
+		if(!value) value = '#ffffff';
 		this.$input.css({
 			"background-color": value
 		});
@@ -721,6 +723,9 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
 		});
 	},
 	validate: function (value) {
+		if(value === '') {
+			return '';
+		}
 		var is_valid = /^#[0-9A-F]{6}$/i.test(value);
 		if(is_valid) {
 			return value;
diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js
index 7a48fa2c6fa93f895109901e7e648b3d6c6bd2dd..a950aed6a8ce83cf5d99a0d00124b19bc8c57793 100644
--- a/frappe/public/js/frappe/form/footer/timeline.js
+++ b/frappe/public/js/frappe/form/footer/timeline.js
@@ -159,12 +159,12 @@ frappe.ui.form.Timeline = Class.extend({
 		this.prepare_timeline_item(c);
 		var $timeline_item = $(frappe.render_template("timeline_item", {data:c, frm:this.frm}))
 			.appendTo(me.list)
-			.on("click", ".close", function() {
+			.on("click", ".delete-comment", function() {
 				var name = $timeline_item.data('name');
 				me.delete_comment(name);
 				return false;
 			})
-			.on('click', '.edit', function(e) {
+			.on('click', '.edit-comment', function(e) {
 				e.preventDefault();
 				var name = $timeline_item.data('name');
 
@@ -176,6 +176,7 @@ frappe.ui.form.Timeline = Class.extend({
 					var content = $timeline_item.find('.timeline-item-content').html();
 
 					$edit_btn
+						.text("Save")
 						.find('i')
 						.removeClass('octicon-pencil')
 						.addClass('octicon-check');
@@ -232,6 +233,7 @@ frappe.ui.form.Timeline = Class.extend({
 			new frappe.views.CommunicationComposer({
 				doc: me.frm.doc,
 				txt: "",
+				title: __('Reply'),
 				frm: me.frm,
 				last_email: last_email
 			});
@@ -251,11 +253,11 @@ frappe.ui.form.Timeline = Class.extend({
 		c["edit"] = "";
 		if(c.communication_type=="Comment" && (c.comment_type || "Comment") === "Comment") {
 			if(frappe.model.can_delete("Communication")) {
-				c["delete"] = '<a class="close" title="Delete"  href="#"><i class="octicon octicon-x"></i></a>';
+				c["delete"] = '<a class="close delete-comment" title="Delete"  href="#"><i class="octicon octicon-x"></i></a>';
 			}
 
 			if(frappe.user.name == c.sender || (frappe.user.name == 'Administrator')) {
-				c["edit"] = '<a class="edit" title="Edit" href="#"><i class="octicon octicon-pencil"></i></a>';
+				c["edit"] = '<a class="edit-comment text-muted" title="Edit" href="#">Edit</a>';
 			}
 		}
 		c.comment_on_small = comment_when(c.creation, true);
diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html
index 215dd06fc24bbe20a51248dc8e89d9ba8f1efa1d..4baa5504c58a2fea43fe4e3171b337324afdfdb3 100755
--- a/frappe/public/js/frappe/form/footer/timeline_item.html
+++ b/frappe/public/js/frappe/form/footer/timeline_item.html
@@ -91,7 +91,7 @@
 							{% if (data.communication_medium === "Email"
 								&& data.sender !== frappe.session.user_email) { %}
 							<a class="text-muted reply-link pull-right timeline-content-show"
-								data-name="{%= data.name %}" title="{%= __("Reply") %}"><i class="octicon octicon-mail-reply"></i></a>
+								data-name="{%= data.name %}" title="{%= __("Reply") %}">{%= __("Reply") %}</a>
 							{% } %}
 						{% } %}
 					<span class="text-muted commented-on hidden-xs">
diff --git a/frappe/public/js/frappe/form/print.js b/frappe/public/js/frappe/form/print.js
index 0e7e75299a333fc1da5e564aa46d0e4bfa8a2701..cc99eae370b4911329bfb4c891e8aa0cabdc8ebd 100644
--- a/frappe/public/js/frappe/form/print.js
+++ b/frappe/public/js/frappe/form/print.js
@@ -132,11 +132,14 @@ frappe.ui.form.PrintPreview = Class.extend({
 	show_footer: function() {
 		// footer is hidden by default as reqd by pdf generation
 		// simple hack to show it in print preview
-		this.wrapper.find('.print-format').css('position', 'relative');
+		this.wrapper.find('.page-break').css({
+			'display': 'flex',
+			'flex-direction': 'column'
+		});
 		this.wrapper.find('#footer-html').attr('style', `
 			display: block !important;
-			position: absolute;
-			bottom: 0.75in;
+			order: 1;
+			margin-top: 20px;
 		`);
 	},
 	printit: function () {
diff --git a/frappe/public/js/frappe/ui/base_list.js b/frappe/public/js/frappe/ui/base_list.js
index 6426db79884396e500835336018c887b9f4282ad..383efccded9a581fc8f81b786637f2e33fafa20e 100644
--- a/frappe/public/js/frappe/ui/base_list.js
+++ b/frappe/public/js/frappe/ui/base_list.js
@@ -197,14 +197,14 @@ frappe.ui.BaseList = Class.extend({
 				onchange: () => { me.refresh(true); }
 			});
 
-			this.meta.fields.forEach(function(df) {
+			this.meta.fields.forEach(function(df, i) {
 				if(df.in_standard_filter && !frappe.model.no_value_type.includes(df.fieldtype)) {
 					let options = df.options;
 					let condition = '=';
 					let fieldtype = df.fieldtype;
 					if (['Text', 'Small Text', 'Text Editor', 'Data'].includes(fieldtype)) {
-						fieldtype = 'Data',
-						condition = 'like'
+						fieldtype = 'Data';
+						condition = 'like';
 					}
 					if(df.fieldtype == "Select" && df.options) {
 						options = df.options.split("\n");
@@ -213,7 +213,7 @@ frappe.ui.BaseList = Class.extend({
 							options = options.join("\n");
 						}
 					}
-					me.page.add_field({
+					let f = me.page.add_field({
 						fieldtype: fieldtype,
 						label: __(df.label),
 						options: options,
@@ -221,6 +221,13 @@ frappe.ui.BaseList = Class.extend({
 						condition: condition,
 						onchange: () => {me.refresh(true);}
 					});
+					filter_count ++;
+					if (filter_count > 3) {
+						$(f.wrapper).addClass('hidden-sm').addClass('hidden-xs');
+					}
+					if (filter_count > 5) {
+						return false;
+					}
 				}
 			});
 		}
diff --git a/frappe/public/js/frappe/ui/colors.js b/frappe/public/js/frappe/ui/colors.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b3e41ff2769ec4955d6dc899d55d18a02d38d0d
--- /dev/null
+++ b/frappe/public/js/frappe/ui/colors.js
@@ -0,0 +1,121 @@
+// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
+// MIT License. See license.txt
+
+frappe.provide("frappe.ui");
+
+frappe.ui.color_map = {
+	red: ["#ffc4c4", "#ff8989", "#ff4d4d", "#a83333"],
+	brown: ["#ffe8cd", "#ffd19c", "#ffb868", "#a87945"],
+	orange: ["#ffd2c2", "#ffa685", "#ff7846", "#a85b5b"],
+	peach: ["#ffd7d7", "#ffb1b1", "#ff8989", "#a84f2e"],
+	yellow: ["#fffacd", "#fff168", "#fff69c", "#a89f45"],
+	yellowgreen: ["#ebf8cc", "#d9f399", "#c5ec63", "#7b933d"],
+	green: ["#cef6d1", "#9deca2", "#6be273", "#428b46"],
+	cyan: ["#d2f8ed", "#a4f3dd", "#77ecca", "#49937e"],
+	skyblue: ["#d2f1ff", "#a6e4ff", "#78d6ff", "#4f8ea8"],
+	blue: ["#d2d2ff", "#a3a3ff", "#7575ff", "#4d4da8"],
+	purple: ["#dac7ff", "#b592ff", "#8e58ff", "#5e3aa8"],
+	pink: ["#f8d4f8", "#f3aaf0", "#ec7dea", "#934f92"]
+};
+
+frappe.ui.color = {
+	get: function(color_name, shade) {
+		if(color_name && shade) return this.get_color_shade(color_name, shade);
+		if(color_name) return this.get_color_shade(color_name, 'default');
+		return frappe.ui.color_map;
+	},
+	get_color: function(color_name) {
+		const color_names = Object.keys(frappe.ui.color_map);
+		if(color_names.includes(color_name)) {
+			return frappe.ui.color_map[color_name];
+		} else {
+			throw new RangeError(`${color_name} can be one of ${color_names}`);
+		}
+	},
+	get_color_shade: function(color_name, shade) {
+		const shades = {
+			'default': 2,
+			'light': 1,
+			'extra-light': 0,
+			'dark': 3
+		};
+
+		if(Object.keys(shades).includes(shade)) {
+			return frappe.ui.color_map[color_name][shades[shade]];
+		} else {
+			throw new RangeError(`${shade} can be one of ${Object.keys(shades)}`);
+		}
+	},
+	all: function() {
+		return Object.values(frappe.ui.color_map)
+			.reduce((acc, curr) => acc.concat(curr) , []);
+	},
+	names: function() {
+		return Object.keys(frappe.ui.color_map);
+	},
+	validate: function(color_name) {
+		if(!color_name) return false;
+		if(color_name.startsWith('#')) {
+			return this.all().includes(color_name);
+		}
+		return this.names().includes(color_name);
+	},
+	get_color_name: function(hex) {
+		for (const key in frappe.ui.color_map) {
+			const colors = frappe.ui.color_map[key];
+			if (colors.includes(hex)) return key;
+		}
+	},
+	get_contrast_color: function(hex) {
+		if(!this.validate(hex)) {
+			const brightness = this.brightness(hex);
+			if(brightness < 128) {
+				return this.lighten(hex, 0.5);
+			}
+			return this.lighten(hex, -0.5);
+		}
+
+		const color_name = this.get_color_name(hex);
+		const colors = this.get_color(color_name);
+		const shade_value = colors.indexOf(hex);
+		if(shade_value <= 1) {
+			return this.get(color_name, 'dark');
+		}
+		return this.get(color_name, 'extra-light');
+	},
+
+	lighten(color, percent) {
+		// https://stackoverflow.com/a/13542669/5353542
+		var f = parseInt(color.slice(1), 16),
+			t = percent < 0 ? 0 : 255,
+			p = percent < 0 ? percent * -1 : percent,
+			R = f >> 16,
+			G = f >> 8 & 0x00FF,
+			B = f & 0x0000FF;
+		return "#" +
+			(0x1000000 +
+				(Math.round((t - R) * p) + R) *
+				0x10000 +
+				(Math.round((t - G) * p) + G) *
+				0x100 + (Math.round((t - B) * p) + B)
+			).toString(16).slice(1);
+	},
+
+	hex_to_rgb(hex) {
+		if(hex.startsWith('#')) {
+			hex = hex.substring(1);
+		}
+		const r = parseInt(hex.substring(0, 2), 16);
+		const g = parseInt(hex.substring(2, 4), 16);
+		const b = parseInt(hex.substring(4, 6), 16);
+		return {r, g, b};
+	},
+
+	brightness(hex) {
+		const rgb = this.hex_to_rgb(hex);
+		// https://www.w3.org/TR/AERT#color-contrast
+		// 255 - brightest (#fff)
+		// 0 - darkest (#000)
+		return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
+	}
+};
diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js
index aebb74c4ea1fa2d7ed14e360dbfe6e53078d13fb..8fb77c625cbb37c6eb8c6749326f4bd055aca194 100644
--- a/frappe/public/js/frappe/ui/page.js
+++ b/frappe/public/js/frappe/ui/page.js
@@ -401,8 +401,13 @@ frappe.ui.Page = Class.extend({
 			.addClass('col-md-2')
 			.attr("title", __(df.label)).tooltip();
 
+		// html fields in toolbar are only for display
+		if (df.fieldtype=='HTML') {
+			return;
+		}
+
 		// hidden fields dont have $input
-		if(!f.$input) f.make_input();
+		if (!f.$input) f.make_input();
 
 		f.$input.addClass("input-sm").attr("placeholder", __(df.label));
 
diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js
index ce49e5c33d00b07ad497973b6d4db8e4a44531de..4b8b4febf939a35c4b205ba1b8223261b0a70cce 100644
--- a/frappe/public/js/frappe/views/calendar/calendar.js
+++ b/frappe/public/js/frappe/views/calendar/calendar.js
@@ -100,7 +100,8 @@ frappe.views.Calendar = Class.extend({
 	color_map: {
 		"danger": "red",
 		"success": "green",
-		"warning": "orange"
+		"warning": "orange",
+		"default": "blue"
 	},
 	get_system_datetime: function(date) {
 		date._offset = moment.user_utc_offset;
@@ -232,25 +233,28 @@ frappe.views.Calendar = Class.extend({
 			d.end = frappe.datetime.convert_to_user_tz(d.end);
 
 			me.fix_end_date_for_event_render(d);
-
-			let color;
-			if(me.get_css_class) {
-				color = me.color_map[me.get_css_class(d)];
-				// if invalid, fallback to blue color
-				if(!Object.values(me.color_map).includes(color)) {
-					color = "blue";
-				}
-			} else {
-				// color field can be set in {doctype}_calendar.js
-				// see event_calendar.js
-				color = d.color;
-			}
-
-			if(!color) color = "blue";
-			d.className = "fc-bg-" + color;
+			me.prepare_colors(d);
 			return d;
 		});
 	},
+	prepare_colors: function(d) {
+		let color, color_name;
+		if(this.get_css_class) {
+			color_name = this.color_map[this.get_css_class(d)];
+			color_name =
+				frappe.ui.color.validate(color_name) ?
+					color_name :
+					'blue';
+			d.backgroundColor = frappe.ui.color.get(color_name, 'extra-light');
+			d.textColor = frappe.ui.color.get(color_name, 'dark');
+		} else {
+			color = d.color;
+			if(!color) color = frappe.ui.color.get('blue', 'extra-light');
+			d.backgroundColor = color;
+			d.textColor = frappe.ui.color.get_contrast_color(color);
+		}
+		return d;
+	},
 	update_event: function(event, revertFunc) {
 		var me = this;
 		frappe.model.remove_from_locals(me.doctype, event.name);
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 1315da12ed01f0246195f27638a93e7783bd094c..85d0b4820f00a0908a032c7d536ceb1d72326b9f 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -12,7 +12,7 @@ frappe.views.CommunicationComposer = Class.extend({
 	make: function() {
 		var me = this;
 		this.dialog = new frappe.ui.Dialog({
-			title: (this.subject || ""),
+			title: (this.title || this.subject || __("New Email")),
 			no_submit_on_enter: true,
 			fields: this.get_fields(),
 			primary_action_label: __("Send"),
@@ -49,12 +49,12 @@ frappe.views.CommunicationComposer = Class.extend({
 		var fields= [
 			{label:__("To"), fieldtype:"Data", reqd: 0, fieldname:"recipients",length:524288},
 			{fieldtype: "Section Break", collapsible: 1, label: "CC & Standard Reply"},
-			{label:__("CC"), fieldtype:"Data", fieldname:"cc",length:524288},
+			{label:__("CC"), fieldtype:"Data", fieldname:"cc", length:524288},
 			{label:__("Standard Reply"), fieldtype:"Link", options:"Standard Reply",
 				fieldname:"standard_reply"},
 			{fieldtype: "Section Break"},
 			{label:__("Subject"), fieldtype:"Data", reqd: 1,
-				fieldname:"subject",length:524288},
+				fieldname:"subject", length:524288},
 			{fieldtype: "Section Break"},
 			{label:__("Message"), fieldtype:"Text Editor", reqd: 1,
 				fieldname:"content"},
@@ -444,6 +444,7 @@ frappe.views.CommunicationComposer = Class.extend({
 
 	send_email: function(btn, form_values, selected_attachments, print_html, print_format) {
 		var me = this;
+		me.dialog.hide();
 
 		if((form_values.send_email || form_values.communication_medium === "Email") && !form_values.recipients) {
 			frappe.msgprint(__("Enter Email Recipient(s)"));
@@ -496,8 +497,6 @@ frappe.views.CommunicationComposer = Class.extend({
 							[ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) );
 					}
 
-					me.dialog.hide();
-
 					if ((frappe.last_edited_communication[me.doc] || {})[me.key]) {
 						delete frappe.last_edited_communication[me.doc][me.key];
 					}
@@ -506,7 +505,7 @@ frappe.views.CommunicationComposer = Class.extend({
 						cur_frm.timeline.input && cur_frm.timeline.input.val("");
 						cur_frm.reload_doc();
 					}
-					
+
 					// try the success callback if it exists
 					if (me.success) {
 						try {
@@ -515,10 +514,10 @@ frappe.views.CommunicationComposer = Class.extend({
 							console.log(e);
 						}
 					}
-					
+
 				} else {
 					frappe.msgprint(__("There were errors while sending email. Please try again."));
-					
+
 					// try the error callback if it exists
 					if (me.error) {
 						try {
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index ceb0a696bb7f1751d09ba6637af46c683abd2a6d..ba78c489ca4a495baa955edd40b9737a82e9f326 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -184,12 +184,12 @@ frappe.views.QueryReport = Class.extend({
 			frappe.msgprint(__("You are not allowed to print this report"));
 			return false;
 		}
-
 		if(this.html_format) {
 			var content = frappe.render(this.html_format, {
 				data: frappe.slickgrid_tools.get_filtered_items(this.dataView),
 				filters: this.get_values(),
-				report: this
+				report: this,
+				data_to_be_printed: this.data_to_be_printed
 			});
 
 			frappe.render_grid({
@@ -223,7 +223,8 @@ frappe.views.QueryReport = Class.extend({
 			var content = frappe.render(this.html_format, {
 				data: frappe.slickgrid_tools.get_filtered_items(this.dataView),
 				filters:this.get_values(),
-				report:this
+				report:this,
+				data_to_be_printed: this.data_to_be_printed
 			});
 
 			//Render Report in HTML
@@ -487,6 +488,7 @@ frappe.views.QueryReport = Class.extend({
 
 		this.set_message(res.message);
 		this.setup_chart(res);
+		this.set_print_data(res.data_to_be_printed);
 
 		this.toggle_expand_collapse_buttons(this.is_tree_report);
 	},
@@ -897,5 +899,9 @@ frappe.views.QueryReport = Class.extend({
 		if(this.chart && opts.data && opts.data.rows && opts.data.rows.length) {
 			this.chart_area.toggle(true);
 		}
+	},
+
+	set_print_data: function(data_to_be_printed) {
+		this.data_to_be_printed = data_to_be_printed;
 	}
 })
diff --git a/frappe/public/js/legacy/form.js b/frappe/public/js/legacy/form.js
index e5ac26c2f9650bad8eb84f013256e5b6d9fe4c8d..69f03327effbd56bba4ce2c0ddcac5be9eeade67 100644
--- a/frappe/public/js/legacy/form.js
+++ b/frappe/public/js/legacy/form.js
@@ -335,7 +335,7 @@ _f.Frm.prototype.refresh_header = function(is_a_different_doc) {
 		! this.is_dirty() &&
 		! this.is_new() &&
 		this.doc.docstatus===0) {
-		this.dashboard.add_comment(__('Submit this document to confirm'), 'alert-warning', true);
+		this.dashboard.add_comment(__('Submit this document to confirm'), 'orange', true);
 	}
 
 	this.clear_custom_buttons();
@@ -459,6 +459,7 @@ _f.Frm.prototype.refresh = function(docname) {
 _f.Frm.prototype.show_if_needs_refresh = function() {
 	if(this.doc.__needs_refresh) {
 		if(this.doc.__unsaved) {
+			this.dashboard.clear_headline();
 			this.dashboard.set_headline_alert(__("This form has been modified after you have loaded it")
 				+ '<a class="btn btn-xs btn-primary pull-right" onclick="cur_frm.reload_doc()">'
 				+ __("Refresh") + '</a>', "alert-warning");
diff --git a/frappe/public/less/calendar.less b/frappe/public/less/calendar.less
index f69a7323e382411cd6964f32866e3e2981371a59..482beb3cfe1ade25d6f7a4b568b5ad3f6301f288 100644
--- a/frappe/public/less/calendar.less
+++ b/frappe/public/less/calendar.less
@@ -27,7 +27,7 @@ th.fc-widget-header {
 
 .fc-unthemed .fc-today {
 	background-color: #FFF !important;
-	
+
 	.fc-day-number {
 		background-color: @brand-primary;
 		min-width: 20px;
@@ -90,7 +90,6 @@ th.fc-day-header {
 }
 
 .fc-day-grid-event {
-	background-color: rgba(94, 100, 255, 0.2) !important;
 	border: none !important;
 	margin: 5px 4px 0 !important;
 	padding: 1px 5px !important;
diff --git a/frappe/public/less/email.less b/frappe/public/less/email.less
index f07107891ae32bb932f3323113100f0feeb8e853..b7f9ee90fccd967c1b2b7d7c2cbede9df17fcc88 100644
--- a/frappe/public/less/email.less
+++ b/frappe/public/less/email.less
@@ -64,7 +64,7 @@ hr {
 }
 
 .email-footer-container {
-	margin-top: 10px;
+	margin-top: 30px;
 
 	& > div:not(:last-child) {
 		margin-bottom: 5px;
diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less
index 12860bc07d5007c54cc52ddc444d46359367e906..3440d6f4b52c6e69cbb36fc4acc3dde0c6b2e4e2 100644
--- a/frappe/public/less/form.less
+++ b/frappe/public/less/form.less
@@ -391,17 +391,32 @@ h6.uppercase, .h6.uppercase {
 	.action-btns {
 	    position: absolute;
 	    right: 0;
-	    padding: 5px 15px 2px 5px;
+		padding: 8px 15px 0 5px;
+		.edit-btn-container {
+			margin-right: 13px;
+		}
 	}
 
 	.comment-header {
 		background-color: @light-bg;
-		padding: 10px 15px 10px 13px;
+		padding: 10px 15px 8px 13px;
 		margin: 0px;
 		color: @text-muted;
 		border-bottom: 1px solid @light-border-color;
 		&.links-active {
-			padding-right: 60px;
+			padding-right: 77px;
+		}
+		.asset-details {
+			display: inline-block;
+			width: 100%;
+			.btn-link {
+				border: 0;
+				border-radius: 0;
+				padding: 0;
+				&:hover {
+					text-decoration: none;
+				}
+			}
 		}
 		.commented-on-small {
 			display: none;
@@ -434,7 +449,8 @@ h6.uppercase, .h6.uppercase {
 		.close {
 			color: inherit;
 			opacity: 1;
-			padding: 0 0 0 10px;
+			padding: 0;
+			font-size: 18px;
 		}
 	}
 
@@ -530,7 +546,8 @@ h6.uppercase, .h6.uppercase {
 }
 
 .timeline-item .reply-link {
-	padding-left: 7px;
+	margin-left: 15px;
+	font-size: 12px;
 }
 
 .timeline-head {
diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less
index 517c6e4059d8301db8980152492be9975dab3904..d14a533ea9e2cf22a838689e57d26b192bc4360e 100644
--- a/frappe/public/less/list.less
+++ b/frappe/public/less/list.less
@@ -226,8 +226,27 @@
 	padding: 5px 15px;
 }
 
-.listview-main-section .octicon-heart {
-	cursor: pointer;
+.listview-main-section {
+	.octicon-heart {
+		cursor: pointer;
+	}
+	.page-form {
+		padding-left: 17px;
+
+		@media (max-width: @screen-sm) {
+			padding-left: 25px;
+		}
+
+		.octicon-search {
+		    float: left;
+		    padding-top: 7px;
+		    margin-left: -4px;
+		    margin-right: -4px;
+			@media (max-width: @screen-sm) {
+				margin-left: -12px;
+			}
+		}
+	}
 }
 
 .like-action.octicon-heart {
diff --git a/frappe/public/less/mobile.less b/frappe/public/less/mobile.less
index 40d673c169e8ceef7e1a81471472bdfcbeac38bd..3c93a88177d996f61d61eea6af2140863011d508 100644
--- a/frappe/public/less/mobile.less
+++ b/frappe/public/less/mobile.less
@@ -34,6 +34,9 @@ body {
 	body[data-route^="Form"] {
 		.page-title h1 {
 			margin-top: 12px;
+			&.editable-title {
+				padding-right: 80px;
+			}
 		}
 
 		.page-title .indicator {
@@ -230,7 +233,7 @@ body {
 		.page-title {
 			.title-text {
 				font-size: 16px;
-				width: calc(~"100% - 30px");
+				width: calc(~"100% - 90px");
 			}
 			.indicator {
 				float: left;
@@ -432,13 +435,22 @@ body {
 					}
 				}
 				.action-btns {
-					padding: 5px 10px 2px 5px;
+					padding: 7px 10px 2px 5px;
+					.edit-btn-container {
+						margin-right: 0;
+					}
 				}
 				.comment-header{
 					padding: 7px 10px;
 					.links-active {
 						padding-right: 10px;
 					}
+					.reply-link {
+						margin-left: 0;
+					}
+					.asset-details {
+						width: calc(~"100% - 30px")
+					}
 				}
 				.avatar-medium {
 				    margin-right: 10px;
diff --git a/frappe/public/less/page.less b/frappe/public/less/page.less
index d141c5bc133e3e01b5054462b99d4c5c5141f8d1..5dc338d3ece02dcaf9c98e6f992b532da24d2d15 100644
--- a/frappe/public/less/page.less
+++ b/frappe/public/less/page.less
@@ -54,7 +54,6 @@
 	}
 
 	.title-image {
-		display: inline-block;
 		width: 46px;
 		height: 0;
 		padding: 23px 0;
@@ -66,6 +65,7 @@
 		text-align: center;
 		line-height: 0;
 		float: left;
+		margin-right: 10px;
 	}
 }
 
diff --git a/frappe/public/less/website.less b/frappe/public/less/website.less
index 6e9e7916a1a4c9454d5a625498fa9f96ebb14b93..87f5065e66ad40b16884667a182dd46f40e4efc9 100644
--- a/frappe/public/less/website.less
+++ b/frappe/public/less/website.less
@@ -125,6 +125,7 @@ li {
 }
 
 .page_content {
+	padding-top: 30px;
 	padding-bottom: 30px;
 }
 
@@ -181,6 +182,7 @@ li {
 }
 
 .page-head {
+	margin-bottom: -30px;
 	h1, h2 {
 		margin-top: 0px;
 	}
@@ -221,9 +223,13 @@ fieldset {
 }
 
 .page-container {
-	padding: 0px;
+	display: flex;
 	max-width: 970px;
-	margin: auto;
+	margin: 0 auto;
+
+	@media(max-width: @screen-xs) {
+		flex-direction: column-reverse;
+	}
 }
 
 .page-max-width {
@@ -241,12 +247,11 @@ fieldset {
 .web-sidebar {
 	position: relative;
 
-	.sidebar-item {
+	.sidebar-item:not(:last-child) {
 		margin: 0px;
 		padding-bottom: 12px;
 		border: none;
 		color: @text-muted;
-		font-size: 12px;
 
 		.badge {
 			font-weight: normal;
@@ -255,21 +260,22 @@ fieldset {
 	}
 
 	.sidebar-item a {
-		color: @text-color !important;
-	}
+		color: @text-muted;
 
-	.sidebar-item a.active {
-		color: @text-color !important;
-		font-weight: 500 !important;
+		&.active {
+			color: @text-color;
+		}
 	}
 
 	.sidebar-items {
-		// margin-top:30px;
-		margin-bottom:30px;
 		.title{
 			font-size: 14px;
 			font-weight: bold;
 		}
+
+		ul {
+			margin-bottom: 0;
+		}
 	}
 }
 
@@ -378,11 +384,6 @@ textarea {
 	}
 }
 
-@media (max-width: 767px) {
-	.visible-xs {
-		display: inline-block !important;
-	}
-}
 
 .more-block {
 	padding-bottom: 30px;
@@ -477,16 +478,54 @@ a.active {
 	margin-top: 60px;
 }
 
-.sidebar-block, .page-content {
+.sidebar-block {
+	flex: 1;
+	font-size: @text-medium;
+	border-right: 1px solid @border-color;
+	padding: 30px;
+	padding-left: 0px;
+
+	@media(max-width: @screen-xs) {
+		font-size: @text-regular;
+		border-right: none;
+		border-top: 1px solid @border-color;
+		padding-left: 20px;
+	}
+}
+
+.page-content {
+	flex: 6;
+
+	h1:first-child {
+		margin-top: 0;
+	}
+}
+
+.page-content.with-sidebar {
+	padding: 30px;
+	padding-left: 40px;
+}
+
+.page-content.without-sidebar {
 	padding-top: 30px;
-	padding-bottom: 50px;
 }
+
 .your-account-info {
 	margin-top: 30px;
 }
 
-.page-content.with-sidebar {
-	padding-left: 50px;
+@media (max-width: 767px) {
+	.visible-xs {
+		display: inline-block !important;
+	}
+	.sidebar-block {
+		width: 100%;
+	}
+	.page-content.with-sidebar {
+		width: 100%;
+		padding-left: 20px;
+		padding-right: 20px;
+	}
 }
 
 @media screen and (max-width: 480px) {
diff --git a/frappe/templates/includes/list/list.html b/frappe/templates/includes/list/list.html
index 3a1c72fb5d31f0f74db8c7863a6aacacb5ca75a6..bf2efba02be3878b688157a418d3f801707f0140 100644
--- a/frappe/templates/includes/list/list.html
+++ b/frappe/templates/includes/list/list.html
@@ -20,7 +20,7 @@
     		{% endfor %}
     	</div>
     	<div class="more-block {% if not show_more -%} hide {%- endif %}">
-               <button class="btn btn-default btn-more">{{ _("More") }}</button>
+    		<button class="btn btn-default btn-more btn-sm">{{ _("More") }}</button>
     	</div>
     </div>
 {%- endif %}
diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js
index 88eb76daf2043a7c35eda4c0b99ced2143503619..69e1199254e5b580ade23fc268d004d02014064a 100644
--- a/frappe/templates/includes/login/login.js
+++ b/frappe/templates/includes/login/login.js
@@ -5,11 +5,14 @@ window.disable_signup = {{ disable_signup and "true" or "false" }};
 
 window.login = {};
 
+window.verify = {};
+
 login.bind_events = function() {
 	$(window).on("hashchange", function() {
 		login.route();
 	});
 
+
 	$(".form-login").on("submit", function(event) {
 		event.preventDefault();
 		var args = {};
@@ -92,6 +95,11 @@ login.login = function() {
 	$(".for-login").toggle(true);
 }
 
+login.steptwo = function() {
+	login.reset_sections();
+	$(".for-login").toggle(true);
+}
+
 login.forgot = function() {
 	login.reset_sections();
 	$(".for-forgot").toggle(true);
@@ -150,7 +158,7 @@ login.login_handlers = (function() {
 
 	var login_handlers = {
 		200: function(data) {
-			if(data.message=="Logged In") {
+			if(data.message == 'Logged In'){
 				login.set_indicator("{{ _("Success") }}", 'green');
 				window.location.href = get_url_arg("redirect-to") || data.home_page;
 			} else if(data.message=="No App") {
@@ -190,15 +198,31 @@ login.login_handlers = (function() {
 				}
 				//login.set_indicator(__(data.message), 'green');
 			}
+
+			//OTP verification
+			if(data.verification && data.message != 'Logged In') {
+				login.set_indicator("{{ _("Success") }}", 'green');
+
+				document.cookie = "tmp_id="+data.tmp_id;
+
+				if (data.verification.method == 'OTP App'){
+					continue_otp_app(data.verification.setup, data.verification.qrcode);
+				} else if (data.verification.method == 'SMS'){
+					continue_sms(data.verification.setup, data.verification.prompt);
+				} else if (data.verification.method == 'Email'){
+					continue_email(data.verification.setup, data.verification.prompt);
+				}
+			}
 		},
 		401: get_error_handler("{{ _("Invalid Login. Try again.") }}"),
 		417: get_error_handler("{{ _("Oops! Something went wrong") }}")
 	};
 
 	return login_handlers;
-})();
+} )();
 
 frappe.ready(function() {
+
 	login.bind_events();
 
 	if (!window.location.hash) {
@@ -210,3 +234,76 @@ frappe.ready(function() {
 	$(".form-signup, .form-forgot").removeClass("hide");
 	$(document).trigger('login_rendered');
 });
+
+var verify_token =  function(event) {
+	$(".form-verify").on("submit", function(eventx) {
+		eventx.preventDefault();
+		var args = {};
+		args.cmd = "login";
+		args.otp = $("#login_token").val();
+		args.tmp_id = frappe.get_cookie('tmp_id');
+		if(!args.otp) {
+			frappe.msgprint('{{ _("Login token required") }}');
+			return false;
+		}
+		login.call(args);
+		return false;
+	});
+}
+
+var request_otp = function(r){
+	$('.login-content').empty().append($('<div>').attr({'id':'twofactor_div'}).html(
+		'<form class="form-verify">\
+			<div class="page-card-head">\
+				<span class="indicator blue" data-text="Verification">Verification</span>\
+			</div>\
+			<div id="otp_div"></div>\
+			<input type="text" id="login_token" autocomplete="off" class="form-control" placeholder="Verification Code" required="" autofocus="">\
+			<button class="btn btn-sm btn-primary btn-block" id="verify_token">Verify</button>\
+		</form>'));
+	// add event handler for submit button
+	verify_token();
+}
+
+var continue_otp_app = function(setup, qrcode){
+	request_otp();
+	var qrcode_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');
+
+	if (setup){
+		direction = $('<div>').attr('id','qr_info').text('Enter Code displayed in OTP App.');
+		qrcode_div.append(direction);
+		$('#otp_div').prepend(qrcode_div);
+	} else {
+		direction = $('<div>').attr('id','qr_info').text('OTP setup using OTP App was not completed. Please contact Administrator.');
+		qrcode_div.append(direction);
+		$('#otp_div').prepend(qrcode_div);
+	}
+}
+
+var continue_sms = function(setup, prompt){
+	request_otp();
+	var sms_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');
+
+	if (setup){
+		sms_div.append(prompt)
+		$('#otp_div').prepend(sms_div);
+	} else {
+		direction = $('<div>').attr('id','qr_info').text(prompt || 'SMS was not sent. Please contact Administrator.');
+		sms_div.append(direction);
+		$('#otp_div').prepend(sms_div)
+	}
+}
+
+var continue_email = function(setup, prompt){
+	request_otp();
+	var email_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');
+
+	if (setup){
+		email_div.append(prompt)
+		$('#otp_div').prepend(email_div);
+	} else {
+		var direction = $('<div>').attr('id','qr_info').text(prompt || 'Verification code email not sent. Please contact Administrator.');
+		email_div.append(direction);
+		$('#otp_div').prepend(email_div);
+	}
+}
\ No newline at end of file
diff --git a/frappe/templates/web.html b/frappe/templates/web.html
index 3f0fc56a856459ac817ac02ff625c2308d674ff7..44b5c1cb8cb51d4cbee3b27fa970e801e9a29076 100644
--- a/frappe/templates/web.html
+++ b/frappe/templates/web.html
@@ -6,13 +6,12 @@
 	data-path="{{ pathname }}"
 	{%- if page_or_generator=="Generator" %}
 		data-doctype="{{ doctype }}"{% endif %}>
-	<div class="row {% if show_sidebar %}vert-line{% endif %}">
 		{% if show_sidebar %}
-			<div class="col-sm-3 sidebar-block hidden-xs">
+			<div class="sidebar-block">
 				{% include "templates/includes/web_sidebar.html" %}
 			</div>
 		{% endif %}
-		<div class="{% if show_sidebar %}page-content with-sidebar col-sm-9{% else %} page-content col-sm-12 {% endif %}">
+		<div class="{% if show_sidebar %}page-content with-sidebar{% else %}page-content without-sidebar{% endif %}">
 			<div class="page-content-wrapper">
 				<div class="row page-head">
 					<div class='col-sm-12'>
@@ -48,7 +47,7 @@
 					{%- block page_content -%}{%- endblock -%}
 				</div>
 			</div>
+			<!-- sidebar ends -->
 		</div>
-	</div>
 </div>
 {% endblock %}
diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py
new file mode 100644
index 0000000000000000000000000000000000000000..e993b2d517fd5fe0bd45bc443fd3fef5e8f07285
--- /dev/null
+++ b/frappe/tests/test_twofactor.py
@@ -0,0 +1,132 @@
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+from __future__ import unicode_literals
+
+import unittest, frappe, pyotp
+from werkzeug.wrappers import Request
+from werkzeug.test import EnvironBuilder
+from frappe.auth import HTTPRequest
+from frappe.twofactor import (should_run_2fa, authenticate_for_2factor, get_cached_user_pass,
+	two_factor_is_enabled_for_, confirm_otp_token, get_otpsecret_for_, get_verification_obj,
+	render_string_template)
+
+import time
+
+class TestTwoFactor(unittest.TestCase):
+	def setUp(self):
+		self.http_requests = create_http_request()
+		self.login_manager = frappe.local.login_manager
+		self.user = self.login_manager.user
+
+	def tearDown(self):
+		frappe.local.response['verification'] = None
+		frappe.local.response['tmp_id'] = None
+		disable_2fa()
+		frappe.clear_cache(user=self.user)
+
+	def test_should_run_2fa(self):
+		'''Should return true if enabled.'''
+		toggle_2fa_all_role(state=True)
+		self.assertTrue(should_run_2fa(self.user))
+		toggle_2fa_all_role(state=False)
+		self.assertFalse(should_run_2fa(self.user))
+
+	def test_get_cached_user_pass(self):
+		'''Cached data should not contain user and pass before 2fa.'''
+		user,pwd = get_cached_user_pass()
+		self.assertTrue(all([not user, not pwd]))
+
+	def test_authenticate_for_2factor(self):
+		'''Verification obj and tmp_id should be set in frappe.local.'''
+		authenticate_for_2factor(self.user)
+		verification_obj = frappe.local.response['verification']
+		tmp_id = frappe.local.response['tmp_id']
+		self.assertTrue(verification_obj)
+		self.assertTrue(tmp_id)
+		for k in ['_usr','_pwd','_otp_secret']:
+			self.assertTrue(frappe.cache().get('{0}{1}'.format(tmp_id,k)),
+							'{} not available'.format(k))
+
+	def test_two_factor_is_enabled_for_user(self):
+		'''Should return true if enabled for user.'''
+		toggle_2fa_all_role(state=True)
+		self.assertTrue(two_factor_is_enabled_for_(self.user))
+		toggle_2fa_all_role(state=False)
+		self.assertFalse(two_factor_is_enabled_for_(self.user))
+
+	def test_get_otpsecret_for_user(self):
+		'''OTP secret should be set for user.'''
+		self.assertTrue(get_otpsecret_for_(self.user))
+		self.assertTrue(frappe.db.get_default(self.user + '_otpsecret'))
+
+	def test_confirm_otp_token(self):
+		'''Ensure otp is confirmed'''
+		authenticate_for_2factor(self.user)
+		tmp_id = frappe.local.response['tmp_id']
+		otp = 'wrongotp'
+		with self.assertRaises(frappe.AuthenticationError):
+			confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)
+		otp = get_otp(self.user)
+		self.assertTrue(confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id))
+		if frappe.flags.tests_verbose:
+			print('Sleeping for 30secs to confirm token expires..')
+		time.sleep(30)
+		with self.assertRaises(frappe.AuthenticationError):
+			confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)
+
+	def test_get_verification_obj(self):
+		'''Confirm verification object is returned.'''
+		otp_secret = get_otpsecret_for_(self.user)
+		token = int(pyotp.TOTP(otp_secret).now())
+		self.assertTrue(get_verification_obj(self.user,token,otp_secret))
+
+	def test_render_string_template(self):
+		'''String template renders as expected with variables.'''
+		args = {'issuer_name':'Frappe Technologies'}
+		_str = 'Verification Code from {{issuer_name}}'
+		_str = render_string_template(_str,args)
+		self.assertEqual(_str,'Verification Code from Frappe Technologies')
+
+
+def set_request(**kwargs):
+	builder = EnvironBuilder(**kwargs)
+	frappe.local.request = Request(builder.get_environ())
+
+def create_http_request():
+	'''Get http request object.'''
+	set_request(method='POST', path='login')
+	enable_2fa()
+	frappe.form_dict['usr'] = 'test@erpnext.com'
+	frappe.form_dict['pwd'] = 'test'
+	frappe.local.form_dict['cmd'] = 'login'
+	http_requests = HTTPRequest()
+	return http_requests
+
+def enable_2fa():
+	'''Enable Two factor in system settings.'''
+	system_settings = frappe.get_doc('System Settings')
+	system_settings.enable_two_factor_auth = 1
+	system_settings.two_factor_method = 'OTP App'
+	system_settings.save(ignore_permissions=True)
+	frappe.db.commit()
+
+def disable_2fa():
+	system_settings = frappe.get_doc('System Settings')
+	system_settings.enable_two_factor_auth = 0
+	system_settings.save(ignore_permissions=True)
+	frappe.db.commit()
+
+def toggle_2fa_all_role(state=None):
+	'''Enable or disable 2fa for 'all' role on the system.'''
+	all_role = frappe.get_doc('Role','All')
+	if state == None:
+		state = False if all_role.two_factor_auth == True else False
+	if state not in [True,False]:return
+	all_role.two_factor_auth = state
+	all_role.save(ignore_permissions=True)
+	frappe.db.commit()
+
+def get_otp(user):
+	otp_secret = get_otpsecret_for_(user)
+	otp = pyotp.TOTP(otp_secret)
+	return otp.now()
\ No newline at end of file
diff --git a/frappe/tests/ui/test_test_runner.py b/frappe/tests/ui/test_test_runner.py
index 8b396b6b957d8366c90ebf9717543f6eade3dbd4..fec5a20d8297895d9aa8e09d4f9671525e18d41c 100644
--- a/frappe/tests/ui/test_test_runner.py
+++ b/frappe/tests/ui/test_test_runner.py
@@ -6,6 +6,7 @@ class TestTestRunner(unittest.TestCase):
 	def test_test_runner(self):
 		driver = TestDriver()
 		driver.login()
+		frappe.db.set_default('in_selenium', '1')
 		for test in get_tests():
 			if test.startswith('#'):
 				continue
@@ -33,6 +34,7 @@ class TestTestRunner(unittest.TestCase):
 			print('Checking if passed "{0}"'.format(test))
 			self.assertTrue('Tests Passed' in console)
 			time.sleep(1)
+		frappe.db.set_default('in_selenium', None)
 		driver.close()
 
 def get_tests():
diff --git a/frappe/tests/ui/tests.txt b/frappe/tests/ui/tests.txt
index 93907305f70ff408546de05c7277a2947abe1e26..221257babc26c6ed88509383f1693b37bd6b952e 100644
--- a/frappe/tests/ui/tests.txt
+++ b/frappe/tests/ui/tests.txt
@@ -9,4 +9,5 @@ frappe/tests/ui/test_kanban/test_kanban_filters.js
 frappe/tests/ui/test_kanban/test_kanban_column.js
 frappe/core/doctype/report/test_query_report.js
 frappe/tests/ui/test_linked_with.js
-frappe/custom/doctype/customize_form/test_customize_form.js
\ No newline at end of file
+frappe/custom/doctype/customize_form/test_customize_form.js
+frappe/desk/doctype/event/test_event.js
diff --git a/frappe/twofactor.py b/frappe/twofactor.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb796fd2965ef5b1541d7d31e9322f6c30cf7e93
--- /dev/null
+++ b/frappe/twofactor.py
@@ -0,0 +1,369 @@
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe import _
+import pyotp, os
+from frappe.utils.background_jobs import enqueue
+from jinja2 import Template
+from pyqrcode import create as qrcreate
+from StringIO import StringIO
+from base64 import b64encode, b32encode
+from frappe.utils import get_url, get_datetime, time_diff_in_seconds
+
+class ExpiredLoginException(Exception): pass
+
+def toggle_two_factor_auth(state, roles=[]):
+	'''Enable or disable 2FA in site_config and roles'''
+	for role in roles:
+		role = frappe.get_doc('Role', {'role_name': role})
+		role.two_factor_auth = state
+		role.save(ignore_permissions=True)
+
+def two_factor_is_enabled(user=None):
+	'''Returns True if 2FA is enabled.'''
+	enabled = int(frappe.db.get_value('System Settings', None, 'enable_two_factor_auth') or 0)
+	if not user or not enabled:
+		return enabled
+	return two_factor_is_enabled_for_(user)
+
+def should_run_2fa(user):
+	'''Check if 2fa should run.'''
+	return two_factor_is_enabled(user=user)
+
+def get_cached_user_pass():
+	'''Get user and password if set.'''
+	user = pwd = None
+	tmp_id = frappe.form_dict.get('tmp_id')
+	if tmp_id:
+		user = frappe.cache().get(tmp_id+'_usr')
+		pwd = frappe.cache().get(tmp_id+'_pwd')
+	return (user, pwd)
+
+def authenticate_for_2factor(user):
+	'''Authenticate two factor for enabled user before login.'''
+	if frappe.form_dict.get('otp'):
+		return
+	otp_secret = get_otpsecret_for_(user)
+	token = int(pyotp.TOTP(otp_secret).now())
+	tmp_id = frappe.generate_hash(length=8)
+	cache_2fa_data(user, token, otp_secret, tmp_id)
+	verification_obj = get_verification_obj(user, token, otp_secret)
+	# Save data in local
+	frappe.local.response['verification'] = verification_obj
+	frappe.local.response['tmp_id'] = tmp_id
+
+def cache_2fa_data(user, token, otp_secret, tmp_id):
+	'''Cache and set expiry for data.'''
+	pwd = frappe.form_dict.get('pwd')
+	verification_method = get_verification_method()
+
+	# set increased expiry time for SMS and Email
+	if verification_method in ['SMS', 'Email']:
+		expiry_time = 300
+		frappe.cache().set(tmp_id + '_token', token)
+		frappe.cache().expire(tmp_id + '_token', expiry_time)
+	else:
+		expiry_time = 180
+	for k, v in {'_usr': user, '_pwd': pwd, '_otp_secret': otp_secret}.iteritems():
+		frappe.cache().set("{0}{1}".format(tmp_id, k), v)
+		frappe.cache().expire("{0}{1}".format(tmp_id, k), expiry_time)
+
+def two_factor_is_enabled_for_(user):
+	'''Check if 2factor is enabled for user.'''
+	if isinstance(user, basestring):
+		user = frappe.get_doc('User', user)
+
+	roles = [frappe.db.escape(d.role) for d in user.roles or []]
+	roles.append('All')
+
+	query = """select name from `tabRole` where two_factor_auth=1
+		and name in ({0}) limit 1""".format(', '.join('\"{}\"'.format(i) for \
+											i in roles))
+	if len(frappe.db.sql(query)) > 0:
+		return True
+
+	return False
+
+def get_otpsecret_for_(user):
+	'''Set OTP Secret for user even if not set.'''
+	otp_secret = frappe.db.get_default(user + '_otpsecret')
+	if not otp_secret:
+		otp_secret = b32encode(os.urandom(10)).decode('utf-8')
+		frappe.db.set_default(user + '_otpsecret', otp_secret)
+		frappe.db.commit()
+	return otp_secret
+
+def get_verification_method():
+	return frappe.db.get_value('System Settings', None, 'two_factor_method')
+
+def confirm_otp_token(login_manager, otp=None, tmp_id=None):
+	'''Confirm otp matches.'''
+	if not otp:
+		otp = frappe.form_dict.get('otp')
+	if not otp:
+		if two_factor_is_enabled_for_(login_manager.user):
+			return False
+		return True
+	if not tmp_id:
+		tmp_id = frappe.form_dict.get('tmp_id')
+	hotp_token = frappe.cache().get(tmp_id + '_token')
+	otp_secret = frappe.cache().get(tmp_id + '_otp_secret')
+	if not otp_secret:
+		raise ExpiredLoginException(_('Login session expired, refresh page to retry'))
+	hotp = pyotp.HOTP(otp_secret)
+	if hotp_token:
+		if hotp.verify(otp, int(hotp_token)):
+			frappe.cache().delete(tmp_id + '_token')
+			return True
+		else:
+			login_manager.fail(_('Incorrect Verification code'), login_manager.user)
+
+	totp = pyotp.TOTP(otp_secret)
+	if totp.verify(otp):
+		# show qr code only once
+		if not frappe.db.get_default(login_manager.user + '_otplogin'):
+			frappe.db.set_default(login_manager.user + '_otplogin', 1)
+			delete_qrimage(login_manager.user)
+		return True
+	else:
+		login_manager.fail(_('Incorrect Verification code'), login_manager.user)
+
+
+def get_verification_obj(user, token, otp_secret):
+	otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
+	verification_method = get_verification_method()
+	verification_obj = None
+	if verification_method == 'SMS':
+		verification_obj = process_2fa_for_sms(user, token, otp_secret)
+	elif verification_method == 'OTP App':
+		#check if this if the first time that the user is trying to login. If so, send an email
+		if not frappe.db.get_default(user + '_otplogin'):
+			verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer, method='OTP App')
+		else:
+			verification_obj = process_2fa_for_otp_app(user, otp_secret, otp_issuer)
+	elif verification_method == 'Email':
+		verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer)
+	return verification_obj
+
+
+def process_2fa_for_sms(user, token, otp_secret):
+	'''Process sms method for 2fa.'''
+	phone = frappe.db.get_value('User', user, ['phone', 'mobile_no'], as_dict=1)
+	phone = phone.mobile_no or phone.phone
+	status = send_token_via_sms(otp_secret, token=token, phone_no=phone)
+	verification_obj = {
+		'token_delivery': status,
+		'prompt': status and 'Enter verification code sent to {}'.format(phone[:4] + '******' + phone[-3:]),
+		'method': 'SMS',
+		'setup': status
+	}
+	return verification_obj
+
+def process_2fa_for_otp_app(user, otp_secret, otp_issuer):
+	'''Process OTP App method for 2fa.'''
+	totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
+	if frappe.db.get_default(user + '_otplogin'):
+		otp_setup_completed = True
+	else:
+		otp_setup_completed = False
+
+	verification_obj = {
+		'totp_uri': totp_uri,
+		'method': 'OTP App',
+		'qrcode': get_qr_svg_code(totp_uri),
+		'setup': otp_setup_completed
+	}
+	return verification_obj
+
+def process_2fa_for_email(user, token, otp_secret, otp_issuer, method='Email'):
+	'''Process Email method for 2fa.'''
+	subject = None
+	message = None
+	status = True
+	prompt = ''
+	if method == 'OTP App' and not frappe.db.get_default(user + '_otplogin'):
+		'''Sending one-time email for OTP App'''
+		totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
+		qrcode_link = get_link_for_qrcode(user, totp_uri)
+		message = get_email_body_for_qr_code({'qrcode_link': qrcode_link})
+		subject = get_email_subject_for_qr_code({'qrcode_link': qrcode_link})
+		prompt = _('Please check your registered email address for instructions on how to proceed. Do not close this window as you will have to return to it.')
+	else:
+		'''Sending email verification'''
+		prompt = _('Verification code has been sent to your registered email address.')
+	status = send_token_via_email(user, token, otp_secret, otp_issuer, subject=subject, message=message)
+	verification_obj = {
+		'token_delivery': status,
+		'prompt': status and prompt,
+		'method': 'Email',
+		'setup': status
+	}
+	return verification_obj
+
+def get_email_subject_for_2fa(kwargs_dict):
+	'''Get email subject for 2fa.'''
+	subject_template = _('Login Verification Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name'))
+	subject = render_string_template(subject_template, kwargs_dict)
+	return subject
+
+def get_email_body_for_2fa(kwargs_dict):
+	'''Get email body for 2fa.'''
+	body_template = 'Enter this code to complete your login:<br><br> <b>{{otp}}</b>'
+	body = render_string_template(body_template, kwargs_dict)
+	return body
+
+def get_email_subject_for_qr_code(kwargs_dict):
+	'''Get QRCode email subject.'''
+	subject_template = _('One Time Password (OTP) Registration Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name'))
+	subject = render_string_template(subject_template, kwargs_dict)
+	return subject
+
+def get_email_body_for_qr_code(kwargs_dict):
+	'''Get QRCode email body.'''
+	body_template = 'Please click on the following link and follow the instructions on the page.<br><br> {{qrcode_link}}'
+	body = render_string_template(body_template, kwargs_dict)
+	return body
+
+def render_string_template(_str, kwargs_dict):
+	'''Render string with jinja.'''
+	s = Template(_str)
+	s = s.render(**kwargs_dict)
+	return s
+
+def get_link_for_qrcode(user, totp_uri):
+	'''Get link to temporary page showing QRCode.'''
+	key = frappe.generate_hash(length=20)
+	key_user = "{}_user".format(key)
+	key_uri = "{}_uri".format(key)
+	lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image'))
+	if lifespan<=0:
+		lifespan = 240
+	frappe.cache().set_value(key_uri, totp_uri, expires_in_sec=lifespan)
+	frappe.cache().set_value(key_user, user, expires_in_sec=lifespan)
+	return get_url('/qrcode?k={}'.format(key))
+
+def send_token_via_sms(otpsecret, token=None, phone_no=None):
+	'''Send token as sms to user.'''
+	otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
+	try:
+		from frappe.core.doctype.sms_settings.sms_settings import send_request
+	except:
+		return False
+
+	if not phone_no:
+		return False
+
+	ss = frappe.get_doc('SMS Settings', 'SMS Settings')
+	if not ss.sms_gateway_url:
+		return False
+
+	hotp = pyotp.HOTP(otpsecret)
+	args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token))), ss.sms_sender_name: otp_issuer}
+	for d in ss.get("parameters"):
+		args[d.parameter] = d.value
+
+	args[ss.receiver_parameter] = phone_no
+
+	sms_args = {'gateway_url': ss.sms_gateway_url, 'params': args}
+	enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args)
+	return True
+
+def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, message=None):
+	'''Send token to user as email.'''
+	user_email = frappe.db.get_value('User', user, 'email')
+	if not user_email:
+		return False
+	hotp = pyotp.HOTP(otp_secret)
+	otp = hotp.at(int(token))
+	template_args = {'otp': otp, 'otp_issuer': otp_issuer}
+	if not subject:
+		subject = get_email_subject_for_2fa(template_args)
+	if not message:
+		message = get_email_body_for_2fa(template_args)
+
+	email_args = {
+		'recipients': user_email,
+		'sender': None,
+		'subject': subject,
+		'message': message,
+		'header': [_('Verfication Code'), 'blue'],
+		'delayed': False,
+		'retry':3
+	}
+
+	enqueue(method=frappe.sendmail, queue='short',
+		timeout=300, event=None, async=True, job_name=None, now=False, **email_args)
+	return True
+
+def get_qr_svg_code(totp_uri):
+	'''Get SVG code to display Qrcode for OTP.'''
+	url = qrcreate(totp_uri)
+	svg = ''
+	stream = StringIO()
+	try:
+		url.svg(stream, scale=4, background="#eee", module_color="#222")
+		svg = stream.getvalue().replace('\n', '')
+		svg = b64encode(bytes(svg))
+	finally:
+		stream.close()
+	return svg
+
+def qrcode_as_png(user, totp_uri):
+	'''Save temporary Qrcode to server.'''
+	from frappe.utils.file_manager import save_file
+	folder = create_barcode_folder()
+	png_file_name = '{}.png'.format(frappe.generate_hash(length=20))
+	file_obj = save_file(png_file_name, png_file_name, 'User', user, folder=folder)
+	frappe.db.commit()
+	file_url = get_url(file_obj.file_url)
+	file_path = os.path.join(frappe.get_site_path('public', 'files'), file_obj.file_name)
+	url = qrcreate(totp_uri)
+	with open(file_path, 'w') as png_file:
+		url.png(png_file, scale=8, module_color=[0, 0, 0, 180], background=[0xff, 0xff, 0xcc])
+	return file_url
+
+def create_barcode_folder():
+	'''Get Barcodes folder.'''
+	folder_name = 'Barcodes'
+	folder = frappe.db.exists('File', {'file_name': folder_name})
+	if folder:
+		return folder
+	folder = frappe.get_doc({
+			'doctype': 'File',
+			'file_name': folder_name,
+			'is_folder':1,
+			'folder': 'Home'
+		})
+	folder.insert(ignore_permissions=True)
+	return folder.name
+
+def delete_qrimage(user, check_expiry=False):
+	'''Delete Qrimage when user logs in.'''
+	user_barcodes = frappe.get_all('File', {'attached_to_doctype': 'User',
+							'attached_to_name': user, 'folder': 'Home/Barcodes'})
+	for barcode in user_barcodes:
+		if check_expiry and not should_remove_barcode_image(barcode): continue
+		barcode = frappe.get_doc('File', barcode.name)
+		frappe.delete_doc('File', barcode.name, ignore_permissions=True)
+
+def delete_all_barcodes_for_users():
+	'''Task to delete all barcodes for user.'''
+	users = frappe.get_all('User', {'enabled':1})
+	for user in users:
+		delete_qrimage(user.name, check_expiry=True)
+
+def should_remove_barcode_image(barcode):
+	'''Check if it's time to delete barcode image from server. '''
+	if isinstance(barcode, basestring):
+		barcode = frappe.get_doc('File', barcode)
+	lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')
+	if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan):
+		return True
+	return False
+
+def disable():
+	frappe.db.set_value('System Settings', None, 'enable_two_factor_auth', 0)
+
diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py
index fe90f561f657156f0e7447b51942784911a340ba..4feafda718c6ebe38b4f94016390252dcf814d3b 100644
--- a/frappe/utils/redis_wrapper.py
+++ b/frappe/utils/redis_wrapper.py
@@ -3,7 +3,7 @@
 from __future__ import unicode_literals
 
 import redis, frappe, re
-import cPickle as pickle
+from six.moves import cPickle as pickle
 from frappe.utils import cstr
 from six import iteritems
 
diff --git a/frappe/website/router.py b/frappe/website/router.py
index 66781baf15675fad463559625452d6228e58ce19..ae90645ea43e75732c8609139eaaafd6bc4e226e 100644
--- a/frappe/website/router.py
+++ b/frappe/website/router.py
@@ -35,7 +35,6 @@ def get_page_context(path):
 		page_context = make_page_context(path)
 		if can_cache(page_context.no_cache):
 			page_context_cache[frappe.local.lang] = page_context
-
 			frappe.cache().hset("page_context", path, page_context_cache)
 
 	return page_context
diff --git a/frappe/website/utils.py b/frappe/website/utils.py
index c4f167f2bc4d7998b97312fbd73743363efa9969..7a80d03f851ba29842bd00f31b8fb64ba8765dff 100644
--- a/frappe/website/utils.py
+++ b/frappe/website/utils.py
@@ -24,7 +24,11 @@ def find_first_image(html):
 		return None
 
 def can_cache(no_cache=False):
-	return not (frappe.conf.disable_website_cache or getattr(frappe.local, "no_cache", False) or no_cache)
+	if frappe.conf.disable_website_cache or frappe.conf.developer_mode:
+		return False
+	if getattr(frappe.local, "no_cache", False):
+		return False
+	return not no_cache
 
 def get_comment_list(doctype, name):
 	return frappe.db.sql("""select
diff --git a/frappe/www/desk.html b/frappe/www/desk.html
index 5572df07faf5d2c63a88ef078e29ea23c52d5afa..841b82bf99daab74779a6c33fd0afeedaf2a2763 100644
--- a/frappe/www/desk.html
+++ b/frappe/www/desk.html
@@ -1,6 +1,11 @@
 <!DOCTYPE html>
 <head>
-	<meta charset="utf-8">
+	<!-- Chrome, Firefox OS and Opera -->
+	<meta name="theme-color" content="#7575ff">
+	<!-- Windows Phone -->
+	<meta name="msapplication-navbutton-color" content="#7575ff">
+	<!-- iOS Safari -->
+	<meta name="apple-mobile-web-app-status-bar-style" content="#7575ff">	<meta charset="utf-8">
 	<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
 	<meta content="utf-8" http-equiv="encoding">
 	<meta name="author" content="">
@@ -15,7 +20,7 @@
 	<link rel="icon"
 		href="{{ favicon or "/assets/frappe/images/favicon.png" }}" type="image/x-icon">
 	{% for include in include_css -%}
-	<link type="text/css" rel="stylesheet" href="{{ include }}">
+	<link type="text/css" rel="stylesheet" href="{{ include }}?ver={{ build_version }}">
 	{%- endfor -%}
 </head>
 <body>
@@ -50,7 +55,7 @@
 	</script>
 
 	{% for include in include_js %}
-	<script type="text/javascript" src="{{ include }}"></script>
+	<script type="text/javascript" src="{{ include }}?ver={{ build_version }}"></script>
 	{% endfor %}
     {% include "templates/includes/app_analytics/google_analytics.html" %}
     {% include "templates/includes/app_analytics/mixpanel_analytics.html" %}
diff --git a/frappe/www/desk.py b/frappe/www/desk.py
index ccc9577b34e3fe07e8681d84bb255acd5304a311..7dbaaf41c616727f2be2bf5fb1f32d16a89e9fc1 100644
--- a/frappe/www/desk.py
+++ b/frappe/www/desk.py
@@ -35,7 +35,8 @@ def get_context(context):
 	# remove script tags from boot
 	boot_json = re.sub("\<script\>[^<]*\</script\>", "", boot_json)
 
-	return {
+	context.update({
+		"no_cache": 1,
 		"build_version": get_build_version(),
 		"include_js": hooks["app_include_js"],
 		"include_css": hooks["app_include_css"],
@@ -46,7 +47,7 @@ def get_context(context):
 			(boot.user.background_image or boot.default_background_image) or None),
 		"google_analytics_id": frappe.conf.get("google_analytics_id"),
 		"mixpanel_id": frappe.conf.get("mixpanel_id")
-	}
+	})
 
 @frappe.whitelist()
 def get_desk_assets(build_version):
@@ -64,7 +65,7 @@ def get_desk_assets(build_version):
 			try:
 				with open(os.path.join(frappe.local.sites_path, path) ,"r") as f:
 					assets[0]["data"] = assets[0]["data"] + "\n" + text_type(f.read(), "utf-8")
-			except IOError as e:
+			except IOError:
 				pass
 
 		for path in data["include_css"]:
@@ -78,5 +79,4 @@ def get_desk_assets(build_version):
 	}
 
 def get_build_version():
-	return str(os.path.getmtime(os.path.join(frappe.local.sites_path, "assets", "js",
-			"desk.min.js")))
+	return str(os.path.getmtime(os.path.join(frappe.local.sites_path, '.build')))
diff --git a/frappe/www/login.html b/frappe/www/login.html
index e95f0fe63be012f2e5eec34e618b65b38b5ba344..003b234bb6c48d8db37767dcbd68356fcf49cfef 100644
--- a/frappe/www/login.html
+++ b/frappe/www/login.html
@@ -9,16 +9,16 @@
 {% block page_content %}
 <!-- {{ for_test }} -->
 <section class='for-login'>
-	<div class="login-content page-card" style="margin-top: 20px;">
+	<div class="login-content page-card" style="margin-top: 30px;">
 		<form class="form-signin form-login" role="form">
 			<div class="page-card-head">
 				<span class="indicator blue" data-text="{{ _("Sign In") }}"></span>
 			</div>
 
 			<input type="text" id="login_email"
-				class="form-control" placeholder="{{ 
-					_('Email address or Mobile number') 
-					if frappe.utils.cint(frappe.db.get_value('System Settings', 'System Settings', 'allow_login_using_mobile_number')) 
+				class="form-control" placeholder="{{
+					_('Email address or Mobile number')
+					if frappe.utils.cint(frappe.db.get_value('System Settings', 'System Settings', 'allow_login_using_mobile_number'))
 					else _('Email address') }}"
 				required autofocus>
 
diff --git a/frappe/www/login.py b/frappe/www/login.py
index cc149abbec9b2f7a1022d87febfeb80fb08b73f5..5002a44b35af7a8f5e99d57210aee09158b0eb16 100644
--- a/frappe/www/login.py
+++ b/frappe/www/login.py
@@ -68,4 +68,3 @@ def login_via_token(login_token):
 	frappe.local.login_manager = LoginManager()
 
 	redirect_post_login(desk_user = frappe.db.get_value("User", frappe.session.user, "user_type")=="System User")
-
diff --git a/frappe/www/qrcode.html b/frappe/www/qrcode.html
new file mode 100644
index 0000000000000000000000000000000000000000..4cbedb1060e8797ae3b581c3aab235e901659b76
--- /dev/null
+++ b/frappe/www/qrcode.html
@@ -0,0 +1,27 @@
+{% extends "templates/web.html" %}
+
+{% block title %}{{ _("QR Code") }}{% endblock %}
+
+{% block page_content %}
+<h1>{{ _("QR Code for Login Verification") }}</h1>
+<div class='row'>
+	<div class='col-sm-6'>
+		<p>{{ _("Hi {0}").format(qr_code_user.first_name) }},</p>
+
+		<p>{{ _("Steps to verify your login") }}:</p>
+			<ol>
+			<li> {{ _("Open your authentication app on your mobile phone.") }}
+			<li> {{ _("Scan the QR Code and enter the resulting code displayed.") }}
+			<li> {{ _("Return to the Verification screen and enter the code displayed by your authentication app") }}
+			</ol>
+		</p>
+		<br>
+		<p class='text-muted small'>{{ _("Authentication Apps you can use are: ") }}
+			Google Authenticator, Lastpass Authenticator, Authy and Duo Mobile.
+		</p>
+	</div>
+	<div class='col-sm-6' style='padding-top: 15px;'>
+		<img src="data:image/svg+xml;base64,{{qrcode_svg}}">
+	</div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/frappe/www/qrcode.py b/frappe/www/qrcode.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf7d79236e3d885a8a3f0806ae1aa90e405a1690
--- /dev/null
+++ b/frappe/www/qrcode.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe import _
+from urlparse import parse_qs
+from frappe.twofactor import get_qr_svg_code
+
+def get_context(context):
+	context.no_cache = 1
+	context.qr_code_user,context.qrcode_svg = get_user_svg_from_cache()
+
+def get_query_key():
+	'''Return query string arg.'''
+	query_string = frappe.local.request.query_string
+	query = parse_qs(query_string)
+	if not 'k' in query.keys():
+		frappe.throw(_('Not Permitted'),frappe.PermissionError)
+	query = (query['k'][0]).strip()
+	if False in [i.isalpha() or i.isdigit() for i in query]:
+		frappe.throw(_('Not Permitted'),frappe.PermissionError)
+	return query
+
+def get_user_svg_from_cache():
+	'''Get User and SVG code from cache.'''
+	key = get_query_key()
+	totp_uri = frappe.cache().get_value("{}_uri".format(key))
+	user = frappe.cache().get_value("{}_user".format(key))
+	if not totp_uri or not user:
+		frappe.throw(_('Page has expired!'),frappe.PermissionError)
+	if not frappe.db.exists('User',user):
+		frappe.throw(_('Not Permitted'), frappe.PermissionError)
+	user = frappe.get_doc('User',user)
+	svg = get_qr_svg_code(totp_uri)
+	return (user,svg)
diff --git a/package.json b/package.json
index b26b4f2a53ed74122dadc3c2bdb9c312c86562f5..10ad3df7ea4d6dc866ab49e79160e0bf53b25a24 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
     "nightwatch": "^0.9.16",
     "redis": "^2.7.1",
     "socket.io": "^2.0.1",
-    "superagent": "^3.5.2"
+    "superagent": "^3.5.2",
+	"touch": "^3.1.0"
   }
 }
diff --git a/requirements.txt b/requirements.txt
index 5beb2ecc3c179b4ed4aa6fecd945f99b8dbd8642..0f6a4ef421a60a35ac28ac0528a04ba46da13475 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -41,4 +41,8 @@ oauthlib
 PyJWT
 pypdf
 openpyxl
+pyotp
+pyqrcode
+pypng
 premailer
+