Merge branch 'develop' into fix-payment-entry-wrong-bank-account-fetch-develop
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index f9db14b..9df8655 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -911,7 +911,7 @@
 	elif reference_doctype != "Journal Entry":
 		if party_account_currency == company_currency:
 			if ref_doc.doctype == "Expense Claim":
-				total_amount = ref_doc.total_sanctioned_amount
+				total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
 			elif ref_doc.doctype == "Employee Advance":
 				total_amount = ref_doc.advance_amount
 			else:
@@ -929,8 +929,8 @@
 			outstanding_amount = ref_doc.get("outstanding_amount")
 			bill_no = ref_doc.get("bill_no")
 		elif reference_doctype == "Expense Claim":
-			outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) \
-				- flt(ref_doc.get("total_amount+reimbursed")) - flt(ref_doc.get("total_advance_amount"))
+			outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
+				- flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
 		elif reference_doctype == "Employee Advance":
 			outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount)
 		else:
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 287e00f..e93ec95 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -140,9 +140,6 @@
 		})
 
 	def set_as_paid(self):
-		if frappe.session.user == "Guest":
-			frappe.set_user("Administrator")
-
 		payment_entry = self.create_payment_entry()
 		self.make_invoice()
 
@@ -254,7 +251,7 @@
 
 		if status in ["Authorized", "Completed"]:
 			redirect_to = None
-			self.run_method("set_as_paid")
+			self.set_as_paid()
 
 			# if shopping cart enabled and in session
 			if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 639ef6c..df77dc8 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -26,6 +26,7 @@
   "accounting_dimensions_section",
   "cost_center",
   "dimension_col_break",
+  "project",
   "supplier_invoice_details",
   "bill_no",
   "column_break_15",
@@ -1319,13 +1320,19 @@
    "fieldtype": "Small Text",
    "label": "Billing Address",
    "read_only": 1
+  },
+  {
+   "fieldname": "project",
+   "fieldtype": "Link",
+   "label": "Project",
+   "options": "Project"
   }
  ],
  "icon": "fa fa-file-text",
  "idx": 204,
  "is_submittable": 1,
  "links": [],
- "modified": "2020-07-18 05:06:08.488761",
+ "modified": "2020-07-24 09:46:40.405463",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Purchase Invoice",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 4e22b05..2563b66 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -223,9 +223,9 @@
 		# IMP NOTE
 		# stock_ledger_entries should already be filtered by item_code and warehouse and
 		# sorted by posting_date desc, posting_time desc
-		if item_code in self.non_stock_items:
+		if item_code in self.non_stock_items and (row.project or row.cost_center):
 			#Issue 6089-Get last purchasing rate for non-stock item
-			item_rate = self.get_last_purchase_rate(item_code)
+			item_rate = self.get_last_purchase_rate(item_code, row)
 			return flt(row.qty) * item_rate
 
 		else:
@@ -253,38 +253,34 @@
 	def get_average_buying_rate(self, row, item_code):
 		args = row
 		if not item_code in self.average_buying_rate:
-			if item_code in self.non_stock_items:
-				self.average_buying_rate[item_code] = flt(frappe.db.sql("""
-					select sum(base_net_amount) / sum(qty * conversion_factor)
-					from `tabPurchase Invoice Item`
-					where item_code = %s and docstatus=1""", item_code)[0][0])
-			else:
-				args.update({
-					'voucher_type': row.parenttype,
-					'voucher_no': row.parent,
-					'allow_zero_valuation': True,
-					'company': self.filters.company
-				})
+			args.update({
+				'voucher_type': row.parenttype,
+				'voucher_no': row.parent,
+				'allow_zero_valuation': True,
+				'company': self.filters.company
+			})
 
-				average_buying_rate = get_incoming_rate(args)
-				self.average_buying_rate[item_code] =  flt(average_buying_rate)
+			average_buying_rate = get_incoming_rate(args)
+			self.average_buying_rate[item_code] =  flt(average_buying_rate)
 
 		return self.average_buying_rate[item_code]
 
-	def get_last_purchase_rate(self, item_code):
+	def get_last_purchase_rate(self, item_code, row):
+		condition = ''
+		if row.project:
+			condition += " AND a.project='%s'" % (row.project)
+		elif row.cost_center:
+			condition += " AND a.cost_center='%s'" % (row.cost_center)
 		if self.filters.to_date:
-			last_purchase_rate = frappe.db.sql("""
-			select (a.base_rate / a.conversion_factor)
-			from `tabPurchase Invoice Item` a
-			where a.item_code = %s and a.docstatus=1
-			and modified <= %s
-			order by a.modified desc limit 1""", (item_code, self.filters.to_date))
-		else:
-			last_purchase_rate = frappe.db.sql("""
-			select (a.base_rate / a.conversion_factor)
-			from `tabPurchase Invoice Item` a
-			where a.item_code = %s and a.docstatus=1
-			order by a.modified desc limit 1""", item_code)
+			condition += " AND modified='%s'" % (self.filters.to_date)
+
+		last_purchase_rate = frappe.db.sql("""
+		select (a.base_rate / a.conversion_factor)
+		from `tabPurchase Invoice Item` a
+		where a.item_code = %s and a.docstatus=1
+		{0}
+		order by a.modified desc limit 1""".format(condition), item_code)
+
 		return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
 
 	def load_invoice_items(self):
@@ -321,7 +317,8 @@
 				`tabSales Invoice Item`.brand, `tabSales Invoice Item`.dn_detail,
 				`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
 				`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
-				`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return
+				`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
+				`tabSales Invoice Item`.cost_center
 				{sales_person_cols}
 			from
 				`tabSales Invoice` inner join `tabSales Invoice Item`
