feat: Patient Appointment Analytics Script Report
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index be43184..51db3e9 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -228,7 +228,6 @@
    "default": "0",
    "fieldname": "invoiced",
    "fieldtype": "Check",
-   "in_list_view": 1,
    "label": "Invoiced",
    "read_only": 1
   },
@@ -286,7 +285,7 @@
   }
  ],
  "links": [],
- "modified": "2020-02-25 17:57:56.971064",
+ "modified": "2020-03-02 14:35:54.040428",
  "modified_by": "Administrator",
  "module": "Healthcare",
  "name": "Patient Appointment",
diff --git a/erpnext/healthcare/report/patient_appointment_analytics/__init__.py b/erpnext/healthcare/report/patient_appointment_analytics/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/healthcare/report/patient_appointment_analytics/__init__.py
diff --git a/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.js b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.js
new file mode 100644
index 0000000..6494ef2
--- /dev/null
+++ b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.js
@@ -0,0 +1,73 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports['Patient Appointment Analytics'] = {
+	"filters": [
+		{
+			fieldname: 'tree_type',
+			label: __('Tree Type'),
+			fieldtype: 'Select',
+			options: ['Healthcare Practitioner', 'Medical Department'],
+			default: 'Healthcare Practitioner',
+			reqd: 1
+		},
+		{
+			fieldname: 'status',
+			label: __('Appointment Status'),
+			fieldtype: 'Select',
+			options:[
+				{label: __('Scheduled'), value: 'Scheduled'},
+				{label: __('Open'), value: 'Open'},
+				{label: __('Closed'), value: 'Closed'},
+				{label: __('Expired'), value: 'Expired'},
+				{label: __('Cancelled'), value: 'Cancelled'}
+			]
+		},
+		{
+			fieldname: 'appointment_type',
+			label: __('Appointment Type'),
+			fieldtype: 'Link',
+			options: 'Appointment Type'
+		},
+		{
+			fieldname: 'practitioner',
+			label: __('Healthcare Practitioner'),
+			fieldtype: 'Link',
+			options: 'Healthcare Practitioner'
+		},
+		{
+			fieldname: 'department',
+			label: __('Medical Department'),
+			fieldtype: 'Link',
+			options: 'Medical Department'
+		},
+		{
+			fieldname: 'from_date',
+			label: __('From Date'),
+			fieldtype: 'Date',
+			default: frappe.defaults.get_user_default('year_start_date'),
+			reqd: 1
+		},
+		{
+			fieldname: 'to_date',
+			label: __('To Date'),
+			fieldtype: 'Date',
+			default: frappe.defaults.get_user_default('year_end_date'),
+			reqd: 1
+		},
+		{
+			fieldname: 'range',
+			label: __('Range'),
+			fieldtype: 'Select',
+			options:[
+				{label: __('Weekly'), value: 'Weekly'},
+				{label: __('Monthly'), value: 'Monthly'},
+				{label: __('Quarterly'), value: 'Quarterly'},
+				{label: __('Yearly'), value: 'Yearly'}
+			],
+			default: 'Monthly',
+			reqd: 1
+		}
+	]
+};
diff --git a/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.json b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.json
new file mode 100644
index 0000000..64750c0
--- /dev/null
+++ b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.json
@@ -0,0 +1,36 @@
+{
+ "add_total_row": 1,
+ "creation": "2020-03-02 15:13:16.273493",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-03-02 15:13:16.273493",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient Appointment Analytics",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Patient Appointment",
+ "report_name": "Patient Appointment Analytics",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Healthcare Administrator"
+  },
+  {
+   "role": "LabTest Approver"
+  },
+  {
+   "role": "Physician"
+  },
+  {
+   "role": "Nursing User"
+  },
+  {
+   "role": "Laboratory User"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py
new file mode 100644
index 0000000..627c388
--- /dev/null
+++ b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import getdate, flt, add_to_date, add_days
+from frappe import _ , scrub
+from six import iteritems
+from erpnext.accounts.utils import get_fiscal_year
+
+def execute(filters=None):
+	return Analytics(filters).run()
+
+class Analytics(object):
+	def __init__(self, filters=None):
+		self.filters = frappe._dict(filters or {})
+		self.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+		self.get_period_date_ranges()
+
+	def run(self):
+		self.get_columns()
+		self.get_data()
+		self.get_chart_data()
+
+		self.chart = ''
+		return self.columns, self.data, None, self.chart
+
+	def get_period_date_ranges(self):
+		from dateutil.relativedelta import relativedelta, MO
+		from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date)
+
+		increment = {
+			'Monthly': 1,
+			'Quarterly': 3,
+			'Half-Yearly': 6,
+			'Yearly': 12
+		}.get(self.filters.range, 1)
+
+		if self.filters.range in ['Monthly', 'Quarterly']:
+			from_date = from_date.replace(day=1)
+		elif self.filters.range == 'Yearly':
+			from_date = get_fiscal_year(from_date)[1]
+		else:
+			from_date = from_date + relativedelta(from_date, weekday=MO(-1))
+
+		self.periodic_daterange = []
+		for dummy in range(1, 53):
+			if self.filters.range == 'Weekly':
+				period_end_date = add_days(from_date, 6)
+			else:
+				period_end_date = add_to_date(from_date, months=increment, days=-1)
+
+			if period_end_date > to_date:
+				period_end_date = to_date
+
+			self.periodic_daterange.append(period_end_date)
+
+			from_date = add_days(period_end_date, 1)
+			if period_end_date == to_date:
+				break
+
+	def get_columns(self):
+		self.columns = []
+
+		if self.filters.tree_type == 'Healthcare Practitioner':
+			self.columns.append({
+				'label': _('Healthcare Practitioner'),
+				'options': 'Healthcare Practitioner',
+				'fieldname': 'practitioner',
+				'fieldtype': 'Link',
+				'width': 200
+			})
+
+		elif self.filters.tree_type == 'Medical Department':
+			self.columns.append({
+				'label': _('Medical Department'),
+				'fieldname': 'department',
+				'fieldtype': 'Link',
+				'options': 'Medical Department',
+				'width': 150
+			})
+
+		for end_date in self.periodic_daterange:
+			period = self.get_period(end_date)
+			self.columns.append({
+				'label': _(period),
+				'fieldname': scrub(period),
+				'fieldtype': 'Float',
+				'width': 120
+			})
+
+		self.columns.append({
+			'label': _('Total'),
+			'fieldname': 'total',
+			'fieldtype': 'Float',
+			'width': 120
+		})
+
+	def get_data(self):
+		if self.filters.tree_type == 'Healthcare Practitioner':
+			self.get_appointments_based_on_healthcare_practitioner()
+			self.get_rows()
+
+		elif self.filters.tree_type == 'Medical Department':
+			self.get_appointments_based_on_medical_department()
+			self.get_rows()
+
+	def get_chart_data(self):
+		pass
+
+	def get_period(self, appointment_date):
+		if self.filters.range == 'Weekly':
+			period = 'Week ' + str(appointment_date.isocalendar()[1]) + ' ' + str(appointment_date.year)
+		elif self.filters.range == 'Monthly':
+			period = str(self.months[appointment_date.month - 1]) + ' ' + str(appointment_date.year)
+		elif self.filters.range == 'Quarterly':
+			period = 'Quarter ' + str(((appointment_date.month - 1) // 3) + 1) + ' ' + str(appointment_date.year)
+		else:
+			year = get_fiscal_year(appointment_date, company=self.filters.company)
+			period = str(year[0])
+
+		return period
+
+	def get_appointments_based_on_healthcare_practitioner(self):
+		filters = self.get_common_filters()
+
+		self.entries = frappe.db.get_all('Patient Appointment',
+			fields=['appointment_date', 'name', 'patient', 'practitioner'],
+			filters=filters
+		)
+
+	def get_appointments_based_on_medical_department(self):
+		filters = self.get_common_filters()
+		if not filters.get('department'):
+			filters['department'] = ('!=', '')
+
+		self.entries = frappe.db.get_all('Patient Appointment',
+			fields=['appointment_date', 'name', 'patient', 'practitioner', 'department'],
+			filters=filters
+		)
+
+	def get_common_filters(self):
+		filters = {}
+		filters['appointment_date'] = ('between', [self.filters.from_date, self.filters.to_date])
+		for entry in ['appointment_type', 'practitioner', 'department', 'status']:
+			if self.filters.get(entry):
+				filters[entry] = self.filters.get(entry)
+
+		return filters
+
+	def get_rows(self):
+		self.data = []
+		self.get_periodic_data()
+
+		for entity, period_data in iteritems(self.appointment_periodic_data):
+			if self.filters.tree_type == 'Healthcare Practitioner':
+				row = {'practitioner': entity}
+			elif self.filters.tree_type == 'Medical Department':
+				row = {'department': entity}
+
+			total = 0
+			for end_date in self.periodic_daterange:
+				period = self.get_period(end_date)
+				amount = flt(period_data.get(period, 0.0))
+				row[scrub(period)] = amount
+				total += amount
+
+			row['total'] = total
+
+			self.data.append(row)
+
+	def get_periodic_data(self):
+		self.appointment_periodic_data = frappe._dict()
+
+		for d in self.entries:
+			period = self.get_period(d.get('appointment_date'))
+			if self.filters.tree_type == 'Healthcare Practitioner':
+				self.appointment_periodic_data.setdefault(d.practitioner, frappe._dict()).setdefault(period, 0.0)
+				self.appointment_periodic_data[d.practitioner][period] += 1
+
+			elif self.filters.tree_type == 'Medical Department':
+				self.appointment_periodic_data.setdefault(d.department, frappe._dict()).setdefault(period, 0.0)
+				self.appointment_periodic_data[d.department][period] += 1
\ No newline at end of file