diff --git a/erpnext/assets/assets_dashboard/asset/asset.json b/erpnext/assets/assets_dashboard/asset/asset.json
new file mode 100644
index 0000000..56b1e2a
--- /dev/null
+++ b/erpnext/assets/assets_dashboard/asset/asset.json
@@ -0,0 +1,39 @@
+{
+ "cards": [
+  {
+   "card": "Total Assets"
+  },
+  {
+   "card": "New Assets (This Year)"
+  },
+  {
+   "card": "Asset Value"
+  }
+ ],
+ "charts": [
+  {
+   "chart": "Asset Value Analytics",
+   "width": "Full"
+  },
+  {
+   "chart": "Category-wise Asset Value",
+   "width": "Half"
+  },
+  {
+   "chart": "Location-wise Asset Value",
+   "width": "Half"
+  }
+ ],
+ "creation": "2020-07-14 18:23:53.343082",
+ "dashboard_name": "Asset",
+ "docstatus": 0,
+ "doctype": "Dashboard",
+ "idx": 0,
+ "is_default": 0,
+ "is_standard": 1,
+ "modified": "2020-07-21 18:14:25.078929",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset",
+ "owner": "Administrator"
+}
\ No newline at end of file
diff --git a/erpnext/assets/dashboard_chart/asset_value_analytics/asset_value_analytics.json b/erpnext/assets/dashboard_chart/asset_value_analytics/asset_value_analytics.json
new file mode 100644
index 0000000..bc2edc9
--- /dev/null
+++ b/erpnext/assets/dashboard_chart/asset_value_analytics/asset_value_analytics.json
@@ -0,0 +1,27 @@
+{
+ "chart_name": "Asset Value Analytics",
+ "chart_type": "Report",
+ "creation": "2020-07-14 18:23:53.091233",
+ "custom_options": "{\"type\": \"bar\", \"barOptions\": {\"stacked\": 1}, \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}}",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
+ "filters_json": "{\"status\":\"In Location\",\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"date_based_on\":\"Purchase Date\",\"group_by\":\"--Select a group--\"}",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "modified": "2020-07-23 13:53:33.211371",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Value Analytics",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "report_name": "Fixed Asset Register",
+ "time_interval": "Yearly",
+ "timeseries": 0,
+ "timespan": "Last Year",
+ "type": "Bar",
+ "use_report_chart": 1,
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/assets/dashboard_chart/category_wise_asset_value/category_wise_asset_value.json b/erpnext/assets/dashboard_chart/category_wise_asset_value/category_wise_asset_value.json
new file mode 100644
index 0000000..e79d2d7
--- /dev/null
+++ b/erpnext/assets/dashboard_chart/category_wise_asset_value/category_wise_asset_value.json
@@ -0,0 +1,29 @@
+{
+ "chart_name": "Category-wise Asset Value",
+ "chart_type": "Report",
+ "creation": "2020-07-14 18:23:53.146304",
+ "custom_options": "{\"type\": \"donut\", \"height\": 300, \"axisOptions\": {\"shortenYAxisNumbers\": 1}}",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
+ "filters_json": "{\"status\":\"In Location\",\"group_by\":\"Asset Category\",\"is_existing_asset\":0}",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "modified": "2020-07-23 13:39:32.429240",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Category-wise Asset Value",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "report_name": "Fixed Asset Register",
+ "timeseries": 0,
+ "type": "Donut",
+ "use_report_chart": 0,
+ "x_field": "asset_category",
+ "y_axis": [
+  {
+   "y_field": "asset_value"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/assets/dashboard_chart/location_wise_asset_value/location_wise_asset_value.json b/erpnext/assets/dashboard_chart/location_wise_asset_value/location_wise_asset_value.json
new file mode 100644
index 0000000..481586e
--- /dev/null
+++ b/erpnext/assets/dashboard_chart/location_wise_asset_value/location_wise_asset_value.json
@@ -0,0 +1,29 @@
+{
+ "chart_name": "Location-wise Asset Value",
+ "chart_type": "Report",
+ "creation": "2020-07-14 18:23:53.195389",
+ "custom_options": "{\"type\": \"donut\", \"height\": 300, \"axisOptions\": {\"shortenYAxisNumbers\": 1}}",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
+ "filters_json": "{\"status\":\"In Location\",\"group_by\":\"Location\",\"is_existing_asset\":0}",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "modified": "2020-07-23 13:42:44.912551",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Location-wise Asset Value",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "report_name": "Fixed Asset Register",
+ "timeseries": 0,
+ "type": "Donut",
+ "use_report_chart": 0,
+ "x_field": "location",
+ "y_axis": [
+  {
+   "y_field": "asset_value"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/assets/number_card/asset_value/asset_value.json b/erpnext/assets/number_card/asset_value/asset_value.json
new file mode 100644
index 0000000..68e5f54
--- /dev/null
+++ b/erpnext/assets/number_card/asset_value/asset_value.json
@@ -0,0 +1,21 @@
+{
+ "aggregate_function_based_on": "value_after_depreciation",
+ "creation": "2020-07-14 18:23:53.302457",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Asset",
+ "filters_json": "[]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 1,
+ "is_standard": 1,
+ "label": "Asset Value",
+ "modified": "2020-07-21 18:13:47.647997",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Value",
+ "owner": "Administrator",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git "a/erpnext/assets/number_card/new_assets_\050this_year\051/new_assets_\050this_year\051.json" "b/erpnext/assets/number_card/new_assets_\050this_year\051/new_assets_\050this_year\051.json"
new file mode 100644
index 0000000..6c8fb35
--- /dev/null
+++ "b/erpnext/assets/number_card/new_assets_\050this_year\051/new_assets_\050this_year\051.json"
@@ -0,0 +1,20 @@
+{
+ "creation": "2020-07-14 18:23:53.267919",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Asset",
+ "filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 1,
+ "is_standard": 1,
+ "label": "New Assets (This Year)",
+ "modified": "2020-07-23 13:45:20.418766",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "New Assets (This Year)",
+ "owner": "Administrator",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/assets/number_card/total_assets/total_assets.json b/erpnext/assets/number_card/total_assets/total_assets.json
new file mode 100644
index 0000000..d127de8
--- /dev/null
+++ b/erpnext/assets/number_card/total_assets/total_assets.json
@@ -0,0 +1,20 @@
+{
+ "creation": "2020-07-14 18:23:53.233328",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Asset",
+ "filters_json": "[]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 1,
+ "is_standard": 1,
+ "label": "Total Assets",
+ "modified": "2020-07-21 18:12:51.664292",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Total Assets",
+ "owner": "Administrator",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json
index 736a9d6..0340364 100644
--- a/erpnext/crm/doctype/email_campaign/email_campaign.json
+++ b/erpnext/crm/doctype/email_campaign/email_campaign.json
@@ -1,4 +1,5 @@
 {
+ "actions": [],
  "autoname": "format:MAIL-CAMP-{YYYY}-{#####}",
  "creation": "2019-06-30 16:05:30.015615",
  "doctype": "DocType",
@@ -52,7 +53,7 @@
    "fieldtype": "Select",
    "in_list_view": 1,
    "label": "Email Campaign For ",
-   "options": "\nLead\nContact",
+   "options": "\nLead\nContact\nEmail Group",
    "reqd": 1
   },
   {
@@ -70,7 +71,8 @@
    "options": "User"
   }
  ],
- "modified": "2019-11-11 17:18:47.342839",
+ "links": [],
+ "modified": "2020-07-15 12:43:25.548682",
  "modified_by": "Administrator",
  "module": "CRM",
  "name": "Email Campaign",
diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py
index 8f60ecf..71c93e8 100644
--- a/erpnext/crm/doctype/email_campaign/email_campaign.py
+++ b/erpnext/crm/doctype/email_campaign/email_campaign.py
@@ -70,10 +70,15 @@
 				send_mail(entry, email_campaign)
 
 def send_mail(entry, email_campaign):
-	recipient = frappe.db.get_value(email_campaign.email_campaign_for, email_campaign.get("recipient"), 'email_id')
+	recipient_list = []
+	if email_campaign.email_campaign_for == "Email Group":
+		for member in frappe.db.get_list("Email Group Member", filters={"email_group": email_campaign.get("recipient")}, fields=["email"]):
+			recipient_list.append(member['email'])
+	else:
+		recipient_list.append(frappe.db.get_value(email_campaign.email_campaign_for, email_campaign.get("recipient"), "email_id"))
 
 	email_template = frappe.get_doc("Email Template", entry.get("email_template"))
-	sender = frappe.db.get_value("User", email_campaign.get("sender"), 'email')
+	sender = frappe.db.get_value("User", email_campaign.get("sender"), "email")
 	context = {"doc": frappe.get_doc(email_campaign.email_campaign_for, email_campaign.recipient)}
 	# send mail and link communication to document
 	comm = make(
@@ -82,7 +87,7 @@
 		subject = frappe.render_template(email_template.get("subject"), context),
 		content = frappe.render_template(email_template.get("response"), context),
 		sender = sender,
-		recipients = recipient,
+		recipients = recipient_list,
 		communication_medium = "Email",
 		sent_or_received = "Sent",
 		send_email = True,
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index fe033d4..bf9f221 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -152,7 +152,7 @@
 	:param fee_structure: Fee Structure.
 	"""
 	if fee_structure:
-		fs = frappe.get_list("Fee Component", fields=["fees_category", "amount"] , filters={"parent": fee_structure}, order_by= "idx")
+		fs = frappe.get_list("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx")
 		return fs
 
 
diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js
index 867866f..aaf42b4 100644
--- a/erpnext/education/doctype/fees/fees.js
+++ b/erpnext/education/doctype/fees/fees.js
@@ -162,6 +162,7 @@
 						$.each(r.message, function(i, d) {
 							var row = frappe.model.add_child(frm.doc, "Fee Component", "components");
 							row.fees_category = d.fees_category;
+							row.description = d.description;
 							row.amount = d.amount;
 						});
 					}
diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json
index 334b655..6546b08 100644
--- a/erpnext/healthcare/desk_page/healthcare/healthcare.json
+++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json
@@ -38,7 +38,7 @@
   {
    "hidden": 0,
    "label": "Records and History",
-   "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t}\n]"
+   "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t}\n]"
   },
   {
    "hidden": 0,
@@ -64,7 +64,7 @@
  "idx": 0,
  "is_standard": 1,
  "label": "Healthcare",
- "modified": "2020-05-28 19:02:28.824995",
+ "modified": "2020-06-25 23:50:56.951698",
  "modified_by": "Administrator",
  "module": "Healthcare",
  "name": "Healthcare",
diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py
index 30a1e45..63dd8d4 100644
--- a/erpnext/healthcare/doctype/patient/patient.py
+++ b/erpnext/healthcare/doctype/patient/patient.py
@@ -172,3 +172,15 @@
 	if vital_sign:
 		details.update(vital_sign[0])
 	return details
+
+def get_timeline_data(doctype, name):
+	"""Return timeline data from medical records"""
+	return dict(frappe.db.sql('''
+		SELECT
+			unix_timestamp(communication_date), count(*)
+		FROM
+			`tabPatient Medical Record`
+		WHERE
+			patient=%s
+			and `communication_date` > date_sub(curdate(), interval 1 year)
+		GROUP BY communication_date''', name))
diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json
index 15c9434..eb0021f 100644
--- a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json
+++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json
@@ -63,7 +63,8 @@
   {
    "fieldname": "assessment_datetime",
    "fieldtype": "Datetime",
-   "label": "Assessment Datetime"
+   "label": "Assessment Datetime",
+   "reqd": 1
   },
   {
    "fieldname": "section_break_7",
@@ -139,7 +140,7 @@
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2020-05-25 14:38:38.302399",
+ "modified": "2020-06-25 00:25:13.208400",
  "modified_by": "Administrator",
  "module": "Healthcare",
  "name": "Patient Assessment",
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index c19be17..e0f015f 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -5,6 +5,7 @@
 from __future__ import unicode_literals
 import frappe
 from frappe.model.document import Document
+from frappe.utils import today
 
 class TherapyPlan(Document):
 	def validate(self):
@@ -45,4 +46,6 @@
 	therapy_session.rate = therapy_type.rate
 	therapy_session.exercises = therapy_type.exercises
 
+	if frappe.flags.in_test:
+		therapy_session.start_date = today()
 	return therapy_session.as_dict()
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.json b/erpnext/healthcare/doctype/therapy_session/therapy_session.json
index c75d934..dc0cafc 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.json
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.json
@@ -154,7 +154,8 @@
   {
    "fieldname": "start_date",
    "fieldtype": "Date",
-   "label": "Start Date"
+   "label": "Start Date",
+   "reqd": 1
   },
   {
    "fieldname": "start_time",
@@ -219,7 +220,7 @@
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2020-06-29 14:33:34.836594",
+ "modified": "2020-06-30 10:56:10.354268",
  "modified_by": "Administrator",
  "module": "Healthcare",
  "name": "Therapy Session",
diff --git a/erpnext/healthcare/page/patient_progress/__init__.py b/erpnext/healthcare/page/patient_progress/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/healthcare/page/patient_progress/__init__.py
diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.css b/erpnext/healthcare/page/patient_progress/patient_progress.css
new file mode 100644
index 0000000..5d85a74
--- /dev/null
+++ b/erpnext/healthcare/page/patient_progress/patient_progress.css
@@ -0,0 +1,165 @@
+/* sidebar */
+
+.layout-side-section .frappe-control[data-fieldname='patient'] {
+  max-width: 300px;
+}
+
+.patient-image-container {
+  margin-top: 17px;
+}
+
+.patient-image {
+  display: inline-block;
+  width: 100%;
+  height: 0;
+  padding: 50% 0px;
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center center;
+  border-radius: 4px;
+}
+
+.patient-details {
+  margin: -5px 5px;
+}
+
+.important-links {
+  margin: 30px 5px;
+}
+
+.patient-name {
+  font-size: 20px;
+}
+
+/* heatmap */
+
+.heatmap-container {
+  height: 170px;
+}
+
+.patient-heatmap {
+  width: 80%;
+  display: inline-block;
+}
+
+.patient-heatmap .chart-container {
+  margin-left: 30px;
+}
+
+.patient-heatmap .frappe-chart {
+  margin-top: 5px;
+}
+
+.patient-heatmap .frappe-chart .chart-legend {
+  display: none;
+}
+
+.heatmap-container .chart-filter {
+  position: relative;
+  top: 5px;
+  margin-right: 10px;
+}
+
+/* percentage chart */
+
+.percentage-chart-container {
+  height: 130px;
+}
+
+.percentage-chart-container .chart-filter {
+  position: relative;
+  top: 5px;
+  margin-right: 10px;
+}
+
+.therapy-session-percentage-chart .frappe-chart {
+  position: absolute;
+  top: 5px;
+}
+
+/* line charts */
+
+.date-field .clearfix {
+  display: none;
+}
+
+.date-field .help-box {
+  display: none;
+}
+
+.date-field .frappe-control {
+  margin-bottom: 0px !important;
+}
+
+.date-field .form-group {
+  margin-bottom: 0px !important;
+}
+
+/* common */
+
+text.title {
+  text-transform: uppercase;
+  font-size: 11px;
+  margin-left: 20px;
+  margin-top: 20px;
+  display: block;
+}
+
+.chart-filter-search {
+  margin-left: 35px;
+  width: 25%;
+}
+
+.chart-column-container {
+  border-bottom: 1px solid #d1d8dd;
+  margin: 5px 0;
+}
+
+.line-chart-container .frappe-chart {
+  margin-top: -20px;
+}
+
+.line-chart-container {
+  margin-bottom: 20px;
+}
+
+.chart-control {
+  align-self: center;
+  display: flex;
+  flex-direction: row-reverse;
+  margin-top: -25px;
+}
+
+.chart-control > * {
+  margin-right: 10px;
+}
+
+/* mobile */
+
+@media (max-width: 991px) {
+  .patient-progress-sidebar {
+    display: flex;
+  }
+
+  .percentage-chart-container {
+    border-top: 1px solid #d1d8dd;
+  }
+
+  .percentage-chart-container .chart-filter {
+    position: relative;
+    top: 12px;
+    margin-right: 10px;
+  }
+
+  .patient-progress-sidebar .important-links {
+    margin: 0;
+  }
+
+  .patient-progress-sidebar .patient-details {
+    width: 50%;
+  }
+
+  .chart-filter-search {
+    width: 40%;
+  }
+}
diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.html b/erpnext/healthcare/page/patient_progress/patient_progress.html
new file mode 100644
index 0000000..c20537e
--- /dev/null
+++ b/erpnext/healthcare/page/patient_progress/patient_progress.html
@@ -0,0 +1,68 @@
+<div class="row patient-progress">
+	<div class="col-md-12">
+		<div class="progress-graphs">
+			<div class="chart-column-container heatmap-container hidden-xs hidden-sm">
+				<div class="patient-heatmap"></div>
+			</div>
+			<div class="chart-column-container percentage-chart-container">
+				<div class="therapy-session-percentage-chart"></div>
+			</div>
+
+			<div class="therapy-progress">
+				<div class="chart-head">
+					<text class="title" text-anchor="start">Therapy Progress</text>
+					<div class="chart-control pull-right"></div>
+				</div>
+				<div class="row">
+					<div class="chart-filter-search therapy-type-search"></div>
+				</div>
+				<div class="col-md-12 chart-column-container line-chart-container">
+					<div class="therapy-progress-line-chart">
+					</div>
+				</div>
+			</div>
+
+			<div class="assessment-results">
+				<div class="chart-head">
+					<text class="title" text-anchor="start">Assessment Results</text>
+					<div class="chart-control pull-right"></div>
+				</div>
+				<div class="row">
+					<div class="chart-filter-search assessment-template-search"></div>
+				</div>
+				<div class="col-md-12 chart-column-container line-chart-container">
+					<div class="assessment-results-line-chart">
+					</div>
+				</div>
+			</div>
+
+			<div class="therapy-assessment-correlation progress-line-chart">
+				<div class="chart-head">
+					<text class="title" text-anchor="start">Therapy Type and Assessment Correlation</text>
+					<div class="chart-control pull-right"></div>
+				</div>
+				<div class="row">
+					<div class="chart-filter-search assessment-correlation-template-search"></div>
+				</div>
+				<div class="col-md-12 chart-column-container line-chart-container">
+					<div class="therapy-assessment-correlation-chart">
+					</div>
+				</div>
+			</div>
+
+			<div class="assessment-parameter-progress progress-line-chart">
+				<div class="chart-head">
+					<text class="title" text-anchor="start">Assessment Parameter Wise Progress</text>
+					<div class="chart-control pull-right"></div>
+				</div>
+				<div class="row">
+					<div class="chart-filter-search assessment-parameter-search"></div>
+				</div>
+				<div class="col-md-12 line-chart-container">
+					<div class="assessment-parameter-progress-chart">
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
\ No newline at end of file
diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.js b/erpnext/healthcare/page/patient_progress/patient_progress.js
new file mode 100644
index 0000000..2410b0c
--- /dev/null
+++ b/erpnext/healthcare/page/patient_progress/patient_progress.js
@@ -0,0 +1,531 @@
+frappe.pages['patient-progress'].on_page_load = function(wrapper) {
+
+	frappe.ui.make_app_page({
+		parent: wrapper,
+		title: __('Patient Progress')
+	});
+
+	let patient_progress = new PatientProgress(wrapper);
+	$(wrapper).bind('show', ()=> {
+		patient_progress.show();
+	});
+};
+
+class PatientProgress {
+
+	constructor(wrapper) {
+		this.wrapper = $(wrapper);
+		this.page = wrapper.page;
+		this.sidebar = this.wrapper.find('.layout-side-section');
+		this.main_section = this.wrapper.find('.layout-main-section');
+	}
+
+	show() {
+		frappe.breadcrumbs.add('Healthcare');
+		this.sidebar.empty();
+
+		let me = this;
+		let patient = frappe.ui.form.make_control({
+			parent: me.sidebar,
+			df: {
+				fieldtype: 'Link',
+				options: 'Patient',
+				fieldname: 'patient',
+				placeholder: __('Select Patient'),
+				only_select: true,
+				change: () => {
+					me.patient_id = '';
+					if (me.patient_id != patient.get_value() && patient.get_value()) {
+						me.start = 0;
+						me.patient_id = patient.get_value();
+						me.make_patient_profile();
+					}
+				}
+			}
+		});
+		patient.refresh();
+
+		if (frappe.route_options && !this.patient) {
+			patient.set_value(frappe.route_options.patient);
+			this.patient_id = frappe.route_options.patient;
+		}
+
+		this.sidebar.find('[data-fieldname="patient"]').append('<div class="patient-info"></div>');
+	}
+
+	make_patient_profile() {
+		this.page.set_title(__('Patient Progress'));
+		this.main_section.empty().append(frappe.render_template('patient_progress'));
+		this.render_patient_details();
+		this.render_heatmap();
+		this.render_percentage_chart('therapy_type', 'Therapy Type Distribution');
+		this.create_percentage_chart_filters();
+		this.show_therapy_progress();
+		this.show_assessment_results();
+		this.show_therapy_assessment_correlation();
+		this.show_assessment_parameter_progress();
+	}
+
+	get_patient_info() {
+		return frappe.xcall('frappe.client.get', {
+			doctype: 'Patient',
+			name: this.patient_id
+		}).then((patient) => {
+			if (patient) {
+				this.patient = patient;
+			}
+		});
+	}
+
+	get_therapy_sessions_count() {
+		return frappe.xcall(
+			'erpnext.healthcare.page.patient_progress.patient_progress.get_therapy_sessions_count', {
+				patient: this.patient_id,
+			}
+		).then(data => {
+			if (data) {
+				this.total_therapy_sessions = data.total_therapy_sessions;
+				this.therapy_sessions_this_month = data.therapy_sessions_this_month;
+			}
+		});
+	}
+
+	render_patient_details() {
+		this.get_patient_info().then(() => {
+			this.get_therapy_sessions_count().then(() => {
+				$('.patient-info').empty().append(frappe.render_template('patient_progress_sidebar', {
+					patient_image: this.patient.image,
+					patient_name: this.patient.patient_name,
+					patient_gender: this.patient.sex,
+					patient_mobile: this.patient.mobile,
+					total_therapy_sessions: this.total_therapy_sessions,
+					therapy_sessions_this_month: this.therapy_sessions_this_month
+				}));
+
+				this.setup_patient_profile_links();
+			});
+		});
+	}
+
+	setup_patient_profile_links() {
+		this.wrapper.find('.patient-profile-link').on('click', () => {
+			frappe.set_route('Form', 'Patient', this.patient_id);
+		});
+
+		this.wrapper.find('.therapy-plan-link').on('click', () => {
+			frappe.route_options = {
+				'patient': this.patient_id,
+				'docstatus': 1
+			};
+			frappe.set_route('List', 'Therapy Plan');
+		});
+
+		this.wrapper.find('.patient-history').on('click', () => {
+			frappe.route_options = {
+				'patient': this.patient_id
+			};
+			frappe.set_route('patient_history');
+		});
+	}
+
+	render_heatmap() {
+		this.heatmap = new frappe.Chart('.patient-heatmap', {
+			type: 'heatmap',
+			countLabel: 'Interactions',
+			data: {},
+			discreteDomains: 0
+		});
+		this.update_heatmap_data();
+		this.create_heatmap_chart_filters();
+	}
+
+	update_heatmap_data(date_from) {
+		frappe.xcall('erpnext.healthcare.page.patient_progress.patient_progress.get_patient_heatmap_data', {
+			patient: this.patient_id,
+			date: date_from || frappe.datetime.year_start(),
+		}).then((data) => {
+			this.heatmap.update( {dataPoints: data} );
+		});
+	}
+
+	create_heatmap_chart_filters() {
+		this.get_patient_info().then(() => {
+			let filters = [
+				{
+					label: frappe.dashboard_utils.get_year(frappe.datetime.now_date()),
+					options: frappe.dashboard_utils.get_years_since_creation(this.patient.creation),
+					action: (selected_item) => {
+						this.update_heatmap_data(frappe.datetime.obj_to_str(selected_item));
+					}
+				},
+			];
+			frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.heatmap-container');
+		});
+	}
+
+	render_percentage_chart(field, title) {
+		frappe.xcall(
+			'erpnext.healthcare.page.patient_progress.patient_progress.get_therapy_sessions_distribution_data', {
+				patient: this.patient_id,
+				field: field
+			}
+		).then(chart => {
+			if (chart.labels.length) {
+				this.percentage_chart = new frappe.Chart('.therapy-session-percentage-chart', {
+					title: title,
+					type: 'percentage',
+					data: {
+						labels: chart.labels,
+						datasets: chart.datasets
+					},
+					truncateLegends: 1,
+					barOptions: {
+						height: 11,
+						depth: 1
+					},
+					height: 160,
+					maxSlices: 8,
+					colors: ['#5e64ff', '#743ee2', '#ff5858', '#ffa00a', '#feef72', '#28a745', '#98d85b', '#a9a7ac'],
+				});
+			} else {
+				this.wrapper.find('.percentage-chart-container').hide();
+			}
+		});
+	}
+
+	create_percentage_chart_filters() {
+		let filters = [
+			{
+				label: 'Therapy Type',
+				options: ['Therapy Type', 'Exercise Type'],
+				fieldnames: ['therapy_type', 'exercise_type'],
+				action: (selected_item, fieldname) => {
+					let title = selected_item + ' Distribution';
+					this.render_percentage_chart(fieldname, title);
+				}
+			},
+		];
+		frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.percentage-chart-container');
+	}
+
+	create_time_span_filters(action_method, parent) {
+		let chart_control = $(parent).find('.chart-control');
+		let filters = [
+			{
+				label: 'Last Month',
+				options: ['Select Date Range', 'Last Week', 'Last Month', 'Last Quarter', 'Last Year'],
+				action: (selected_item) => {
+					if (selected_item === 'Select Date Range') {
+						this.render_date_range_fields(action_method, chart_control);
+					} else {
+						// hide date range field if visible
+						let date_field = $(parent).find('.date-field');
+						if (date_field.is(':visible')) {
+							date_field.hide();
+						}
+						this[action_method](selected_item);
+					}
+				}
+			}
+		];
+		frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', chart_control, 1);
+	}
+
+	render_date_range_fields(action_method, parent) {
+		let date_field = $(parent).find('.date-field');
+
+		if (!date_field.length) {
+			let date_field_wrapper = $(
+				`<div class="date-field pull-right"></div>`
+			).appendTo(parent);
+
+			let date_range_field = frappe.ui.form.make_control({
+				df: {
+					fieldtype: 'DateRange',
+					fieldname: 'from_date',
+					placeholder: 'Date Range',
+					input_class: 'input-xs',
+					reqd: 1,
+					change: () => {
+						let selected_date_range = date_range_field.get_value();
+						if (selected_date_range && selected_date_range.length === 2) {
+							this[action_method](selected_date_range);
+						}
+					}
+				},
+				parent: date_field_wrapper,
+				render_input: 1
+			});
+		} else if (!date_field.is(':visible')) {
+			date_field.show();
+		}
+	}
+
+	show_therapy_progress() {
+		let me = this;
+		let therapy_type = frappe.ui.form.make_control({
+			parent: $('.therapy-type-search'),
+			df: {
+				fieldtype: 'Link',
+				options: 'Therapy Type',
+				fieldname: 'therapy_type',
+				placeholder: __('Select Therapy Type'),
+				only_select: true,
+				change: () => {
+					if (me.therapy_type != therapy_type.get_value() && therapy_type.get_value()) {
+						me.therapy_type = therapy_type.get_value();
+						me.render_therapy_progress_chart();
+					}
+				}
+			}
+		});
+		therapy_type.refresh();
+		this.create_time_span_filters('render_therapy_progress_chart', '.therapy-progress');
+	}
+
+	render_therapy_progress_chart(time_span='Last Month') {
+		if (!this.therapy_type) return;
+
+		frappe.xcall(
+			'erpnext.healthcare.page.patient_progress.patient_progress.get_therapy_progress_data', {
+				patient: this.patient_id,
+				therapy_type: this.therapy_type,
+				time_span: time_span
+			}
+		).then(chart => {
+			let data = {
+				labels: chart.labels,
+				datasets: chart.datasets
+			}
+			let parent = '.therapy-progress-line-chart';
+			if (!chart.labels.length) {
+				this.show_null_state(parent);
+			} else {
+				if (!this.therapy_line_chart) {
+					this.therapy_line_chart = new frappe.Chart(parent, {
+						type: 'axis-mixed',
+						height: 250,
+						data: data,
+						lineOptions: {
+							regionFill: 1
+						},
+						axisOptions: {
+							xIsSeries: 1
+						},
+					});
+				} else {
+					$(parent).find('.chart-container').show();
+					$(parent).find('.chart-empty-state').hide();
+					this.therapy_line_chart.update(data);
+				}
+			}
+		});
+	}
+
+	show_assessment_results() {
+		let me = this;
+		let assessment_template = frappe.ui.form.make_control({
+			parent: $('.assessment-template-search'),
+			df: {
+				fieldtype: 'Link',
+				options: 'Patient Assessment Template',
+				fieldname: 'assessment_template',
+				placeholder: __('Select Assessment Template'),
+				only_select: true,
+				change: () => {
+					if (me.assessment_template != assessment_template.get_value() && assessment_template.get_value()) {
+						me.assessment_template = assessment_template.get_value();
+						me.render_assessment_result_chart();
+					}
+				}
+			}
+		});
+		assessment_template.refresh();
+		this.create_time_span_filters('render_assessment_result_chart', '.assessment-results');
+	}
+
+	render_assessment_result_chart(time_span='Last Month') {
+		if (!this.assessment_template) return;
+
+		frappe.xcall(
+			'erpnext.healthcare.page.patient_progress.patient_progress.get_patient_assessment_data', {
+				patient: this.patient_id,
+				assessment_template: this.assessment_template,
+				time_span: time_span
+			}
+		).then(chart => {
+			let data = {
+				labels: chart.labels,
+				datasets: chart.datasets,
+				yMarkers: [
+					{ label: 'Max Score', value: chart.max_score }
+				],
+			}
+			let parent = '.assessment-results-line-chart';
+			if (!chart.labels.length) {
+				this.show_null_state(parent);
+			} else {
+				if (!this.assessment_line_chart) {
+					this.assessment_line_chart = new frappe.Chart(parent, {
+						type: 'axis-mixed',
+						height: 250,
+						data: data,
+						lineOptions: {
+							regionFill: 1
+						},
+						axisOptions: {
+							xIsSeries: 1
+						},
+						tooltipOptions: {
+							formatTooltipY: d => d + __(' out of ') + chart.max_score
+						}
+					});
+				} else {
+					$(parent).find('.chart-container').show();
+					$(parent).find('.chart-empty-state').hide();
+					this.assessment_line_chart.update(data);
+				}
+			}
+		});
+	}
+
+	show_therapy_assessment_correlation() {
+		let me = this;
+		let assessment = frappe.ui.form.make_control({
+			parent: $('.assessment-correlation-template-search'),
+			df: {
+				fieldtype: 'Link',
+				options: 'Patient Assessment Template',
+				fieldname: 'assessment',
+				placeholder: __('Select Assessment Template'),
+				only_select: true,
+				change: () => {
+					if (me.assessment != assessment.get_value() && assessment.get_value()) {
+						me.assessment = assessment.get_value();
+						me.render_therapy_assessment_correlation_chart();
+					}
+				}
+			}
+		});
+		assessment.refresh();
+		this.create_time_span_filters('render_therapy_assessment_correlation_chart', '.therapy-assessment-correlation');
+	}
+
+	render_therapy_assessment_correlation_chart(time_span='Last Month') {
+		if (!this.assessment) return;
+
+		frappe.xcall(
+			'erpnext.healthcare.page.patient_progress.patient_progress.get_therapy_assessment_correlation_data', {
+				patient: this.patient_id,
+				assessment_template: this.assessment,
+				time_span: time_span
+			}
+		).then(chart => {
+			let data = {
+				labels: chart.labels,
+				datasets: chart.datasets,
+				yMarkers: [
+					{ label: 'Max Score', value: chart.max_score }
+				],
+			}
+			let parent = '.therapy-assessment-correlation-chart';
+			if (!chart.labels.length) {
+				this.show_null_state(parent);
+			} else {
+				if (!this.correlation_chart) {
+					this.correlation_chart = new frappe.Chart(parent, {
+						type: 'axis-mixed',
+						height: 300,
+						data: data,
+						axisOptions: {
+							xIsSeries: 1
+						}
+					});
+				} else {
+					$(parent).find('.chart-container').show();
+					$(parent).find('.chart-empty-state').hide();
+					this.correlation_chart.update(data);
+				}
+			}
+		});
+	}
+
+	show_assessment_parameter_progress() {
+		let me = this;
+		let parameter = frappe.ui.form.make_control({
+			parent: $('.assessment-parameter-search'),
+			df: {
+				fieldtype: 'Link',
+				options: 'Patient Assessment Parameter',
+				fieldname: 'assessment',
+				placeholder: __('Select Assessment Parameter'),
+				only_select: true,
+				change: () => {
+					if (me.parameter != parameter.get_value() && parameter.get_value()) {
+						me.parameter = parameter.get_value();
+						me.render_assessment_parameter_progress_chart();
+					}
+				}
+			}
+		});
+		parameter.refresh();
+		this.create_time_span_filters('render_assessment_parameter_progress_chart', '.assessment-parameter-progress');
+	}
+
+	render_assessment_parameter_progress_chart(time_span='Last Month') {
+		if (!this.parameter) return;
+
+		frappe.xcall(
+			'erpnext.healthcare.page.patient_progress.patient_progress.get_assessment_parameter_data', {
+				patient: this.patient_id,
+				parameter: this.parameter,
+				time_span: time_span
+			}
+		).then(chart => {
+			let data = {
+				labels: chart.labels,
+				datasets: chart.datasets
+			}
+			let parent = '.assessment-parameter-progress-chart';
+			if (!chart.labels.length) {
+				this.show_null_state(parent);
+			} else {
+				if (!this.parameter_chart) {
+					this.parameter_chart = new frappe.Chart(parent, {
+						type: 'line',
+						height: 250,
+						data: data,
+						lineOptions: {
+							regionFill: 1
+						},
+						axisOptions: {
+							xIsSeries: 1
+						},
+						tooltipOptions: {
+							formatTooltipY: d => d + '%'
+						}
+					});
+				} else {
+					$(parent).find('.chart-container').show();
+					$(parent).find('.chart-empty-state').hide();
+					this.parameter_chart.update(data);
+				}
+			}
+		});
+	}
+
+	show_null_state(parent) {
+		let null_state = $(parent).find('.chart-empty-state');
+		if (null_state.length) {
+			$(null_state).show();
+		} else {
+			null_state = $(
+				`<div class="chart-empty-state text-muted text-center" style="margin-bottom: 20px;">${__(
+					"No Data..."
+				)}</div>`
+			);
+			$(parent).append(null_state);
+		}
+		$(parent).find('.chart-container').hide();
+	}
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.json b/erpnext/healthcare/page/patient_progress/patient_progress.json
new file mode 100644
index 0000000..0175cb9
--- /dev/null
+++ b/erpnext/healthcare/page/patient_progress/patient_progress.json
@@ -0,0 +1,33 @@
+{
+ "content": null,
+ "creation": "2020-06-12 15:46:23.111928",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2020-07-23 21:45:45.540055",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "patient-progress",
+ "owner": "Administrator",
+ "page_name": "patient-progress",
+ "restrict_to_domain": "Healthcare",
+ "roles": [
+  {
+   "role": "Healthcare Administrator"
+  },
+  {
+   "role": "Physician"
+  },
+  {
+   "role": "Patient"
+  },
+  {
+   "role": "System Manager"
+  }
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0,
+ "title": "Patient Progress"
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.py b/erpnext/healthcare/page/patient_progress/patient_progress.py
new file mode 100644
index 0000000..a04fb2b
--- /dev/null
+++ b/erpnext/healthcare/page/patient_progress/patient_progress.py
@@ -0,0 +1,197 @@
+import frappe
+from datetime import datetime
+from frappe import _
+from frappe.utils import getdate, get_timespan_date_range
+import json
+
+@frappe.whitelist()
+def get_therapy_sessions_count(patient):
+	total = frappe.db.count('Therapy Session', filters={
+		'docstatus': 1,
+		'patient': patient
+	})
+
+	month_start = datetime.today().replace(day=1)
+	this_month = frappe.db.count('Therapy Session', filters={
+		'creation': ['>', month_start],
+		'docstatus': 1,
+		'patient': patient
+	})
+
+	return {
+		'total_therapy_sessions': total,
+		'therapy_sessions_this_month': this_month
+	}
+
+
+@frappe.whitelist()
+def get_patient_heatmap_data(patient, date):
+	return dict(frappe.db.sql("""
+		SELECT
+			unix_timestamp(communication_date), count(*)
+		FROM
+			`tabPatient Medical Record`
+		WHERE
+			communication_date > subdate(%(date)s, interval 1 year) and
+			communication_date < subdate(%(date)s, interval -1 year) and
+			patient = %(patient)s
+		GROUP BY communication_date
+		ORDER BY communication_date asc""", {'date': date, 'patient': patient}))
+
+
+@frappe.whitelist()
+def get_therapy_sessions_distribution_data(patient, field):
+	if field == 'therapy_type':
+		result = frappe.db.get_all('Therapy Session',
+			filters = {'patient': patient, 'docstatus': 1},
+			group_by = field,
+			order_by = field,
+			fields = [field, 'count(*)'],
+			as_list = True)
+
+	elif field == 'exercise_type':
+		data = frappe.db.get_all('Therapy Session',  filters={
+			'docstatus': 1,
+			'patient': patient
+		}, as_list=True)
+		therapy_sessions = [entry[0] for entry in data]
+
+		result = frappe.db.get_all('Exercise',
+			filters = {
+				'parenttype': 'Therapy Session',
+				'parent': ['in', therapy_sessions],
+				'docstatus': 1
+			},
+			group_by = field,
+			order_by = field,
+			fields = [field, 'count(*)'],
+			as_list = True)
+
+	return {
+		'labels': [r[0] for r in result if r[0] != None],
+		'datasets': [{
+			'values': [r[1] for r in result]
+		}]
+	}
+
+
+@frappe.whitelist()
+def get_therapy_progress_data(patient, therapy_type, time_span):
+	date_range = get_date_range(time_span)
+	query_values = {'from_date': date_range[0], 'to_date': date_range[1], 'therapy_type': therapy_type, 'patient': patient}
+	result = frappe.db.sql("""
+		SELECT
+			start_date, total_counts_targeted, total_counts_completed
+		FROM
+			`tabTherapy Session`
+		WHERE
+			start_date BETWEEN %(from_date)s AND %(to_date)s and
+			docstatus = 1 and
+			therapy_type = %(therapy_type)s and
+			patient = %(patient)s
+		ORDER BY start_date""", query_values, as_list=1)
+
+	return {
+		'labels': [r[0] for r in result if r[0] != None],
+		'datasets': [
+			{ 'name': _('Targetted'), 'values': [r[1] for r in result if r[0] != None] },
+			{ 'name': _('Completed'), 'values': [r[2] for r in result if r[0] != None] }
+		]
+	}
+
+@frappe.whitelist()
+def get_patient_assessment_data(patient, assessment_template, time_span):
+	date_range = get_date_range(time_span)
+	query_values = {'from_date': date_range[0], 'to_date': date_range[1], 'assessment_template': assessment_template, 'patient': patient}
+	result = frappe.db.sql("""
+		SELECT
+			assessment_datetime, total_score, total_score_obtained
+		FROM
+			`tabPatient Assessment`
+		WHERE
+			DATE(assessment_datetime) BETWEEN %(from_date)s AND %(to_date)s and
+			docstatus = 1 and
+			assessment_template = %(assessment_template)s and
+			patient = %(patient)s
+		ORDER BY assessment_datetime""", query_values, as_list=1)
+
+	return {
+		'labels': [getdate(r[0]) for r in result if r[0] != None],
+		'datasets': [
+			{ 'name': _('Score Obtained'), 'values': [r[2] for r in result if r[0] != None] }
+		],
+		'max_score': result[0][1] if result else None
+	}
+
+@frappe.whitelist()
+def get_therapy_assessment_correlation_data(patient, assessment_template, time_span):
+	date_range = get_date_range(time_span)
+	query_values = {'from_date': date_range[0], 'to_date': date_range[1], 'assessment': assessment_template, 'patient': patient}
+	result = frappe.db.sql("""
+		SELECT
+			therapy.therapy_type, count(*), avg(assessment.total_score_obtained), total_score
+		FROM
+			`tabPatient Assessment` assessment INNER JOIN `tabTherapy Session` therapy
+		ON
+			assessment.therapy_session = therapy.name
+		WHERE
+			DATE(assessment.assessment_datetime) BETWEEN %(from_date)s AND %(to_date)s and
+			assessment.docstatus = 1 and
+			assessment.patient = %(patient)s and
+			assessment.assessment_template = %(assessment)s
+		GROUP BY therapy.therapy_type
+	""", query_values, as_list=1)
+
+	return {
+		'labels': [r[0] for r in result if r[0] != None],
+		'datasets': [
+			{ 'name': _('Sessions'), 'chartType': 'bar', 'values': [r[1] for r in result if r[0] != None] },
+			{ 'name': _('Average Score'), 'chartType': 'line', 'values': [round(r[2], 2) for r in result if r[0] != None] }
+		],
+		'max_score': result[0][1] if result else None
+	}
+
+@frappe.whitelist()
+def get_assessment_parameter_data(patient, parameter, time_span):
+	date_range = get_date_range(time_span)
+	query_values = {'from_date': date_range[0], 'to_date': date_range[1], 'parameter': parameter, 'patient': patient}
+	results = frappe.db.sql("""
+		SELECT
+			assessment.assessment_datetime,
+			sheet.score,
+			template.scale_max
+		FROM
+			`tabPatient Assessment Sheet` sheet
+		INNER JOIN `tabPatient Assessment` assessment
+			ON sheet.parent = assessment.name
+		INNER JOIN `tabPatient Assessment Template` template
+			ON template.name = assessment.assessment_template
+		WHERE
+			DATE(assessment.assessment_datetime) BETWEEN %(from_date)s AND %(to_date)s and
+			assessment.docstatus = 1 and
+			sheet.parameter = %(parameter)s and
+			assessment.patient = %(patient)s
+		ORDER BY
+			assessment.assessment_datetime asc
+	""", query_values, as_list=1)
+
+	score_percentages = []
+	for r in results:
+		if r[2] != 0 and r[0] != None:
+			score = round((int(r[1]) / int(r[2])) * 100, 2)
+			score_percentages.append(score)
+
+	return {
+		'labels': [getdate(r[0]) for r in results if r[0] != None],
+		'datasets': [
+			{ 'name': _('Score'), 'values': score_percentages }
+		]
+	}
+
+def get_date_range(time_span):
+	try:
+		time_span = json.loads(time_span)
+		return time_span
+	except json.decoder.JSONDecodeError:
+		return get_timespan_date_range(time_span.lower())
+
diff --git a/erpnext/healthcare/page/patient_progress/patient_progress_sidebar.html b/erpnext/healthcare/page/patient_progress/patient_progress_sidebar.html
new file mode 100644
index 0000000..cd62dd3
--- /dev/null
+++ b/erpnext/healthcare/page/patient_progress/patient_progress_sidebar.html
@@ -0,0 +1,29 @@
+<div class="patient-progress-sidebar">
+	<div class="patient-image-container">
+		{% if patient_image %}
+			<div class="patient-image" src={{patient_image}} style="background-image: url(\'{%= patient_image %}\')"></div>
+		{% endif %}
+	</div>
+	<div class="patient-details">
+		{% if patient_name %}
+		<p class="patient-name bold">{{patient_name}}</p>
+		{% endif %}
+		{% if patient_gender %}
+		<p class="patient-gender text-muted">{%=__("Gender: ") %} {{patient_gender}}</p>
+		{% endif %}
+		{% if patient_mobile %}
+		<p class="patient-mobile text-muted">{%=__("Contact: ") %} {{patient_mobile}}</p>
+		{% endif %}
+		{% if total_therapy_sessions %}
+		<p class="patient-sessions text-muted">{%=__("Total Therapy Sessions: ") %} {{total_therapy_sessions}}</p>
+		{% endif %}
+		{% if therapy_sessions_this_month %}
+		<p class="patient-sessions text-muted">{%=__("Monthly Therapy Sessions: ") %} {{therapy_sessions_this_month}}</p>
+		{% endif %}
+	</div>
+	<div class="important-links">
+		<p><a class="patient-profile-link">{%=__("Patient Profile") %}</a></p>
+		<p><a class="therapy-plan-link">{%=__("Therapy Plan") %}</a></p>
+		<p><a class="patient-history">{%=__("Patient History") %}</a></p>
+	</div>
+</div>
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 2fb9d7f..a24f5f7 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -713,6 +713,7 @@
 erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
 erpnext.patches.v12_0.add_taxjar_integration_field
+erpnext.patches.v12_0.fix_percent_complete_for_projects
 erpnext.patches.v13_0.delete_report_requested_items_to_order
 erpnext.patches.v12_0.update_item_tax_template_company
 erpnext.patches.v13_0.move_branch_code_to_bank_account
diff --git a/erpnext/patches/v12_0/fix_percent_complete_for_projects.py b/erpnext/patches/v12_0/fix_percent_complete_for_projects.py
new file mode 100644
index 0000000..3622df6
--- /dev/null
+++ b/erpnext/patches/v12_0/fix_percent_complete_for_projects.py
@@ -0,0 +1,14 @@
+import frappe
+from frappe.utils import flt
+
+def execute():
+	for project in frappe.get_all("Project", fields=["name", "percent_complete_method"]):
+		total = frappe.db.count('Task', dict(project=project.name))
+		if project.percent_complete_method == "Task Completion" and total > 0:
+			completed = frappe.db.sql("""select count(name) from tabTask where
+					project=%s and status in ('Cancelled', 'Completed')""", project.name)[0][0]
+			percent_complete = flt(flt(completed) / total * 100, 2)
+			if project.percent_complete != percent_complete:
+				frappe.db.set_value("Project", project.name, "percent_complete", percent_complete)
+				if percent_complete == 100:
+					frappe.db.set_value("Project", project.name, "status", "Completed")
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 1e2983e..4ccf564 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -869,10 +869,10 @@
 
 		# other taxes and charges on income tax
 		for d in tax_slab.other_taxes_and_charges:
-			if flt(d.min_taxable_income) and flt(d.min_taxable_income) > tax_amount:
+			if flt(d.min_taxable_income) and flt(d.min_taxable_income) > annual_taxable_earning:
 				continue
 
-			if flt(d.max_taxable_income) and flt(d.max_taxable_income) < tax_amount:
+			if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning:
 				continue
 
 			tax_amount += tax_amount * flt(d.percent) / 100
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 4bdda68..cf2fd26 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -175,6 +175,9 @@
 
 		self.update_nsm_model()
 
+	def after_delete(self):
+		self.update_project()
+
 	def update_status(self):
 		if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
 			from datetime import datetime
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 42f9cabc..d9f6e1d 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -338,8 +338,8 @@
 							};
 						},
 						change: function () {
-							let val = this.get_value();
-							if (val.length === 0) {
+							const batch_no = this.get_value();
+							if (!batch_no) {
 								this.grid_row.on_grid_fields_dict
 									.available_qty.set_value(0);
 								return;
@@ -359,14 +359,11 @@
 								return;
 							}
 
-							let batch_number = me.item.batch_no ||
-								this.grid_row.on_grid_fields_dict.batch_no.get_value();
-
 							if (me.warehouse_details.name) {
 								frappe.call({
 									method: 'erpnext.stock.doctype.batch.batch.get_batch_qty',
 									args: {
-										batch_no: batch_number,
+										batch_no,
 										warehouse: me.warehouse_details.name,
 										item_code: me.item_code
 									},
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.js b/erpnext/stock/report/stock_ageing/stock_ageing.js
index ccde61a..8495142 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.js
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.js
@@ -37,6 +37,27 @@
 			"options": "Brand"
 		},
 		{
+			"fieldname":"range1",
+			"label": __("Ageing Range 1"),
+			"fieldtype": "Int",
+			"default": "30",
+			"reqd": 1
+		},
+		{
+			"fieldname":"range2",
+			"label": __("Ageing Range 2"),
+			"fieldtype": "Int",
+			"default": "60",
+			"reqd": 1
+		},
+		{
+			"fieldname":"range3",
+			"label": __("Ageing Range 3"),
+			"fieldtype": "Int",
+			"default": "90",
+			"reqd": 1
+		},
+		{
 			"fieldname":"show_warehouse_wise_stock",
 			"label": __("Show Warehouse-wise Stock"),
 			"fieldtype": "Check",
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index d5878cb..4af3c54 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -4,12 +4,11 @@
 from __future__ import unicode_literals
 import frappe
 from frappe import _
-from frappe.utils import date_diff, flt
+from frappe.utils import date_diff, flt, cint
 from six import iteritems
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 
 def execute(filters=None):
-
 	columns = get_columns(filters)
 	item_details = get_fifo_queue(filters)
 	to_date = filters["to_date"]
@@ -25,6 +24,7 @@
 		average_age = get_average_age(fifo_queue, to_date)
 		earliest_age = date_diff(to_date, fifo_queue[0][1])
 		latest_age = date_diff(to_date, fifo_queue[-1][1])
+		range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date)
 
 		row = [details.name, details.item_name,
 			details.description, details.item_group, details.brand]
@@ -33,6 +33,7 @@
 			row.append(details.warehouse)
 
 		row.extend([item_dict.get("total_qty"), average_age,
+			range1, range2, range3, above_range3,
 			earliest_age, latest_age, details.stock_uom])
 
 		data.append(row)
@@ -55,7 +56,25 @@
 
 	return flt(age_qty / total_qty, 2) if total_qty else 0.0
 
+def get_range_age(filters, fifo_queue, to_date):
+	range1 = range2 = range3 = above_range3 = 0.0
+	for item in fifo_queue:
+		age = date_diff(to_date, item[1])
+		
+		if age <= filters.range1:
+			range1 += flt(item[0])
+		elif age <= filters.range2:
+			range2 += flt(item[0])
+		elif age <= filters.range3:
+			range3 += flt(item[0])
+		else:
+			above_range3 += flt(item[0])
+		
+	return range1, range2, range3, above_range3
+
 def get_columns(filters):
+	range_columns = []
+	setup_ageing_columns(filters, range_columns)
 	columns = [
 		{
 			"label": _("Item Code"),
@@ -112,7 +131,9 @@
 			"fieldname": "average_age",
 			"fieldtype": "Float",
 			"width": 100
-		},
+		}])
+	columns.extend(range_columns)
+	columns.extend([
 		{
 			"label": _("Earliest"),
 			"fieldname": "earliest",
@@ -263,3 +284,18 @@
 		},
 		"type" : "bar"
 	}
+
+def setup_ageing_columns(filters, range_columns):
+	for i, label in enumerate(["0-{range1}".format(range1=filters["range1"]),
+		"{range1}-{range2}".format(range1=cint(filters["range1"])+ 1, range2=filters["range2"]),
+		"{range2}-{range3}".format(range2=cint(filters["range2"])+ 1, range3=filters["range3"]),
+		"{range3}-{above}".format(range3=cint(filters["range3"])+ 1, above=_("Above"))]):
+			add_column(range_columns, label="Age ("+ label +")", fieldname='range' + str(i+1))
+
+def add_column(range_columns, label, fieldname, fieldtype='Float', width=140):
+	range_columns.append(dict(
+		label=label,
+		fieldname=fieldname,
+		fieldtype=fieldtype,
+		width=width
+	))
\ No newline at end of file