Merge branch 'develop' into separate-discount-account
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 8cc5826..cdb6849 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -129,6 +129,9 @@
     needs: test
     runs-on: ubuntu-latest
     steps:
+      - name: Clone
+        uses: actions/checkout@v2
+
       - name: Download artifacts
         uses: actions/download-artifact@v3
 
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 907b769..b596df9 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -350,9 +350,13 @@
 			)
 
 			if self.minimum_invoice_amount:
-				condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount))
+				condition += " and {dr_or_cr} >= {amount}".format(
+					dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
+				)
 			if self.maximum_invoice_amount:
-				condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount))
+				condition += " and {dr_or_cr} <= {amount}".format(
+					dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
+				)
 
 		elif get_return_invoices:
 			condition = " and doc.company = '{0}' ".format(self.company)
@@ -367,15 +371,19 @@
 				else ""
 			)
 			dr_or_cr = (
-				"gl.debit_in_account_currency"
+				"debit_in_account_currency"
 				if erpnext.get_party_account_type(self.party_type) == "Receivable"
-				else "gl.credit_in_account_currency"
+				else "credit_in_account_currency"
 			)
 
 			if self.minimum_invoice_amount:
-				condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount))
+				condition += " and gl.{dr_or_cr} >= {amount}".format(
+					dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
+				)
 			if self.maximum_invoice_amount:
-				condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount))
+				condition += " and gl.{dr_or_cr} <= {amount}".format(
+					dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
+				)
 
 		else:
 			condition += (
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index ee29d2a..42917f8 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -30,6 +30,9 @@
 	onload() {
 		super.onload();
 
+		// Ignore linked advances
+		this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
+
 		if(!this.frm.doc.__islocal) {
 			// show credit_to in print format
 			if(!this.frm.doc.supplier && this.frm.doc.credit_to) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index a222dd9..c06809c 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -811,7 +811,9 @@
 
 					if provisional_accounting_for_non_stock_items:
 						if item.purchase_receipt:
-							provisional_account = self.get_company_default("default_provisional_account")
+							provisional_account = frappe.db.get_value(
+								"Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
+							) or self.get_company_default("default_provisional_account")
 							purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
 
 							if not purchase_receipt_doc:
@@ -834,7 +836,7 @@
 							if expense_booked_in_pr:
 								# Intentionally passing purchase invoice item to handle partial billing
 								purchase_receipt_doc.add_provisional_gl_entry(
-									item, gl_entries, self.posting_date, reverse=1
+									item, gl_entries, self.posting_date, provisional_account, reverse=1
 								)
 
 					if not self.is_internal_transfer():
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 73390dd..59bd637 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1482,7 +1482,8 @@
 		self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
 
 	def test_provisional_accounting_entry(self):
-		item = create_item("_Test Non Stock Item", is_stock_item=0)
+		create_item("_Test Non Stock Item", is_stock_item=0)
+
 		provisional_account = create_account(
 			account_name="Provision Account",
 			parent_account="Current Liabilities - _TC",
@@ -1505,6 +1506,8 @@
 		pi.save()
 		pi.submit()
 
+		self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC")
+
 		# Check GLE for Purchase Invoice
 		expected_gle = [
 			["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 6818955..0f7c13f 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -33,7 +33,9 @@
 		var me = this;
 		super.onload();
 
-		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
+		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
+			'POS Closing Entry', 'Journal Entry', 'Payment Entry'];
+
 		if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
 			// show debit_to in print format
 			this.frm.set_df_property("debit_to", "print_hide", 0);
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 7781fe3..caa70d0 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -3124,6 +3124,62 @@
 		si.reload()
 		self.assertTrue(si.items[0].serial_no)
 
+	def test_gain_loss_with_advance_entry(self):
+		from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+
+		unlink_enabled = frappe.db.get_value(
+			"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
+		)
+
+		frappe.db.set_value(
+			"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
+		)
+
+		jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
+
+		jv.accounts[0].exchange_rate = 70
+		jv.accounts[0].credit_in_account_currency = 100
+		jv.accounts[0].party_type = "Customer"
+		jv.accounts[0].party = "_Test Customer USD"
+
+		jv.save()
+		jv.submit()
+
+		si = create_sales_invoice(
+			customer="_Test Customer USD",
+			debit_to="_Test Receivable USD - _TC",
+			currency="USD",
+			conversion_rate=75,
+			do_not_save=1,
+			rate=100,
+		)
+
+		si.append(
+			"advances",
+			{
+				"reference_type": "Journal Entry",
+				"reference_name": jv.name,
+				"reference_row": jv.accounts[0].name,
+				"advance_amount": 100,
+				"allocated_amount": 100,
+				"ref_exchange_rate": 70,
+			},
+		)
+		si.save()
+		si.submit()
+
+		expected_gle = [
+			["_Test Receivable USD - _TC", 7500.0, 500],
+			["Exchange Gain/Loss - _TC", 500.0, 0.0],
+			["Sales - _TC", 0.0, 7500.0],
+		]
+
+		check_gl_entries(self, si.name, expected_gle, nowdate())
+
+		frappe.db.set_value(
+			"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
+		)
+
 
 def get_sales_invoice_for_e_invoice():
 	si = make_sales_invoice_for_ewaybill()
diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
index dcdba09..064b806 100644
--- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
@@ -65,7 +65,6 @@
 		)
 		sq.submit()
 
-		frappe.form_dict = frappe.local("form_dict")
 		frappe.form_dict.name = rfq.name
 
 		self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 5ebbb7a..8558cea 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1997,12 +1997,13 @@
 
 	reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else ""
 
+	# nosemgrep
 	journal_entries = frappe.db.sql(
 		"""
 		select
 			"Journal Entry" as reference_type, t1.name as reference_name,
 			t1.remark as remarks, t2.{0} as amount, t2.name as reference_row,
-			t2.reference_name as against_order
+			t2.reference_name as against_order, t2.exchange_rate
 		from
 			`tabJournal Entry` t1, `tabJournal Entry Account` t2
 		where
diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py
index 1f5df67..522de9e 100644
--- a/erpnext/erpnext_integrations/exotel_integration.py
+++ b/erpnext/erpnext_integrations/exotel_integration.py
@@ -37,11 +37,26 @@
 
 @frappe.whitelist(allow_guest=True)
 def handle_missed_call(**kwargs):
-	update_call_log(kwargs, "Missed")
+	status = ""
+	call_type = kwargs.get("CallType")
+	dial_call_status = kwargs.get("DialCallStatus")
+
+	if call_type == "incomplete" and dial_call_status == "no-answer":
+		status = "No Answer"
+	elif call_type == "client-hangup" and dial_call_status == "canceled":
+		status = "Canceled"
+	elif call_type == "incomplete" and dial_call_status == "failed":
+		status = "Failed"
+
+	update_call_log(kwargs, status)
 
 
 def update_call_log(call_payload, status="Ringing", call_log=None):
 	call_log = call_log or get_call_log(call_payload)
+
+	# for a new sid, call_log and get_call_log will be empty so create a new log
+	if not call_log:
+		call_log = create_call_log(call_payload)
 	if call_log:
 		call_log.status = status
 		call_log.to = call_payload.get("DialWhomNumber")
@@ -53,16 +68,9 @@
 
 
 def get_call_log(call_payload):
-	call_log = frappe.get_all(
-		"Call Log",
-		{
-			"id": call_payload.get("CallSid"),
-		},
-		limit=1,
-	)
-
-	if call_log:
-		return frappe.get_doc("Call Log", call_log[0].name)
+	call_log_id = call_payload.get("CallSid")
+	if frappe.db.exists("Call Log", call_log_id):
+		return frappe.get_doc("Call Log", call_log_id)
 
 
 def create_call_log(call_payload):
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index d592a9c..8a12f3b 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -4,7 +4,7 @@
  "allow_import": 1,
  "allow_rename": 1,
  "autoname": "naming_series:",
- "creation": "2013-03-07 09:04:18",
+ "creation": "2022-02-21 11:54:09.632218",
  "doctype": "DocType",
  "document_type": "Setup",
  "editable_grid": 1,
@@ -813,11 +813,12 @@
  "idx": 24,
  "image_field": "image",
  "links": [],
- "modified": "2021-06-17 11:31:37.730760",
+ "modified": "2022-03-22 13:44:37.088519",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Employee",
  "name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
   {
@@ -857,7 +858,9 @@
  ],
  "search_fields": "employee_name",
  "show_name_in_global_search": 1,
+ "show_title_field_in_link": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "title_field": "employee_name"
 }
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 60b32b8..9ca05b9 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -462,6 +462,7 @@
 			work_order_data = {
 				"wip_warehouse": default_warehouses.get("wip_warehouse"),
 				"fg_warehouse": default_warehouses.get("fg_warehouse"),
+				"company": self.get("company"),
 			}
 
 			self.prepare_data_for_sub_assembly_items(row, work_order_data)
@@ -499,6 +500,7 @@
 
 		for supplier, po_list in subcontracted_po.items():
 			po = frappe.new_doc("Purchase Order")
+			po.company = self.company
 			po.supplier = supplier
 			po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
 			po.is_subcontracted = 1
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 69f60a2..e6b7526 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -262,8 +262,6 @@
 erpnext.patches.v13_0.update_shipment_status
 erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
 erpnext.patches.v12_0.add_ewaybill_validity_field
-erpnext.patches.v13_0.germany_make_custom_fields
-erpnext.patches.v13_0.germany_fill_debtor_creditor_number
 erpnext.patches.v13_0.set_pos_closing_as_failed
 erpnext.patches.v13_0.rename_stop_to_send_birthday_reminders
 execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
@@ -343,6 +341,7 @@
 erpnext.patches.v14_0.delete_hub_doctypes
 erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
 erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v14_0.delete_datev_doctypes
 erpnext.patches.v14_0.rearrange_company_fields
 erpnext.patches.v14_0.update_leave_notification_template
 erpnext.patches.v14_0.restore_einvoice_fields
diff --git a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py
deleted file mode 100644
index fc3e68a..0000000
--- a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright (c) 2019, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-def execute():
-	"""Move account number into the new custom field debtor_creditor_number.
-
-	German companies used to use a dedicated payable/receivable account for
-	every party to mimick party accounts in the external accounting software
-	"DATEV". This is no longer necessary. The reference ID for DATEV will be
-	stored in a new custom field "debtor_creditor_number".
-	"""
-	company_list = frappe.get_all("Company", filters={"country": "Germany"})
-
-	for company in company_list:
-		party_account_list = frappe.get_all(
-			"Party Account",
-			filters={"company": company.name},
-			fields=["name", "account", "debtor_creditor_number"],
-		)
-		for party_account in party_account_list:
-			if (not party_account.account) or party_account.debtor_creditor_number:
-				# account empty or debtor_creditor_number already filled
-				continue
-
-			account_number = frappe.db.get_value("Account", party_account.account, "account_number")
-			if not account_number:
-				continue
-
-			frappe.db.set_value(
-				"Party Account", party_account.name, "debtor_creditor_number", account_number
-			)
-			frappe.db.set_value("Party Account", party_account.name, "account", "")
diff --git a/erpnext/patches/v13_0/germany_make_custom_fields.py b/erpnext/patches/v13_0/germany_make_custom_fields.py
deleted file mode 100644
index cc35813..0000000
--- a/erpnext/patches/v13_0/germany_make_custom_fields.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2019, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-from erpnext.regional.germany.setup import make_custom_fields
-
-
-def execute():
-	"""Execute the make_custom_fields method for german companies.
-
-	It is usually run once at setup of a new company. Since it's new, run it
-	once for existing companies as well.
-	"""
-	company_list = frappe.get_all("Company", filters={"country": "Germany"})
-	if not company_list:
-		return
-
-	make_custom_fields()
diff --git a/erpnext/patches/v14_0/delete_datev_doctypes.py b/erpnext/patches/v14_0/delete_datev_doctypes.py
new file mode 100644
index 0000000..a5de91f
--- /dev/null
+++ b/erpnext/patches/v14_0/delete_datev_doctypes.py
@@ -0,0 +1,13 @@
+import frappe
+
+
+def execute():
+	install_apps = frappe.get_installed_apps()
+	if "erpnext_datev_uo" in install_apps or "erpnext_datev" in install_apps:
+		return
+
+	# doctypes
+	frappe.delete_doc("DocType", "DATEV Settings", ignore_missing=True, force=True)
+
+	# reports
+	frappe.delete_doc("Report", "DATEV", ignore_missing=True, force=True)
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
index 67bb447..aa03d80 100644
--- a/erpnext/payroll/doctype/gratuity/test_gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -24,7 +24,9 @@
 		frappe.db.delete("Gratuity")
 		frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
 
-		make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
+		make_earning_salary_component(
+			setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
+		)
 		make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
 
 	def test_get_last_salary_slip_should_return_none_for_new_employee(self):
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 38fecac..1922329 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -952,8 +952,12 @@
 		)
 
 		# Structured tax amount
-		total_structured_tax_amount = self.calculate_tax_by_tax_slab(
-			total_taxable_earnings_without_full_tax_addl_components, tax_slab
+		eval_locals = self.get_data_for_eval()
+		total_structured_tax_amount = calculate_tax_by_tax_slab(
+			total_taxable_earnings_without_full_tax_addl_components,
+			tax_slab,
+			self.whitelisted_globals,
+			eval_locals,
 		)
 		current_structured_tax_amount = (
 			total_structured_tax_amount - previous_total_paid_taxes
@@ -962,7 +966,9 @@
 		# Total taxable earnings with additional earnings with full tax
 		full_tax_on_additional_earnings = 0.0
 		if current_additional_earnings_with_full_tax:
-			total_tax_amount = self.calculate_tax_by_tax_slab(total_taxable_earnings, tax_slab)
+			total_tax_amount = calculate_tax_by_tax_slab(
+				total_taxable_earnings, tax_slab, self.whitelisted_globals, eval_locals
+			)
 			full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount
 
 		current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings
@@ -1278,50 +1284,6 @@
 			fields="SUM(amount) as total_amount",
 		)[0].total_amount
 
-	def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab):
-		data = self.get_data_for_eval()
-		data.update({"annual_taxable_earning": annual_taxable_earning})
-		tax_amount = 0
-		for slab in tax_slab.slabs:
-			cond = cstr(slab.condition).strip()
-			if cond and not self.eval_tax_slab_condition(cond, data):
-				continue
-			if not slab.to_amount and annual_taxable_earning >= slab.from_amount:
-				tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01
-				continue
-			if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount:
-				tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01
-			elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount:
-				tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * 0.01
-
-		# 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) > annual_taxable_earning:
-				continue
-
-			if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning:
-				continue
-
-			tax_amount += tax_amount * flt(d.percent) / 100
-
-		return tax_amount
-
-	def eval_tax_slab_condition(self, condition, data):
-		try:
-			condition = condition.strip()
-			if condition:
-				return frappe.safe_eval(condition, self.whitelisted_globals, data)
-		except NameError as err:
-			frappe.throw(
-				_("{0} <br> This error can be due to missing or deleted field.").format(err),
-				title=_("Name error"),
-			)
-		except SyntaxError as err:
-			frappe.throw(_("Syntax error in condition: {0}").format(err))
-		except Exception as e:
-			frappe.throw(_("Error in formula or condition: {0}").format(e))
-			raise
-
 	def get_component_totals(self, component_type, depends_on_payment_days=0):
 		joining_date, relieving_date = frappe.get_cached_value(
 			"Employee", self.employee, ["date_of_joining", "relieving_date"]
@@ -1705,3 +1667,60 @@
 		)
 
 	return payroll_payable_account
+
+
+def calculate_tax_by_tax_slab(
+	annual_taxable_earning, tax_slab, eval_globals=None, eval_locals=None
+):
+	eval_locals.update({"annual_taxable_earning": annual_taxable_earning})
+	tax_amount = 0
+	for slab in tax_slab.slabs:
+		cond = cstr(slab.condition).strip()
+		if cond and not eval_tax_slab_condition(cond, eval_globals, eval_locals):
+			continue
+		if not slab.to_amount and annual_taxable_earning >= slab.from_amount:
+			tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01
+			continue
+		if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount:
+			tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01
+		elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount:
+			tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * 0.01
+
+	# 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) > annual_taxable_earning:
+			continue
+
+		if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning:
+			continue
+
+		tax_amount += tax_amount * flt(d.percent) / 100
+
+	return tax_amount
+
+
+def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None):
+	if not eval_globals:
+		eval_globals = {
+			"int": int,
+			"float": float,
+			"long": int,
+			"round": round,
+			"date": datetime.date,
+			"getdate": getdate,
+		}
+
+	try:
+		condition = condition.strip()
+		if condition:
+			return frappe.safe_eval(condition, eval_globals, eval_locals)
+	except NameError as err:
+		frappe.throw(
+			_("{0} <br> This error can be due to missing or deleted field.").format(err),
+			title=_("Name error"),
+		)
+	except SyntaxError as err:
+		frappe.throw(_("Syntax error in condition: {0} in Income Tax Slab").format(err))
+	except Exception as e:
+		frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e))
+		raise
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index dbeadc5..869ea83 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -772,6 +772,7 @@
 			"Monthly",
 			other_details={"max_benefits": 100000},
 			test_tax=True,
+			include_flexi_benefits=True,
 			employee=employee,
 			payroll_period=payroll_period,
 		)
@@ -875,6 +876,7 @@
 			"Monthly",
 			other_details={"max_benefits": 100000},
 			test_tax=True,
+			include_flexi_benefits=True,
 			employee=employee,
 			payroll_period=payroll_period,
 		)
@@ -1022,7 +1024,9 @@
 	return account
 
 
-def make_earning_salary_component(setup=False, test_tax=False, company_list=None):
+def make_earning_salary_component(
+	setup=False, test_tax=False, company_list=None, include_flexi_benefits=False
+):
 	data = [
 		{
 			"salary_component": "Basic Salary",
@@ -1043,7 +1047,7 @@
 		},
 		{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
 	]
-	if test_tax:
+	if include_flexi_benefits:
 		data.extend(
 			[
 				{
@@ -1063,11 +1067,18 @@
 					"type": "Earning",
 					"max_benefit_amount": 15000,
 				},
+			]
+		)
+	if test_tax:
+		data.extend(
+			[
 				{"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"},
 			]
 		)
+
 	if setup or test_tax:
 		make_salary_component(data, test_tax, company_list)
+
 	data.append(
 		{
 			"salary_component": "Basic Salary",
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index def622b..2eb1671 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -149,6 +149,7 @@
 	company=None,
 	currency=erpnext.get_default_currency(),
 	payroll_period=None,
+	include_flexi_benefits=False,
 ):
 	if test_tax:
 		frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
@@ -161,7 +162,10 @@
 		"name": salary_structure,
 		"company": company or erpnext.get_default_company(),
 		"earnings": make_earning_salary_component(
-			setup=True, test_tax=test_tax, company_list=["_Test Company"]
+			setup=True,
+			test_tax=test_tax,
+			company_list=["_Test Company"],
+			include_flexi_benefits=include_flexi_benefits,
 		),
 		"deductions": make_deduction_salary_component(
 			setup=True, test_tax=test_tax, company_list=["_Test Company"]
diff --git a/erpnext/regional/doctype/datev_settings/__init__.py b/erpnext/payroll/report/income_tax_computation/__init__.py
similarity index 100%
rename from erpnext/regional/doctype/datev_settings/__init__.py
rename to erpnext/payroll/report/income_tax_computation/__init__.py
diff --git a/erpnext/payroll/report/income_tax_computation/income_tax_computation.js b/erpnext/payroll/report/income_tax_computation/income_tax_computation.js
new file mode 100644
index 0000000..26e463f
--- /dev/null
+++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.js
@@ -0,0 +1,47 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Income Tax Computation"] = {
+	"filters": [
+		{
+			"fieldname":"company",
+			"label": __("Company"),
+			"fieldtype": "Link",
+			"options": "Company",
+			"default": frappe.defaults.get_user_default("Company"),
+			"width": "100px",
+			"reqd": 1
+		},
+		{
+			"fieldname":"payroll_period",
+			"label": __("Payroll Period"),
+			"fieldtype": "Link",
+			"options": "Payroll Period",
+			"width": "100px",
+			"reqd": 1
+		},
+		{
+			"fieldname":"employee",
+			"label": __("Employee"),
+			"fieldtype": "Link",
+			"options": "Employee",
+			"width": "100px"
+		},
+		{
+			"fieldname":"department",
+			"label": __("Department"),
+			"fieldtype": "Link",
+			"options": "Department",
+			"width": "100px",
+		},
+		{
+			"fieldname":"consider_tax_exemption_declaration",
+			"label": __("Consider Tax Exemption Declaration"),
+			"fieldtype": "Check",
+			"width": "180px"
+		}
+	]
+};
+
+
diff --git a/erpnext/payroll/report/income_tax_computation/income_tax_computation.json b/erpnext/payroll/report/income_tax_computation/income_tax_computation.json
new file mode 100644
index 0000000..7cb5b22
--- /dev/null
+++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.json
@@ -0,0 +1,36 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2022-02-17 17:19:30.921422",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "",
+ "modified": "2022-02-23 13:07:30.347861",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Income Tax Computation",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Salary Slip",
+ "report_name": "Income Tax Computation",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Employee"
+  },
+  {
+   "role": "HR User"
+  },
+  {
+   "role": "HR Manager"
+  },
+  {
+   "role": "Employee Self Service"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py
new file mode 100644
index 0000000..739ed8e
--- /dev/null
+++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py
@@ -0,0 +1,513 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, scrub
+from frappe.query_builder.functions import Sum
+from frappe.utils import add_days, flt, getdate, rounded
+
+from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
+from erpnext.payroll.doctype.salary_slip.salary_slip import calculate_tax_by_tax_slab
+
+
+def execute(filters=None):
+	return IncomeTaxComputationReport(filters).run()
+
+
+class IncomeTaxComputationReport(object):
+	def __init__(self, filters=None):
+		self.filters = frappe._dict(filters or {})
+		self.columns = []
+		self.data = []
+		self.employees = frappe._dict()
+		self.payroll_period_start_date = None
+		self.payroll_period_end_date = None
+		if self.filters.payroll_period:
+			self.payroll_period_start_date, self.payroll_period_end_date = frappe.db.get_value(
+				"Payroll Period", self.filters.payroll_period, ["start_date", "end_date"]
+			)
+
+	def run(self):
+		self.get_fixed_columns()
+		self.get_data()
+		return self.columns, self.data
+
+	def get_data(self):
+		self.get_employee_details()
+		self.get_future_salary_slips()
+		self.get_ctc()
+		self.get_tax_exempted_earnings_and_deductions()
+		self.get_employee_tax_exemptions()
+		self.get_hra()
+		self.get_standard_tax_exemption()
+		self.get_total_taxable_amount()
+		self.get_applicable_tax()
+		self.get_total_deducted_tax()
+		self.get_payable_tax()
+
+		self.data = list(self.employees.values())
+
+	def get_employee_details(self):
+		filters, or_filters = self.get_employee_filters()
+		fields = [
+			"name as employee",
+			"employee_name",
+			"department",
+			"designation",
+			"date_of_joining",
+			"relieving_date",
+		]
+
+		employees = frappe.get_all("Employee", filters=filters, or_filters=or_filters, fields=fields)
+		ss_assignments = self.get_ss_assignments([d.employee for d in employees])
+
+		for d in employees:
+			if d.employee in list(ss_assignments.keys()):
+				d.update(ss_assignments[d.employee])
+				self.employees.setdefault(d.employee, d)
+
+		if not self.employees:
+			frappe.throw(_("No employees found with selected filters and active salary structure"))
+
+	def get_employee_filters(self):
+		filters = {"company": self.filters.company}
+		or_filters = {
+			"status": "Active",
+			"relieving_date": ["between", [self.payroll_period_start_date, self.payroll_period_end_date]],
+		}
+		if self.filters.employee:
+			filters = {"name": self.filters.employee}
+		elif self.filters.department:
+			filters.update({"department": self.filters.department})
+
+		return filters, or_filters
+
+	def get_ss_assignments(self, employees):
+		ss_assignments = frappe.get_all(
+			"Salary Structure Assignment",
+			filters={
+				"employee": ["in", employees],
+				"docstatus": 1,
+				"salary_structure": ["is", "set"],
+				"income_tax_slab": ["is", "set"],
+			},
+			fields=["employee", "income_tax_slab", "salary_structure"],
+			order_by="from_date desc",
+		)
+
+		employee_ss_assignments = frappe._dict()
+		for d in ss_assignments:
+			if d.employee not in list(employee_ss_assignments.keys()):
+				tax_slab = frappe.get_cached_value(
+					"Income Tax Slab", d.income_tax_slab, ["allow_tax_exemption", "disabled"], as_dict=1
+				)
+
+				if tax_slab and not tax_slab.disabled:
+					employee_ss_assignments.setdefault(
+						d.employee,
+						{
+							"salary_structure": d.salary_structure,
+							"income_tax_slab": d.income_tax_slab,
+							"allow_tax_exemption": tax_slab.allow_tax_exemption,
+						},
+					)
+		return employee_ss_assignments
+
+	def get_future_salary_slips(self):
+		self.future_salary_slips = frappe._dict()
+		for employee in list(self.employees.keys()):
+			last_ss = self.get_last_salary_slip(employee)
+			if last_ss and last_ss.end_date == self.payroll_period_end_date:
+				continue
+
+			relieving_date = self.employees[employee].get("relieving_date", "")
+			if last_ss:
+				ss_start_date = add_days(last_ss.end_date, 1)
+			else:
+				ss_start_date = self.payroll_period_start_date
+				last_ss = frappe._dict(
+					{
+						"payroll_frequency": "Monthly",
+						"salary_structure": self.employees[employee].get("salary_structure"),
+					}
+				)
+
+			while getdate(ss_start_date) < getdate(self.payroll_period_end_date) and (
+				not relieving_date or getdate(ss_start_date) < relieving_date
+			):
+				ss_end_date = get_start_end_dates(last_ss.payroll_frequency, ss_start_date).end_date
+
+				ss = frappe.new_doc("Salary Slip")
+				ss.employee = employee
+				ss.start_date = ss_start_date
+				ss.end_date = ss_end_date
+				ss.salary_structure = last_ss.salary_structure
+				ss.payroll_frequency = last_ss.payroll_frequency
+				ss.company = self.filters.company
+				try:
+					ss.process_salary_structure(for_preview=1)
+					self.future_salary_slips.setdefault(employee, []).append(ss.as_dict())
+				except Exception:
+					break
+
+				ss_start_date = add_days(ss_end_date, 1)
+
+	def get_last_salary_slip(self, employee):
+		last_salary_slip = frappe.db.get_value(
+			"Salary Slip",
+			{
+				"employee": employee,
+				"docstatus": 1,
+				"start_date": ["between", [self.payroll_period_start_date, self.payroll_period_end_date]],
+			},
+			["start_date", "end_date", "salary_structure", "payroll_frequency"],
+			order_by="start_date desc",
+			as_dict=1,
+		)
+
+		return last_salary_slip
+
+	def get_ctc(self):
+		# Get total earnings from existing salary slip
+		ss = frappe.qb.DocType("Salary Slip")
+		existing_ss = frappe._dict(
+			(
+				frappe.qb.from_(ss)
+				.select(ss.employee, Sum(ss.base_gross_pay).as_("amount"))
+				.where(ss.docstatus == 1)
+				.where(ss.employee.isin(list(self.employees.keys())))
+				.where(ss.start_date >= self.payroll_period_start_date)
+				.where(ss.end_date <= self.payroll_period_end_date)
+				.groupby(ss.employee)
+			).run()
+		)
+
+		for employee in list(self.employees.keys()):
+			future_ss_earnings = self.get_future_earnings(employee)
+			ctc = flt(existing_ss.get(employee)) + future_ss_earnings
+
+			self.employees[employee].setdefault("ctc", ctc)
+
+	def get_future_earnings(self, employee):
+		future_earnings = 0.0
+		for ss in self.future_salary_slips.get(employee, []):
+			future_earnings += flt(ss.base_gross_pay)
+
+		return future_earnings
+
+	def get_tax_exempted_earnings_and_deductions(self):
+		tax_exempted_components = self.get_tax_exempted_components()
+
+		# Get component totals from existing salary slips
+		ss = frappe.qb.DocType("Salary Slip")
+		ss_comps = frappe.qb.DocType("Salary Detail")
+
+		records = (
+			frappe.qb.from_(ss)
+			.inner_join(ss_comps)
+			.on(ss.name == ss_comps.parent)
+			.select(ss.name, ss.employee, ss_comps.salary_component, Sum(ss_comps.amount).as_("amount"))
+			.where(ss.docstatus == 1)
+			.where(ss.employee.isin(list(self.employees.keys())))
+			.where(ss_comps.salary_component.isin(tax_exempted_components))
+			.where(ss.start_date >= self.payroll_period_start_date)
+			.where(ss.end_date <= self.payroll_period_end_date)
+			.groupby(ss.employee, ss_comps.salary_component)
+		).run(as_dict=True)
+
+		existing_ss_exemptions = frappe._dict()
+		for d in records:
+			existing_ss_exemptions.setdefault(d.employee, {}).setdefault(
+				scrub(d.salary_component), d.amount
+			)
+
+		for employee in list(self.employees.keys()):
+			if not self.employees[employee]["allow_tax_exemption"]:
+				continue
+
+			exemptions = existing_ss_exemptions.get(employee, {})
+			self.add_exemptions_from_future_salary_slips(employee, exemptions)
+			self.employees[employee].update(exemptions)
+
+			total_exemptions = sum(list(exemptions.values()))
+			self.add_to_total_exemption(employee, total_exemptions)
+
+	def add_exemptions_from_future_salary_slips(self, employee, exemptions):
+		for ss in self.future_salary_slips.get(employee, []):
+			for e in ss.earnings:
+				if not e.is_tax_applicable:
+					exemptions.setdefault(scrub(e.salary_component), 0)
+					exemptions[scrub(e.salary_component)] += flt(e.amount)
+
+			for d in ss.deductions:
+				if d.exempted_from_income_tax:
+					exemptions.setdefault(scrub(d.salary_component), 0)
+					exemptions[scrub(d.salary_component)] += flt(d.amount)
+
+		return exemptions
+
+	def get_tax_exempted_components(self):
+		# nontaxable earning components
+		nontaxable_earning_components = [
+			d.name
+			for d in frappe.get_all(
+				"Salary Component", {"type": "Earning", "is_tax_applicable": 0, "disabled": 0}
+			)
+		]
+
+		# tax exempted deduction components
+		tax_exempted_deduction_components = [
+			d.name
+			for d in frappe.get_all(
+				"Salary Component", {"type": "Deduction", "exempted_from_income_tax": 1, "disabled": 0}
+			)
+		]
+
+		tax_exempted_components = nontaxable_earning_components + tax_exempted_deduction_components
+
+		# Add columns
+		for d in tax_exempted_components:
+			self.add_column(d)
+
+		return tax_exempted_components
+
+	def add_to_total_exemption(self, employee, amount):
+		self.employees[employee].setdefault("total_exemption", 0)
+		self.employees[employee]["total_exemption"] += amount
+
+	def get_employee_tax_exemptions(self):
+		# add columns
+		exemption_categories = frappe.get_all("Employee Tax Exemption Category", {"is_active": 1})
+		for d in exemption_categories:
+			self.add_column(d.name)
+
+		self.employees_with_proofs = []
+		self.get_tax_exemptions("Employee Tax Exemption Proof Submission")
+		if self.filters.consider_tax_exemption_declaration:
+			self.get_tax_exemptions("Employee Tax Exemption Declaration")
+
+	def get_tax_exemptions(self, source):
+		# Get category-wise exmeptions based on submitted proofs or declarations
+		if source == "Employee Tax Exemption Proof Submission":
+			child_doctype = "Employee Tax Exemption Proof Submission Detail"
+		else:
+			child_doctype = "Employee Tax Exemption Declaration Category"
+
+		max_exemptions = self.get_max_exemptions_based_on_category()
+
+		par = frappe.qb.DocType(source)
+		child = frappe.qb.DocType(child_doctype)
+
+		records = (
+			frappe.qb.from_(par)
+			.inner_join(child)
+			.on(par.name == child.parent)
+			.select(par.employee, child.exemption_category, Sum(child.amount).as_("amount"))
+			.where(par.docstatus == 1)
+			.where(par.employee.isin(list(self.employees.keys())))
+			.where(par.payroll_period == self.filters.payroll_period)
+			.groupby(par.employee, child.exemption_category)
+		).run(as_dict=True)
+
+		for d in records:
+			if not self.employees[d.employee]["allow_tax_exemption"]:
+				continue
+
+			if source == "Employee Tax Exemption Declaration" and d.employee in self.employees_with_proofs:
+				continue
+
+			amount = flt(d.amount)
+			max_eligible_amount = flt(max_exemptions.get(d.exemption_category))
+			if max_eligible_amount and amount > max_eligible_amount:
+				amount = max_eligible_amount
+
+			self.employees[d.employee].setdefault(scrub(d.exemption_category), amount)
+			self.add_to_total_exemption(d.employee, amount)
+
+			if (
+				source == "Employee Tax Exemption Proof Submission"
+				and d.employee not in self.employees_with_proofs
+			):
+				self.employees_with_proofs.append(d.employee)
+
+	def get_max_exemptions_based_on_category(self):
+		return dict(
+			frappe.get_all(
+				"Employee Tax Exemption Category",
+				filters={"is_active": 1},
+				fields=["name", "max_amount"],
+				as_list=1,
+			)
+		)
+
+	def get_hra(self):
+		if not frappe.get_meta("Employee Tax Exemption Declaration").has_field("monthly_house_rent"):
+			return
+
+		self.add_column("HRA")
+
+		self.employees_with_proofs = []
+		self.get_eligible_hra("Employee Tax Exemption Proof Submission")
+		if self.filters.consider_tax_exemption_declaration:
+			self.get_eligible_hra("Employee Tax Exemption Declaration")
+
+	def get_eligible_hra(self, source):
+		if source == "Employee Tax Exemption Proof Submission":
+			hra_amount_field = "total_eligible_hra_exemption"
+		else:
+			hra_amount_field = "annual_hra_exemption"
+
+		records = frappe.get_all(
+			source,
+			filters={
+				"docstatus": 1,
+				"employee": ["in", list(self.employees.keys())],
+				"payroll_period": self.filters.payroll_period,
+			},
+			fields=["employee", hra_amount_field],
+			as_list=1,
+		)
+
+		for d in records:
+			if not self.employees[d[0]]["allow_tax_exemption"]:
+				continue
+
+			if d[0] not in self.employees_with_proofs:
+				self.employees[d[0]].setdefault("hra", d[1])
+				self.add_to_total_exemption(d[0], d[1])
+				self.employees_with_proofs.append(d[0])
+
+	def get_standard_tax_exemption(self):
+		self.add_column("Standard Tax Exemption")
+
+		standard_exemptions_per_slab = dict(
+			frappe.get_all(
+				"Income Tax Slab",
+				filters={"company": self.filters.company, "docstatus": 1, "disabled": 0},
+				fields=["name", "standard_tax_exemption_amount"],
+				as_list=1,
+			)
+		)
+
+		for emp, emp_details in self.employees.items():
+			if not self.employees[emp]["allow_tax_exemption"]:
+				continue
+
+			income_tax_slab = emp_details.get("income_tax_slab")
+			standard_exemption = standard_exemptions_per_slab.get(income_tax_slab, 0)
+			emp_details["standard_tax_exemption"] = standard_exemption
+			self.add_to_total_exemption(emp, standard_exemption)
+
+		self.add_column("Total Exemption")
+
+	def get_total_taxable_amount(self):
+		self.add_column("Total Taxable Amount")
+		for emp, emp_details in self.employees.items():
+			emp_details["total_taxable_amount"] = flt(emp_details.get("ctc")) - flt(
+				emp_details.get("total_exemption")
+			)
+
+	def get_applicable_tax(self):
+		self.add_column("Applicable Tax")
+
+		is_tax_rounded = frappe.db.get_value(
+			"Salary Component",
+			{"variable_based_on_taxable_salary": 1, "disabled": 0},
+			"round_to_the_nearest_integer",
+		)
+
+		for emp, emp_details in self.employees.items():
+			tax_slab = emp_details.get("income_tax_slab")
+			if tax_slab:
+				tax_slab = frappe.get_cached_doc("Income Tax Slab", tax_slab)
+				employee_dict = frappe.get_doc("Employee", emp).as_dict()
+				tax_amount = calculate_tax_by_tax_slab(
+					emp_details["total_taxable_amount"], tax_slab, eval_globals=None, eval_locals=employee_dict
+				)
+			else:
+				tax_amount = 0.0
+
+			if is_tax_rounded:
+				tax_amount = rounded(tax_amount)
+			emp_details["applicable_tax"] = tax_amount
+
+	def get_total_deducted_tax(self):
+		self.add_column("Total Tax Deducted")
+
+		ss = frappe.qb.DocType("Salary Slip")
+		ss_ded = frappe.qb.DocType("Salary Detail")
+
+		records = (
+			frappe.qb.from_(ss)
+			.inner_join(ss_ded)
+			.on(ss.name == ss_ded.parent)
+			.select(ss.employee, Sum(ss_ded.amount).as_("amount"))
+			.where(ss.docstatus == 1)
+			.where(ss.employee.isin(list(self.employees.keys())))
+			.where(ss_ded.parentfield == "deductions")
+			.where(ss_ded.variable_based_on_taxable_salary == 1)
+			.where(ss.start_date >= self.payroll_period_start_date)
+			.where(ss.end_date <= self.payroll_period_end_date)
+			.groupby(ss.employee)
+		).run(as_dict=True)
+
+		for d in records:
+			self.employees[d.employee].setdefault("total_tax_deducted", d.amount)
+
+	def get_payable_tax(self):
+		self.add_column("Payable Tax")
+
+		for emp, emp_details in self.employees.items():
+			emp_details["payable_tax"] = flt(emp_details.get("applicable_tax")) - flt(
+				emp_details.get("total_tax_deducted")
+			)
+
+	def add_column(self, label, fieldname=None, fieldtype=None, options=None, width=None):
+		col = {
+			"label": _(label),
+			"fieldname": fieldname or scrub(label),
+			"fieldtype": fieldtype or "Currency",
+			"options": options,
+			"width": width or "140px",
+		}
+		self.columns.append(col)
+
+	def get_fixed_columns(self):
+		self.columns = [
+			{
+				"label": _("Employee"),
+				"fieldname": "employee",
+				"fieldtype": "Link",
+				"options": "Employee",
+				"width": "140px",
+			},
+			{
+				"label": _("Employee Name"),
+				"fieldname": "employee_name",
+				"fieldtype": "Data",
+				"width": "160px",
+			},
+			{
+				"label": _("Department"),
+				"fieldname": "department",
+				"fieldtype": "Link",
+				"options": "Department",
+				"width": "140px",
+			},
+			{
+				"label": _("Designation"),
+				"fieldname": "designation",
+				"fieldtype": "Link",
+				"options": "Designation",
+				"width": "140px",
+			},
+			{"label": _("Date of Joining"), "fieldname": "date_of_joining", "fieldtype": "Date"},
+			{
+				"label": _("Income Tax Slab"),
+				"fieldname": "income_tax_slab",
+				"fieldtype": "Link",
+				"options": "Income Tax Slab",
+				"width": "140px",
+			},
+			{"label": _("CTC"), "fieldname": "ctc", "fieldtype": "Currency", "width": "140px"},
+		]
diff --git a/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py b/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py
new file mode 100644
index 0000000..57ca317
--- /dev/null
+++ b/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py
@@ -0,0 +1,115 @@
+import unittest
+
+import frappe
+from frappe.utils import getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
+	create_payroll_period,
+)
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+	create_exemption_declaration,
+	create_salary_slips_for_payroll_period,
+	create_tax_slab,
+)
+from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+from erpnext.payroll.report.income_tax_computation.income_tax_computation import execute
+
+
+class TestIncomeTaxComputation(unittest.TestCase):
+	def setUp(self):
+		self.cleanup_records()
+		self.create_records()
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+	def cleanup_records(self):
+		frappe.db.sql("delete from `tabEmployee Tax Exemption Declaration`")
+		frappe.db.sql("delete from `tabPayroll Period`")
+		frappe.db.sql("delete from `tabIncome Tax Slab`")
+		frappe.db.sql("delete from `tabSalary Component`")
+		frappe.db.sql("delete from `tabEmployee Benefit Application`")
+		frappe.db.sql("delete from `tabEmployee Benefit Claim`")
+		frappe.db.sql("delete from `tabEmployee` where company='_Test Company'")
+		frappe.db.sql("delete from `tabSalary Slip`")
+
+	def create_records(self):
+		self.employee = make_employee(
+			"employee_tax_computation@example.com",
+			company="_Test Company",
+			date_of_joining=getdate("01-10-2021"),
+		)
+
+		self.payroll_period = create_payroll_period(
+			name="_Test Payroll Period 1", company="_Test Company"
+		)
+
+		self.income_tax_slab = create_tax_slab(
+			self.payroll_period,
+			allow_tax_exemption=True,
+			effective_date=getdate("2019-04-01"),
+			company="_Test Company",
+		)
+		salary_structure = make_salary_structure(
+			"Monthly Salary Structure Test Income Tax Computation",
+			"Monthly",
+			employee=self.employee,
+			company="_Test Company",
+			currency="INR",
+			payroll_period=self.payroll_period,
+			test_tax=True,
+		)
+
+		create_exemption_declaration(self.employee, self.payroll_period.name)
+
+		create_salary_slips_for_payroll_period(
+			self.employee, salary_structure.name, self.payroll_period, deduct_random=False, num=3
+		)
+
+	def test_report(self):
+		filters = frappe._dict(
+			{
+				"company": "_Test Company",
+				"payroll_period": self.payroll_period.name,
+				"employee": self.employee,
+			}
+		)
+
+		result = execute(filters)
+
+		expected_data = {
+			"employee": self.employee,
+			"employee_name": "employee_tax_computation@example.com",
+			"department": "All Departments",
+			"income_tax_slab": self.income_tax_slab,
+			"ctc": 936000.0,
+			"professional_tax": 2400.0,
+			"standard_tax_exemption": 50000,
+			"total_exemption": 52400.0,
+			"total_taxable_amount": 883600.0,
+			"applicable_tax": 92789.0,
+			"total_tax_deducted": 17997.0,
+			"payable_tax": 74792,
+		}
+
+		for key, val in expected_data.items():
+			self.assertEqual(result[1][0].get(key), val)
+
+		# Run report considering tax exemption declaration
+		filters.consider_tax_exemption_declaration = 1
+
+		result = execute(filters)
+
+		expected_data.update(
+			{
+				"_test_category": 100000.0,
+				"total_exemption": 152400.0,
+				"total_taxable_amount": 783600.0,
+				"applicable_tax": 71989.0,
+				"payable_tax": 53992.0,
+			}
+		)
+
+		for key, val in expected_data.items():
+			self.assertEqual(result[1][0].get(key), val)
diff --git a/erpnext/payroll/workspace/payroll/payroll.json b/erpnext/payroll/workspace/payroll/payroll.json
index 762bea0..5629e63 100644
--- a/erpnext/payroll/workspace/payroll/payroll.json
+++ b/erpnext/payroll/workspace/payroll/payroll.json
@@ -246,6 +246,17 @@
    "type": "Link"
   },
   {
+    "dependencies": "Salary Structure",
+    "hidden": 0,
+    "is_query_report": 1,
+    "label": "Income Tax Computation",
+    "link_count": 0,
+    "link_to": "Income Tax Computation",
+    "link_type": "Report",
+    "onboard": 0,
+    "type": "Link"
+   },
+  {
    "dependencies": "Salary Slip",
    "hidden": 0,
    "is_query_report": 1,
@@ -312,7 +323,7 @@
    "type": "Link"
   }
  ],
- "modified": "2022-01-13 17:41:19.098813",
+ "modified": "2022-02-23 17:41:19.098813",
  "modified_by": "Administrator",
  "module": "Payroll",
  "name": "Payroll",
diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js
index c954f12..2dbe999 100644
--- a/erpnext/public/js/call_popup/call_popup.js
+++ b/erpnext/public/js/call_popup/call_popup.js
@@ -141,6 +141,14 @@
 				'fieldtype': 'Section Break',
 				'hide_border': 1,
 			}, {
+				'fieldname': 'call_type',
+				'label': 'Call Type',
+				'fieldtype': 'Link',
+				'options': 'Telephony Call Type',
+			}, {
+				'fieldtype': 'Section Break',
+				'hide_border': 1,
+			}, {
 				'fieldtype': 'Small Text',
 				'label': __('Call Summary'),
 				'fieldname': 'call_summary',
@@ -149,10 +157,12 @@
 				'label': __('Save'),
 				'click': () => {
 					const call_summary = this.call_details.get_value('call_summary');
+					const call_type = this.call_details.get_value('call_type');
 					if (!call_summary) return;
-					frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', {
+					frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type', {
 						'call_log': this.call_log.name,
 						'summary': call_summary,
+						'call_type': call_type,
 					}).then(() => {
 						this.close_modal();
 						frappe.show_alert({
diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
index daf7a69..04e8211 100644
--- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
+++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
@@ -19,7 +19,7 @@
 				)
 			).insert()
 
-			frappe.form_dict = dict(
+			frappe.local.form_dict = frappe._dict(
 				doctype="Quality Procedure",
 				quality_procedure_name="Test Child 1",
 				parent_quality_procedure=procedure.name,
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.js b/erpnext/regional/doctype/datev_settings/datev_settings.js
deleted file mode 100644
index 3c36549..0000000
--- a/erpnext/regional/doctype/datev_settings/datev_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('DATEV Settings', {
-	refresh: function(frm) {
-		frm.add_custom_button(__('Show Report'), () => frappe.set_route('query-report', 'DATEV'), "fa fa-table");
-	}
-});
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json
deleted file mode 100644
index f60de4c..0000000
--- a/erpnext/regional/doctype/datev_settings/datev_settings.json
+++ /dev/null
@@ -1,125 +0,0 @@
-{
- "actions": [],
- "autoname": "field:client",
- "creation": "2019-08-13 23:56:34.259906",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
-  "client",
-  "client_number",
-  "column_break_2",
-  "consultant_number",
-  "consultant",
-  "section_break_4",
-  "account_number_length",
-  "column_break_6",
-  "temporary_against_account_number"
- ],
- "fields": [
-  {
-   "fieldname": "client",
-   "fieldtype": "Link",
-   "in_list_view": 1,
-   "label": "Client",
-   "options": "Company",
-   "reqd": 1,
-   "unique": 1
-  },
-  {
-   "fieldname": "client_number",
-   "fieldtype": "Data",
-   "in_list_view": 1,
-   "label": "Client ID",
-   "length": 5,
-   "reqd": 1
-  },
-  {
-   "fieldname": "consultant",
-   "fieldtype": "Link",
-   "in_list_view": 1,
-   "label": "Consultant",
-   "options": "Supplier"
-  },
-  {
-   "fieldname": "consultant_number",
-   "fieldtype": "Data",
-   "in_list_view": 1,
-   "label": "Consultant ID",
-   "length": 7,
-   "reqd": 1
-  },
-  {
-   "fieldname": "column_break_2",
-   "fieldtype": "Column Break"
-  },
-  {
-   "fieldname": "section_break_4",
-   "fieldtype": "Section Break"
-  },
-  {
-   "fieldname": "column_break_6",
-   "fieldtype": "Column Break"
-  },
-  {
-   "default": "4",
-   "fieldname": "account_number_length",
-   "fieldtype": "Int",
-   "label": "Account Number Length",
-   "reqd": 1
-  },
-  {
-   "allow_in_quick_entry": 1,
-   "fieldname": "temporary_against_account_number",
-   "fieldtype": "Data",
-   "label": "Temporary Against Account Number",
-   "reqd": 1
-  }
- ],
- "links": [],
- "modified": "2020-11-19 19:00:09.088816",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "DATEV Settings",
- "owner": "Administrator",
- "permissions": [
-  {
-   "create": 1,
-   "delete": 1,
-   "email": 1,
-   "export": 1,
-   "print": 1,
-   "read": 1,
-   "report": 1,
-   "role": "System Manager",
-   "share": 1,
-   "write": 1
-  },
-  {
-   "create": 1,
-   "delete": 1,
-   "email": 1,
-   "export": 1,
-   "print": 1,
-   "read": 1,
-   "report": 1,
-   "role": "Accounts Manager",
-   "share": 1,
-   "write": 1
-  },
-  {
-   "create": 1,
-   "email": 1,
-   "export": 1,
-   "print": 1,
-   "read": 1,
-   "report": 1,
-   "role": "Accounts User",
-   "share": 1
-  }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.py b/erpnext/regional/doctype/datev_settings/datev_settings.py
deleted file mode 100644
index 686a93e..0000000
--- a/erpnext/regional/doctype/datev_settings/datev_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-# import frappe
-from frappe.model.document import Document
-
-
-class DATEVSettings(Document):
-	pass
diff --git a/erpnext/regional/doctype/datev_settings/test_datev_settings.py b/erpnext/regional/doctype/datev_settings/test_datev_settings.py
deleted file mode 100644
index ba70eb4..0000000
--- a/erpnext/regional/doctype/datev_settings/test_datev_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestDATEVSettings(unittest.TestCase):
-	pass
diff --git a/erpnext/regional/germany/__init__.py b/erpnext/regional/germany/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/germany/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/germany/setup.py b/erpnext/regional/germany/setup.py
deleted file mode 100644
index b8e66c3..0000000
--- a/erpnext/regional/germany/setup.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def setup(company=None, patch=True):
-	make_custom_fields()
-	add_custom_roles_for_reports()
-
-
-def make_custom_fields():
-	custom_fields = {
-		"Party Account": [
-			dict(
-				fieldname="debtor_creditor_number",
-				label="Debtor/Creditor Number",
-				fieldtype="Data",
-				insert_after="account",
-				translatable=0,
-			)
-		]
-	}
-
-	create_custom_fields(custom_fields)
-
-
-def add_custom_roles_for_reports():
-	"""Add Access Control to UAE VAT 201."""
-	if not frappe.db.get_value("Custom Role", dict(report="DATEV")):
-		frappe.get_doc(
-			dict(
-				doctype="Custom Role",
-				report="DATEV",
-				roles=[dict(role="Accounts User"), dict(role="Accounts Manager")],
-			)
-		).insert()
diff --git a/erpnext/regional/germany/utils/__init__.py b/erpnext/regional/germany/utils/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/germany/utils/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/germany/utils/datev/__init__.py b/erpnext/regional/germany/utils/datev/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/germany/utils/datev/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/germany/utils/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py
deleted file mode 100644
index 9524481..0000000
--- a/erpnext/regional/germany/utils/datev/datev_constants.py
+++ /dev/null
@@ -1,501 +0,0 @@
-"""Constants used in datev.py."""
-
-TRANSACTION_COLUMNS = [
-	# All possible columns must tbe listed here, because DATEV requires them to
-	# be present in the CSV.
-	# ---
-	# Umsatz
-	"Umsatz (ohne Soll/Haben-Kz)",
-	"Soll/Haben-Kennzeichen",
-	"WKZ Umsatz",
-	"Kurs",
-	"Basis-Umsatz",
-	"WKZ Basis-Umsatz",
-	# Konto/Gegenkonto
-	"Konto",
-	"Gegenkonto (ohne BU-Schlüssel)",
-	"BU-Schlüssel",
-	# Datum
-	"Belegdatum",
-	# Rechnungs- / Belegnummer
-	"Belegfeld 1",
-	# z.B. Fälligkeitsdatum Format: TTMMJJ
-	"Belegfeld 2",
-	# Skonto-Betrag / -Abzug (Der Wert 0 ist unzulässig)
-	"Skonto",
-	# Beschreibung des Buchungssatzes
-	"Buchungstext",
-	# Mahn- / Zahl-Sperre (1 = Postensperre)
-	"Postensperre",
-	"Diverse Adressnummer",
-	"Geschäftspartnerbank",
-	"Sachverhalt",
-	# Keine Mahnzinsen
-	"Zinssperre",
-	# Link auf den Buchungsbeleg (Programmkürzel + GUID)
-	"Beleglink",
-	# Beleginfo
-	"Beleginfo - Art 1",
-	"Beleginfo - Inhalt 1",
-	"Beleginfo - Art 2",
-	"Beleginfo - Inhalt 2",
-	"Beleginfo - Art 3",
-	"Beleginfo - Inhalt 3",
-	"Beleginfo - Art 4",
-	"Beleginfo - Inhalt 4",
-	"Beleginfo - Art 5",
-	"Beleginfo - Inhalt 5",
-	"Beleginfo - Art 6",
-	"Beleginfo - Inhalt 6",
-	"Beleginfo - Art 7",
-	"Beleginfo - Inhalt 7",
-	"Beleginfo - Art 8",
-	"Beleginfo - Inhalt 8",
-	# Zuordnung des Geschäftsvorfalls für die Kostenrechnung
-	"KOST1 - Kostenstelle",
-	"KOST2 - Kostenstelle",
-	"KOST-Menge",
-	# USt-ID-Nummer (Beispiel: DE133546770)
-	"EU-Mitgliedstaat u. USt-IdNr.",
-	# Der im EU-Bestimmungsland gültige Steuersatz
-	"EU-Steuersatz",
-	# I = Ist-Versteuerung,
-	# K = keine Umsatzsteuerrechnung
-	# P = Pauschalierung (z. B. für Land- und Forstwirtschaft),
-	# S = Soll-Versteuerung
-	"Abw. Versteuerungsart",
-	# Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG
-	"Sachverhalt L+L",
-	# Steuersatz / Funktion zum L+L-Sachverhalt (Beispiel: Wert 190 für 19%)
-	"Funktionsergänzung L+L",
-	# Bei Verwendung des BU-Schlüssels 49 für „andere Steuersätze“ muss der
-	# steuerliche Sachverhalt mitgegeben werden
-	"BU 49 Hauptfunktionstyp",
-	"BU 49 Hauptfunktionsnummer",
-	"BU 49 Funktionsergänzung",
-	# Zusatzinformationen, besitzen den Charakter eines Notizzettels und können
-	# frei erfasst werden.
-	"Zusatzinformation - Art 1",
-	"Zusatzinformation - Inhalt 1",
-	"Zusatzinformation - Art 2",
-	"Zusatzinformation - Inhalt 2",
-	"Zusatzinformation - Art 3",
-	"Zusatzinformation - Inhalt 3",
-	"Zusatzinformation - Art 4",
-	"Zusatzinformation - Inhalt 4",
-	"Zusatzinformation - Art 5",
-	"Zusatzinformation - Inhalt 5",
-	"Zusatzinformation - Art 6",
-	"Zusatzinformation - Inhalt 6",
-	"Zusatzinformation - Art 7",
-	"Zusatzinformation - Inhalt 7",
-	"Zusatzinformation - Art 8",
-	"Zusatzinformation - Inhalt 8",
-	"Zusatzinformation - Art 9",
-	"Zusatzinformation - Inhalt 9",
-	"Zusatzinformation - Art 10",
-	"Zusatzinformation - Inhalt 10",
-	"Zusatzinformation - Art 11",
-	"Zusatzinformation - Inhalt 11",
-	"Zusatzinformation - Art 12",
-	"Zusatzinformation - Inhalt 12",
-	"Zusatzinformation - Art 13",
-	"Zusatzinformation - Inhalt 13",
-	"Zusatzinformation - Art 14",
-	"Zusatzinformation - Inhalt 14",
-	"Zusatzinformation - Art 15",
-	"Zusatzinformation - Inhalt 15",
-	"Zusatzinformation - Art 16",
-	"Zusatzinformation - Inhalt 16",
-	"Zusatzinformation - Art 17",
-	"Zusatzinformation - Inhalt 17",
-	"Zusatzinformation - Art 18",
-	"Zusatzinformation - Inhalt 18",
-	"Zusatzinformation - Art 19",
-	"Zusatzinformation - Inhalt 19",
-	"Zusatzinformation - Art 20",
-	"Zusatzinformation - Inhalt 20",
-	# Wirkt sich nur bei Sachverhalt mit SKR 14 Land- und Forstwirtschaft aus,
-	# für andere SKR werden die Felder beim Import / Export überlesen bzw.
-	# leer exportiert.
-	"Stück",
-	"Gewicht",
-	# 1 = Lastschrift
-	# 2 = Mahnung
-	# 3 = Zahlung
-	"Zahlweise",
-	"Forderungsart",
-	# JJJJ
-	"Veranlagungsjahr",
-	# TTMMJJJJ
-	"Zugeordnete Fälligkeit",
-	# 1 = Einkauf von Waren
-	# 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen
-	"Skontotyp",
-	# Allgemeine Bezeichnung, des Auftrags / Projekts.
-	"Auftragsnummer",
-	# AA = Angeforderte Anzahlung / Abschlagsrechnung
-	# AG = Erhaltene Anzahlung (Geldeingang)
-	# AV = Erhaltene Anzahlung (Verbindlichkeit)
-	# SR = Schlussrechnung
-	# SU = Schlussrechnung (Umbuchung)
-	# SG = Schlussrechnung (Geldeingang)
-	# SO = Sonstige
-	"Buchungstyp",
-	"USt-Schlüssel (Anzahlungen)",
-	"EU-Mitgliedstaat (Anzahlungen)",
-	"Sachverhalt L+L (Anzahlungen)",
-	"EU-Steuersatz (Anzahlungen)",
-	"Erlöskonto (Anzahlungen)",
-	# Wird beim Import durch SV (Stapelverarbeitung) ersetzt.
-	"Herkunft-Kz",
-	# Wird von DATEV verwendet.
-	"Leerfeld",
-	# Format TTMMJJJJ
-	"KOST-Datum",
-	# Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats
-	# (z.B. Rechnungs- oder Kundennummer).
-	"SEPA-Mandatsreferenz",
-	# 1 = Skontosperre
-	# 0 = Keine Skontosperre
-	"Skontosperre",
-	# Gesellschafter und Sonderbilanzsachverhalt
-	"Gesellschaftername",
-	# Amtliche Nummer aus der Feststellungserklärung
-	"Beteiligtennummer",
-	"Identifikationsnummer",
-	"Zeichnernummer",
-	# Format TTMMJJJJ
-	"Postensperre bis",
-	# Gesellschafter und Sonderbilanzsachverhalt
-	"Bezeichnung SoBil-Sachverhalt",
-	"Kennzeichen SoBil-Buchung",
-	# 0 = keine Festschreibung
-	# 1 = Festschreibung
-	"Festschreibung",
-	# Format TTMMJJJJ
-	"Leistungsdatum",
-	# Format TTMMJJJJ
-	"Datum Zuord. Steuerperiode",
-	# OPOS-Informationen, Format TTMMJJJJ
-	"Fälligkeit",
-	# G oder 1 = Generalumkehr
-	# 0 = keine Generalumkehr
-	"Generalumkehr (GU)",
-	# Steuersatz für Steuerschlüssel
-	"Steuersatz",
-	# Beispiel: DE für Deutschland
-	"Land",
-]
-
-DEBTOR_CREDITOR_COLUMNS = [
-	# All possible columns must tbe listed here, because DATEV requires them to
-	# be present in the CSV.
-	# Columns "Leerfeld" have been replaced with "Leerfeld #" to not confuse pandas
-	# ---
-	"Konto",
-	"Name (Adressatentyp Unternehmen)",
-	"Unternehmensgegenstand",
-	"Name (Adressatentyp natürl. Person)",
-	"Vorname (Adressatentyp natürl. Person)",
-	"Name (Adressatentyp keine Angabe)",
-	"Adressatentyp",
-	"Kurzbezeichnung",
-	"EU-Land",
-	"EU-USt-IdNr.",
-	"Anrede",
-	"Titel/Akad. Grad",
-	"Adelstitel",
-	"Namensvorsatz",
-	"Adressart",
-	"Straße",
-	"Postfach",
-	"Postleitzahl",
-	"Ort",
-	"Land",
-	"Versandzusatz",
-	"Adresszusatz",
-	"Abweichende Anrede",
-	"Abw. Zustellbezeichnung 1",
-	"Abw. Zustellbezeichnung 2",
-	"Kennz. Korrespondenzadresse",
-	"Adresse gültig von",
-	"Adresse gültig bis",
-	"Telefon",
-	"Bemerkung (Telefon)",
-	"Telefon Geschäftsleitung",
-	"Bemerkung (Telefon GL)",
-	"E-Mail",
-	"Bemerkung (E-Mail)",
-	"Internet",
-	"Bemerkung (Internet)",
-	"Fax",
-	"Bemerkung (Fax)",
-	"Sonstige",
-	"Bemerkung (Sonstige)",
-	"Bankleitzahl 1",
-	"Bankbezeichnung 1",
-	"Bankkonto-Nummer 1",
-	"Länderkennzeichen 1",
-	"IBAN 1",
-	"Leerfeld 1",
-	"SWIFT-Code 1",
-	"Abw. Kontoinhaber 1",
-	"Kennz. Haupt-Bankverb. 1",
-	"Bankverb. 1 Gültig von",
-	"Bankverb. 1 Gültig bis",
-	"Bankleitzahl 2",
-	"Bankbezeichnung 2",
-	"Bankkonto-Nummer 2",
-	"Länderkennzeichen 2",
-	"IBAN 2",
-	"Leerfeld 2",
-	"SWIFT-Code 2",
-	"Abw. Kontoinhaber 2",
-	"Kennz. Haupt-Bankverb. 2",
-	"Bankverb. 2 gültig von",
-	"Bankverb. 2 gültig bis",
-	"Bankleitzahl 3",
-	"Bankbezeichnung 3",
-	"Bankkonto-Nummer 3",
-	"Länderkennzeichen 3",
-	"IBAN 3",
-	"Leerfeld 3",
-	"SWIFT-Code 3",
-	"Abw. Kontoinhaber 3",
-	"Kennz. Haupt-Bankverb. 3",
-	"Bankverb. 3 gültig von",
-	"Bankverb. 3 gültig bis",
-	"Bankleitzahl 4",
-	"Bankbezeichnung 4",
-	"Bankkonto-Nummer 4",
-	"Länderkennzeichen 4",
-	"IBAN 4",
-	"Leerfeld 4",
-	"SWIFT-Code 4",
-	"Abw. Kontoinhaber 4",
-	"Kennz. Haupt-Bankverb. 4",
-	"Bankverb. 4 Gültig von",
-	"Bankverb. 4 Gültig bis",
-	"Bankleitzahl 5",
-	"Bankbezeichnung 5",
-	"Bankkonto-Nummer 5",
-	"Länderkennzeichen 5",
-	"IBAN 5",
-	"Leerfeld 5",
-	"SWIFT-Code 5",
-	"Abw. Kontoinhaber 5",
-	"Kennz. Haupt-Bankverb. 5",
-	"Bankverb. 5 gültig von",
-	"Bankverb. 5 gültig bis",
-	"Leerfeld 6",
-	"Briefanrede",
-	"Grußformel",
-	"Kundennummer",
-	"Steuernummer",
-	"Sprache",
-	"Ansprechpartner",
-	"Vertreter",
-	"Sachbearbeiter",
-	"Diverse-Konto",
-	"Ausgabeziel",
-	"Währungssteuerung",
-	"Kreditlimit (Debitor)",
-	"Zahlungsbedingung",
-	"Fälligkeit in Tagen (Debitor)",
-	"Skonto in Prozent (Debitor)",
-	"Kreditoren-Ziel 1 (Tage)",
-	"Kreditoren-Skonto 1 (%)",
-	"Kreditoren-Ziel 2 (Tage)",
-	"Kreditoren-Skonto 2 (%)",
-	"Kreditoren-Ziel 3 Brutto (Tage)",
-	"Kreditoren-Ziel 4 (Tage)",
-	"Kreditoren-Skonto 4 (%)",
-	"Kreditoren-Ziel 5 (Tage)",
-	"Kreditoren-Skonto 5 (%)",
-	"Mahnung",
-	"Kontoauszug",
-	"Mahntext 1",
-	"Mahntext 2",
-	"Mahntext 3",
-	"Kontoauszugstext",
-	"Mahnlimit Betrag",
-	"Mahnlimit %",
-	"Zinsberechnung",
-	"Mahnzinssatz 1",
-	"Mahnzinssatz 2",
-	"Mahnzinssatz 3",
-	"Lastschrift",
-	"Verfahren",
-	"Mandantenbank",
-	"Zahlungsträger",
-	"Indiv. Feld 1",
-	"Indiv. Feld 2",
-	"Indiv. Feld 3",
-	"Indiv. Feld 4",
-	"Indiv. Feld 5",
-	"Indiv. Feld 6",
-	"Indiv. Feld 7",
-	"Indiv. Feld 8",
-	"Indiv. Feld 9",
-	"Indiv. Feld 10",
-	"Indiv. Feld 11",
-	"Indiv. Feld 12",
-	"Indiv. Feld 13",
-	"Indiv. Feld 14",
-	"Indiv. Feld 15",
-	"Abweichende Anrede (Rechnungsadresse)",
-	"Adressart (Rechnungsadresse)",
-	"Straße (Rechnungsadresse)",
-	"Postfach (Rechnungsadresse)",
-	"Postleitzahl (Rechnungsadresse)",
-	"Ort (Rechnungsadresse)",
-	"Land (Rechnungsadresse)",
-	"Versandzusatz (Rechnungsadresse)",
-	"Adresszusatz (Rechnungsadresse)",
-	"Abw. Zustellbezeichnung 1 (Rechnungsadresse)",
-	"Abw. Zustellbezeichnung 2 (Rechnungsadresse)",
-	"Adresse Gültig von (Rechnungsadresse)",
-	"Adresse Gültig bis (Rechnungsadresse)",
-	"Bankleitzahl 6",
-	"Bankbezeichnung 6",
-	"Bankkonto-Nummer 6",
-	"Länderkennzeichen 6",
-	"IBAN 6",
-	"Leerfeld 7",
-	"SWIFT-Code 6",
-	"Abw. Kontoinhaber 6",
-	"Kennz. Haupt-Bankverb. 6",
-	"Bankverb 6 gültig von",
-	"Bankverb 6 gültig bis",
-	"Bankleitzahl 7",
-	"Bankbezeichnung 7",
-	"Bankkonto-Nummer 7",
-	"Länderkennzeichen 7",
-	"IBAN 7",
-	"Leerfeld 8",
-	"SWIFT-Code 7",
-	"Abw. Kontoinhaber 7",
-	"Kennz. Haupt-Bankverb. 7",
-	"Bankverb 7 gültig von",
-	"Bankverb 7 gültig bis",
-	"Bankleitzahl 8",
-	"Bankbezeichnung 8",
-	"Bankkonto-Nummer 8",
-	"Länderkennzeichen 8",
-	"IBAN 8",
-	"Leerfeld 9",
-	"SWIFT-Code 8",
-	"Abw. Kontoinhaber 8",
-	"Kennz. Haupt-Bankverb. 8",
-	"Bankverb 8 gültig von",
-	"Bankverb 8 gültig bis",
-	"Bankleitzahl 9",
-	"Bankbezeichnung 9",
-	"Bankkonto-Nummer 9",
-	"Länderkennzeichen 9",
-	"IBAN 9",
-	"Leerfeld 10",
-	"SWIFT-Code 9",
-	"Abw. Kontoinhaber 9",
-	"Kennz. Haupt-Bankverb. 9",
-	"Bankverb 9 gültig von",
-	"Bankverb 9 gültig bis",
-	"Bankleitzahl 10",
-	"Bankbezeichnung 10",
-	"Bankkonto-Nummer 10",
-	"Länderkennzeichen 10",
-	"IBAN 10",
-	"Leerfeld 11",
-	"SWIFT-Code 10",
-	"Abw. Kontoinhaber 10",
-	"Kennz. Haupt-Bankverb. 10",
-	"Bankverb 10 gültig von",
-	"Bankverb 10 gültig bis",
-	"Nummer Fremdsystem",
-	"Insolvent",
-	"SEPA-Mandatsreferenz 1",
-	"SEPA-Mandatsreferenz 2",
-	"SEPA-Mandatsreferenz 3",
-	"SEPA-Mandatsreferenz 4",
-	"SEPA-Mandatsreferenz 5",
-	"SEPA-Mandatsreferenz 6",
-	"SEPA-Mandatsreferenz 7",
-	"SEPA-Mandatsreferenz 8",
-	"SEPA-Mandatsreferenz 9",
-	"SEPA-Mandatsreferenz 10",
-	"Verknüpftes OPOS-Konto",
-	"Mahnsperre bis",
-	"Lastschriftsperre bis",
-	"Zahlungssperre bis",
-	"Gebührenberechnung",
-	"Mahngebühr 1",
-	"Mahngebühr 2",
-	"Mahngebühr 3",
-	"Pauschalberechnung",
-	"Verzugspauschale 1",
-	"Verzugspauschale 2",
-	"Verzugspauschale 3",
-	"Alternativer Suchname",
-	"Status",
-	"Anschrift manuell geändert (Korrespondenzadresse)",
-	"Anschrift individuell (Korrespondenzadresse)",
-	"Anschrift manuell geändert (Rechnungsadresse)",
-	"Anschrift individuell (Rechnungsadresse)",
-	"Fristberechnung bei Debitor",
-	"Mahnfrist 1",
-	"Mahnfrist 2",
-	"Mahnfrist 3",
-	"Letzte Frist",
-]
-
-ACCOUNT_NAME_COLUMNS = [
-	# Account number
-	"Konto",
-	# Account name
-	"Kontenbeschriftung",
-	# Language of the account name
-	# "de-DE" or "en-GB"
-	"Sprach-ID",
-]
-
-
-class DataCategory:
-
-	"""Field of the CSV Header."""
-
-	DEBTORS_CREDITORS = "16"
-	ACCOUNT_NAMES = "20"
-	TRANSACTIONS = "21"
-	POSTING_TEXT_CONSTANTS = "67"
-
-
-class FormatName:
-
-	"""Field of the CSV Header, corresponds to DataCategory."""
-
-	DEBTORS_CREDITORS = "Debitoren/Kreditoren"
-	ACCOUNT_NAMES = "Kontenbeschriftungen"
-	TRANSACTIONS = "Buchungsstapel"
-	POSTING_TEXT_CONSTANTS = "Buchungstextkonstanten"
-
-
-class Transactions:
-	DATA_CATEGORY = DataCategory.TRANSACTIONS
-	FORMAT_NAME = FormatName.TRANSACTIONS
-	FORMAT_VERSION = "9"
-	COLUMNS = TRANSACTION_COLUMNS
-
-
-class DebtorsCreditors:
-	DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS
-	FORMAT_NAME = FormatName.DEBTORS_CREDITORS
-	FORMAT_VERSION = "5"
-	COLUMNS = DEBTOR_CREDITOR_COLUMNS
-
-
-class AccountNames:
-	DATA_CATEGORY = DataCategory.ACCOUNT_NAMES
-	FORMAT_NAME = FormatName.ACCOUNT_NAMES
-	FORMAT_VERSION = "2"
-	COLUMNS = ACCOUNT_NAME_COLUMNS
diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py
deleted file mode 100644
index d4e9c27..0000000
--- a/erpnext/regional/germany/utils/datev/datev_csv.py
+++ /dev/null
@@ -1,184 +0,0 @@
-import datetime
-import zipfile
-from csv import QUOTE_NONNUMERIC
-from io import BytesIO
-
-import frappe
-import pandas as pd
-from frappe import _
-
-from .datev_constants import DataCategory
-
-
-def get_datev_csv(data, filters, csv_class):
-	"""
-	Fill in missing columns and return a CSV in DATEV Format.
-
-	For automatic processing, DATEV requires the first line of the CSV file to
-	hold meta data such as the length of account numbers oder the category of
-	the data.
-
-	Arguments:
-	data -- array of dictionaries
-	filters -- dict
-	csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS
-	"""
-	empty_df = pd.DataFrame(columns=csv_class.COLUMNS)
-	data_df = pd.DataFrame.from_records(data)
-	result = empty_df.append(data_df, sort=True)
-
-	if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS:
-		result["Belegdatum"] = pd.to_datetime(result["Belegdatum"])
-
-		result["Beleginfo - Inhalt 6"] = pd.to_datetime(result["Beleginfo - Inhalt 6"])
-		result["Beleginfo - Inhalt 6"] = result["Beleginfo - Inhalt 6"].dt.strftime("%d%m%Y")
-
-		result["Fälligkeit"] = pd.to_datetime(result["Fälligkeit"])
-		result["Fälligkeit"] = result["Fälligkeit"].dt.strftime("%d%m%y")
-
-		result.sort_values(by="Belegdatum", inplace=True, kind="stable", ignore_index=True)
-
-	if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES:
-		result["Sprach-ID"] = "de-DE"
-
-	data = result.to_csv(
-		# Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035
-		sep=";",
-		# European decimal seperator
-		decimal=",",
-		# Windows "ANSI" encoding
-		encoding="latin_1",
-		# format date as DDMM
-		date_format="%d%m",
-		# Windows line terminator
-		line_terminator="\r\n",
-		# Do not number rows
-		index=False,
-		# Use all columns defined above
-		columns=csv_class.COLUMNS,
-		# Quote most fields, even currency values with "," separator
-		quoting=QUOTE_NONNUMERIC,
-	)
-
-	data = data.encode("latin_1", errors="replace")
-
-	header = get_header(filters, csv_class)
-	header = ";".join(header).encode("latin_1", errors="replace")
-
-	# 1st Row: Header with meta data
-	# 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here.
-	# 3rd - nth Row: Data (Nutzdaten)
-	return header + b"\r\n" + data
-
-
-def get_header(filters, csv_class):
-	description = filters.get("voucher_type", csv_class.FORMAT_NAME)
-	company = filters.get("company")
-	datev_settings = frappe.get_doc("DATEV Settings", {"client": company})
-	default_currency = frappe.get_value("Company", company, "default_currency")
-	coa = frappe.get_value("Company", company, "chart_of_accounts")
-	coa_short_code = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "")
-
-	header = [
-		# DATEV format
-		# 	"DTVF" = created by DATEV software,
-		# 	"EXTF" = created by other software
-		'"EXTF"',
-		# version of the DATEV format
-		# 	141 = 1.41,
-		# 	510 = 5.10,
-		# 	720 = 7.20
-		"700",
-		csv_class.DATA_CATEGORY,
-		'"%s"' % csv_class.FORMAT_NAME,
-		# Format version (regarding format name)
-		csv_class.FORMAT_VERSION,
-		# Generated on
-		datetime.datetime.now().strftime("%Y%m%d%H%M%S") + "000",
-		# Imported on -- stays empty
-		"",
-		# Origin. Any two symbols, will be replaced by "SV" on import.
-		'"EN"',
-		# I = Exported by
-		'"%s"' % frappe.session.user,
-		# J = Imported by -- stays empty
-		"",
-		# K = Tax consultant number (Beraternummer)
-		datev_settings.get("consultant_number", "0000000"),
-		# L = Tax client number (Mandantennummer)
-		datev_settings.get("client_number", "00000"),
-		# M = Start of the fiscal year (Wirtschaftsjahresbeginn)
-		frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"),
-		# N = Length of account numbers (Sachkontenlänge)
-		str(filters.get("account_number_length", 4)),
-		# O = Transaction batch start date (YYYYMMDD)
-		frappe.utils.formatdate(filters.get("from_date"), "yyyyMMdd")
-		if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS
-		else "",
-		# P = Transaction batch end date (YYYYMMDD)
-		frappe.utils.formatdate(filters.get("to_date"), "yyyyMMdd")
-		if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS
-		else "",
-		# Q = Description (for example, "Sales Invoice") Max. 30 chars
-		'"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "",
-		# R = Diktatkürzel
-		"",
-		# S = Buchungstyp
-		# 	1 = Transaction batch (Finanzbuchführung),
-		# 	2 = Annual financial statement (Jahresabschluss)
-		"1" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "",
-		# T = Rechnungslegungszweck
-		# 	0 oder leer = vom Rechnungslegungszweck unabhängig
-		# 	50 = Handelsrecht
-		# 	30 = Steuerrecht
-		# 	64 = IFRS
-		# 	40 = Kalkulatorik
-		# 	11 = Reserviert
-		# 	12 = Reserviert
-		"0" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "",
-		# U = Festschreibung
-		# TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1"
-		"0",
-		# V = Default currency, for example, "EUR"
-		'"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "",
-		# reserviert
-		"",
-		# Derivatskennzeichen
-		"",
-		# reserviert
-		"",
-		# reserviert
-		"",
-		# SKR
-		'"%s"' % coa_short_code,
-		# Branchen-Lösungs-ID
-		"",
-		# reserviert
-		"",
-		# reserviert
-		"",
-		# Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung)
-		"",
-	]
-	return header
-
-
-def zip_and_download(zip_filename, csv_files):
-	"""
-	Put CSV files in a zip archive and send that to the client.
-
-	Params:
-	zip_filename	Name of the zip file
-	csv_files		list of dicts [{'file_name': 'my_file.csv', 'csv_data': 'comma,separated,values'}]
-	"""
-	zip_buffer = BytesIO()
-
-	zip_file = zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED)
-	for csv_file in csv_files:
-		zip_file.writestr(csv_file.get("file_name"), csv_file.get("csv_data"))
-
-	zip_file.close()
-
-	frappe.response["filecontent"] = zip_buffer.getvalue()
-	frappe.response["filename"] = zip_filename
-	frappe.response["type"] = "binary"
diff --git a/erpnext/regional/report/datev/__init__.py b/erpnext/regional/report/datev/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/report/datev/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js
deleted file mode 100644
index 03c729e..0000000
--- a/erpnext/regional/report/datev/datev.js
+++ /dev/null
@@ -1,56 +0,0 @@
-frappe.query_reports["DATEV"] = {
-	"filters": [
-		{
-			"fieldname": "company",
-			"label": __("Company"),
-			"fieldtype": "Link",
-			"options": "Company",
-			"default": frappe.defaults.get_user_default("Company") || frappe.defaults.get_global_default("Company"),
-			"reqd": 1
-		},
-		{
-			"fieldname": "from_date",
-			"label": __("From Date"),
-			"default": moment().subtract(1, 'month').startOf('month').format(),
-			"fieldtype": "Date",
-			"reqd": 1
-		},
-		{
-			"fieldname": "to_date",
-			"label": __("To Date"),
-			"default": moment().subtract(1, 'month').endOf('month').format(),
-			"fieldtype": "Date",
-			"reqd": 1
-		},
-		{
-			"fieldname": "voucher_type",
-			"label": __("Voucher Type"),
-			"fieldtype": "Select",
-			"options": "\nSales Invoice\nPurchase Invoice\nPayment Entry\nExpense Claim\nPayroll Entry\nBank Reconciliation\nAsset\nStock Entry"
-		}
-	],
-	onload: function(query_report) {
-		let company = frappe.query_report.get_filter_value('company');
-		frappe.db.exists('DATEV Settings', company).then((settings_exist) => {
-			if (!settings_exist) {
-				frappe.confirm(__('DATEV Settings for your Company are missing. Would you like to create them now?'),
-					() => frappe.new_doc('DATEV Settings', {'company': company})
-				);
-			}
-		});
-
-		query_report.page.add_menu_item(__("Download DATEV File"), () => {
-			const filters = encodeURIComponent(
-				JSON.stringify(
-					query_report.get_values()
-				)
-			);
-			window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`);
-		});
-
-		query_report.page.add_menu_item(__("Change DATEV Settings"), () => {
-			let company = frappe.query_report.get_filter_value('company'); // read company from filters again – it might have changed by now.
-			frappe.set_route('Form', 'DATEV Settings', company);
-		});
-	}
-};
diff --git a/erpnext/regional/report/datev/datev.json b/erpnext/regional/report/datev/datev.json
deleted file mode 100644
index 94e3960..0000000
--- a/erpnext/regional/report/datev/datev.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "add_total_row": 0,
- "columns": [],
- "creation": "2019-04-24 08:45:16.650129",
- "disable_prepared_report": 0,
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "filters": [],
- "idx": 0,
- "is_standard": "Yes",
- "modified": "2021-04-06 12:23:00.379517",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "DATEV",
- "owner": "Administrator",
- "prepared_report": 0,
- "ref_doctype": "GL Entry",
- "report_name": "DATEV",
- "report_type": "Script Report",
- "roles": []
-}
\ No newline at end of file
diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
deleted file mode 100644
index 2d888a8..0000000
--- a/erpnext/regional/report/datev/datev.py
+++ /dev/null
@@ -1,570 +0,0 @@
-"""
-Provide a report and downloadable CSV according to the German DATEV format.
-
-- Query report showing only the columns that contain data, formatted nicely for
-	dispay to the user.
-- CSV download functionality `download_datev_csv` that provides a CSV file with
-	all required columns. Used to import the data into the DATEV Software.
-"""
-
-import json
-
-import frappe
-from frappe import _
-
-from erpnext.accounts.utils import get_fiscal_year
-from erpnext.regional.germany.utils.datev.datev_constants import (
-	AccountNames,
-	DebtorsCreditors,
-	Transactions,
-)
-from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, zip_and_download
-
-COLUMNS = [
-	{
-		"label": "Umsatz (ohne Soll/Haben-Kz)",
-		"fieldname": "Umsatz (ohne Soll/Haben-Kz)",
-		"fieldtype": "Currency",
-		"width": 100,
-	},
-	{
-		"label": "Soll/Haben-Kennzeichen",
-		"fieldname": "Soll/Haben-Kennzeichen",
-		"fieldtype": "Data",
-		"width": 100,
-	},
-	{"label": "Konto", "fieldname": "Konto", "fieldtype": "Data", "width": 100},
-	{
-		"label": "Gegenkonto (ohne BU-Schlüssel)",
-		"fieldname": "Gegenkonto (ohne BU-Schlüssel)",
-		"fieldtype": "Data",
-		"width": 100,
-	},
-	{"label": "BU-Schlüssel", "fieldname": "BU-Schlüssel", "fieldtype": "Data", "width": 100},
-	{"label": "Belegdatum", "fieldname": "Belegdatum", "fieldtype": "Date", "width": 100},
-	{"label": "Belegfeld 1", "fieldname": "Belegfeld 1", "fieldtype": "Data", "width": 150},
-	{"label": "Buchungstext", "fieldname": "Buchungstext", "fieldtype": "Text", "width": 300},
-	{
-		"label": "Beleginfo - Art 1",
-		"fieldname": "Beleginfo - Art 1",
-		"fieldtype": "Link",
-		"options": "DocType",
-		"width": 100,
-	},
-	{
-		"label": "Beleginfo - Inhalt 1",
-		"fieldname": "Beleginfo - Inhalt 1",
-		"fieldtype": "Dynamic Link",
-		"options": "Beleginfo - Art 1",
-		"width": 150,
-	},
-	{
-		"label": "Beleginfo - Art 2",
-		"fieldname": "Beleginfo - Art 2",
-		"fieldtype": "Link",
-		"options": "DocType",
-		"width": 100,
-	},
-	{
-		"label": "Beleginfo - Inhalt 2",
-		"fieldname": "Beleginfo - Inhalt 2",
-		"fieldtype": "Dynamic Link",
-		"options": "Beleginfo - Art 2",
-		"width": 150,
-	},
-	{
-		"label": "Beleginfo - Art 3",
-		"fieldname": "Beleginfo - Art 3",
-		"fieldtype": "Link",
-		"options": "DocType",
-		"width": 100,
-	},
-	{
-		"label": "Beleginfo - Inhalt 3",
-		"fieldname": "Beleginfo - Inhalt 3",
-		"fieldtype": "Dynamic Link",
-		"options": "Beleginfo - Art 3",
-		"width": 150,
-	},
-	{
-		"label": "Beleginfo - Art 4",
-		"fieldname": "Beleginfo - Art 4",
-		"fieldtype": "Data",
-		"width": 100,
-	},
-	{
-		"label": "Beleginfo - Inhalt 4",
-		"fieldname": "Beleginfo - Inhalt 4",
-		"fieldtype": "Data",
-		"width": 150,
-	},
-	{
-		"label": "Beleginfo - Art 5",
-		"fieldname": "Beleginfo - Art 5",
-		"fieldtype": "Data",
-		"width": 150,
-	},
-	{
-		"label": "Beleginfo - Inhalt 5",
-		"fieldname": "Beleginfo - Inhalt 5",
-		"fieldtype": "Data",
-		"width": 100,
-	},
-	{
-		"label": "Beleginfo - Art 6",
-		"fieldname": "Beleginfo - Art 6",
-		"fieldtype": "Data",
-		"width": 150,
-	},
-	{
-		"label": "Beleginfo - Inhalt 6",
-		"fieldname": "Beleginfo - Inhalt 6",
-		"fieldtype": "Date",
-		"width": 100,
-	},
-	{"label": "Fälligkeit", "fieldname": "Fälligkeit", "fieldtype": "Date", "width": 100},
-]
-
-
-def execute(filters=None):
-	"""Entry point for frappe."""
-	data = []
-	if filters and validate(filters):
-		fn = "temporary_against_account_number"
-		filters[fn] = frappe.get_value("DATEV Settings", filters.get("company"), fn)
-		data = get_transactions(filters, as_dict=0)
-
-	return COLUMNS, data
-
-
-def validate(filters):
-	"""Make sure all mandatory filters and settings are present."""
-	company = filters.get("company")
-	if not company:
-		frappe.throw(_("<b>Company</b> is a mandatory filter."))
-
-	from_date = filters.get("from_date")
-	if not from_date:
-		frappe.throw(_("<b>From Date</b> is a mandatory filter."))
-
-	to_date = filters.get("to_date")
-	if not to_date:
-		frappe.throw(_("<b>To Date</b> is a mandatory filter."))
-
-	validate_fiscal_year(from_date, to_date, company)
-
-	if not frappe.db.exists("DATEV Settings", filters.get("company")):
-		msg = "Please create DATEV Settings for Company {}".format(filters.get("company"))
-		frappe.log_error(msg, title="DATEV Settings missing")
-		return False
-
-	return True
-
-
-def validate_fiscal_year(from_date, to_date, company):
-	from_fiscal_year = get_fiscal_year(date=from_date, company=company)
-	to_fiscal_year = get_fiscal_year(date=to_date, company=company)
-	if from_fiscal_year != to_fiscal_year:
-		frappe.throw(_("Dates {} and {} are not in the same fiscal year.").format(from_date, to_date))
-
-
-def get_transactions(filters, as_dict=1):
-	def run(params_method, filters):
-		extra_fields, extra_joins, extra_filters = params_method(filters)
-		return run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=as_dict)
-
-	def sort_by(row):
-		# "Belegdatum" is in the fifth column when list format is used
-		return row["Belegdatum" if as_dict else 5]
-
-	type_map = {
-		# specific query methods for some voucher types
-		"Payment Entry": get_payment_entry_params,
-		"Sales Invoice": get_sales_invoice_params,
-		"Purchase Invoice": get_purchase_invoice_params,
-	}
-
-	only_voucher_type = filters.get("voucher_type")
-	transactions = []
-
-	for voucher_type, get_voucher_params in type_map.items():
-		if only_voucher_type and only_voucher_type != voucher_type:
-			continue
-
-		transactions.extend(run(params_method=get_voucher_params, filters=filters))
-
-	if not only_voucher_type or only_voucher_type not in type_map:
-		# generic query method for all other voucher types
-		filters["exclude_voucher_types"] = type_map.keys()
-		transactions.extend(run(params_method=get_generic_params, filters=filters))
-
-	return sorted(transactions, key=sort_by)
-
-
-def get_payment_entry_params(filters):
-	extra_fields = """
-		, 'Zahlungsreferenz' as 'Beleginfo - Art 5'
-		, pe.reference_no as 'Beleginfo - Inhalt 5'
-		, 'Buchungstag' as 'Beleginfo - Art 6'
-		, pe.reference_date as 'Beleginfo - Inhalt 6'
-		, '' as 'Fälligkeit'
-	"""
-
-	extra_joins = """
-		LEFT JOIN `tabPayment Entry` pe
-		ON gl.voucher_no = pe.name
-	"""
-
-	extra_filters = """
-		AND gl.voucher_type = 'Payment Entry'
-	"""
-
-	return extra_fields, extra_joins, extra_filters
-
-
-def get_sales_invoice_params(filters):
-	extra_fields = """
-		, '' as 'Beleginfo - Art 5'
-		, '' as 'Beleginfo - Inhalt 5'
-		, '' as 'Beleginfo - Art 6'
-		, '' as 'Beleginfo - Inhalt 6'
-		, si.due_date as 'Fälligkeit'
-	"""
-
-	extra_joins = """
-		LEFT JOIN `tabSales Invoice` si
-		ON gl.voucher_no = si.name
-	"""
-
-	extra_filters = """
-		AND gl.voucher_type = 'Sales Invoice'
-	"""
-
-	return extra_fields, extra_joins, extra_filters
-
-
-def get_purchase_invoice_params(filters):
-	extra_fields = """
-		, 'Lieferanten-Rechnungsnummer' as 'Beleginfo - Art 5'
-		, pi.bill_no as 'Beleginfo - Inhalt 5'
-		, 'Lieferanten-Rechnungsdatum' as 'Beleginfo - Art 6'
-		, pi.bill_date as 'Beleginfo - Inhalt 6'
-		, pi.due_date as 'Fälligkeit'
-	"""
-
-	extra_joins = """
-		LEFT JOIN `tabPurchase Invoice` pi
-		ON gl.voucher_no = pi.name
-	"""
-
-	extra_filters = """
-		AND gl.voucher_type = 'Purchase Invoice'
-	"""
-
-	return extra_fields, extra_joins, extra_filters
-
-
-def get_generic_params(filters):
-	# produce empty fields so all rows will have the same length
-	extra_fields = """
-		, '' as 'Beleginfo - Art 5'
-		, '' as 'Beleginfo - Inhalt 5'
-		, '' as 'Beleginfo - Art 6'
-		, '' as 'Beleginfo - Inhalt 6'
-		, '' as 'Fälligkeit'
-	"""
-	extra_joins = ""
-
-	if filters.get("exclude_voucher_types"):
-		# exclude voucher types that are queried by a dedicated method
-		exclude = "({})".format(
-			", ".join("'{}'".format(key) for key in filters.get("exclude_voucher_types"))
-		)
-		extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude)
-
-	# if voucher type filter is set, allow only this type
-	if filters.get("voucher_type"):
-		extra_filters += " AND gl.voucher_type = %(voucher_type)s"
-
-	return extra_fields, extra_joins, extra_filters
-
-
-def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1):
-	"""
-	Get a list of accounting entries.
-
-	Select GL Entries joined with Account and Party Account in order to get the
-	account numbers. Returns a list of accounting entries.
-
-	Arguments:
-	filters -- dict of filters to be passed to the sql query
-	as_dict -- return as list of dicts [0,1]
-	"""
-	query = """
-		SELECT
-
-			/* either debit or credit amount; always positive */
-			case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)',
-
-			/* 'H' when credit, 'S' when debit */
-			case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen',
-
-			/* account number or, if empty, party account number */
-			acc.account_number as 'Konto',
-
-			/* against number or, if empty, party against number */
-			%(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
-
-			'' as 'BU-Schlüssel',
-
-			gl.posting_date as 'Belegdatum',
-			gl.voucher_no as 'Belegfeld 1',
-			REPLACE(LEFT(gl.remarks, 60), '\n', ' ') as 'Buchungstext',
-			gl.voucher_type as 'Beleginfo - Art 1',
-			gl.voucher_no as 'Beleginfo - Inhalt 1',
-			gl.against_voucher_type as 'Beleginfo - Art 2',
-			gl.against_voucher as 'Beleginfo - Inhalt 2',
-			gl.party_type as 'Beleginfo - Art 3',
-			gl.party as 'Beleginfo - Inhalt 3',
-			case gl.party_type when 'Customer' then 'Debitorennummer' when 'Supplier' then 'Kreditorennummer' else NULL end as 'Beleginfo - Art 4',
-			par.debtor_creditor_number as 'Beleginfo - Inhalt 4'
-
-			{extra_fields}
-
-		FROM `tabGL Entry` gl
-
-			/* Kontonummer */
-			LEFT JOIN `tabAccount` acc
-			ON gl.account = acc.name
-
-			LEFT JOIN `tabParty Account` par
-			ON par.parent = gl.party
-			AND par.parenttype = gl.party_type
-			AND par.company = %(company)s
-
-			{extra_joins}
-
-		WHERE gl.company = %(company)s
-		AND DATE(gl.posting_date) >= %(from_date)s
-		AND DATE(gl.posting_date) <= %(to_date)s
-
-		{extra_filters}
-
-		ORDER BY 'Belegdatum', gl.voucher_no""".format(
-		extra_fields=extra_fields, extra_joins=extra_joins, extra_filters=extra_filters
-	)
-
-	gl_entries = frappe.db.sql(query, filters, as_dict=as_dict)
-
-	return gl_entries
-
-
-def get_customers(filters):
-	"""
-	Get a list of Customers.
-
-	Arguments:
-	filters -- dict of filters to be passed to the sql query
-	"""
-	return frappe.db.sql(
-		"""
-		SELECT
-
-			par.debtor_creditor_number as 'Konto',
-			CASE cus.customer_type
-				WHEN 'Company' THEN cus.customer_name
-				ELSE null
-				END as 'Name (Adressatentyp Unternehmen)',
-			CASE cus.customer_type
-				WHEN 'Individual' THEN TRIM(SUBSTR(cus.customer_name, LOCATE(' ', cus.customer_name)))
-				ELSE null
-				END as 'Name (Adressatentyp natürl. Person)',
-			CASE cus.customer_type
-				WHEN 'Individual' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(cus.customer_name, ' ', 1), ' ', -1)
-				ELSE null
-				END as 'Vorname (Adressatentyp natürl. Person)',
-			CASE cus.customer_type
-				WHEN 'Individual' THEN '1'
-				WHEN 'Company' THEN '2'
-				ELSE '0'
-				END as 'Adressatentyp',
-			adr.address_line1 as 'Straße',
-			adr.pincode as 'Postleitzahl',
-			adr.city as 'Ort',
-			UPPER(country.code) as 'Land',
-			adr.address_line2 as 'Adresszusatz',
-			adr.email_id as 'E-Mail',
-			adr.phone as 'Telefon',
-			adr.fax as 'Fax',
-			cus.website as 'Internet',
-			cus.tax_id as 'Steuernummer'
-
-		FROM `tabCustomer` cus
-
-			left join `tabParty Account` par
-			on par.parent = cus.name
-			and par.parenttype = 'Customer'
-			and par.company = %(company)s
-
-			left join `tabDynamic Link` dyn_adr
-			on dyn_adr.link_name = cus.name
-			and dyn_adr.link_doctype = 'Customer'
-			and dyn_adr.parenttype = 'Address'
-
-			left join `tabAddress` adr
-			on adr.name = dyn_adr.parent
-			and adr.is_primary_address = '1'
-
-			left join `tabCountry` country
-			on country.name = adr.country
-
-		WHERE adr.is_primary_address = '1'
-		""",
-		filters,
-		as_dict=1,
-	)
-
-
-def get_suppliers(filters):
-	"""
-	Get a list of Suppliers.
-
-	Arguments:
-	filters -- dict of filters to be passed to the sql query
-	"""
-	return frappe.db.sql(
-		"""
-		SELECT
-
-			par.debtor_creditor_number as 'Konto',
-			CASE sup.supplier_type
-				WHEN 'Company' THEN sup.supplier_name
-				ELSE null
-				END as 'Name (Adressatentyp Unternehmen)',
-			CASE sup.supplier_type
-				WHEN 'Individual' THEN TRIM(SUBSTR(sup.supplier_name, LOCATE(' ', sup.supplier_name)))
-				ELSE null
-				END as 'Name (Adressatentyp natürl. Person)',
-			CASE sup.supplier_type
-				WHEN 'Individual' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(sup.supplier_name, ' ', 1), ' ', -1)
-				ELSE null
-				END as 'Vorname (Adressatentyp natürl. Person)',
-			CASE sup.supplier_type
-				WHEN 'Individual' THEN '1'
-				WHEN 'Company' THEN '2'
-				ELSE '0'
-				END as 'Adressatentyp',
-			adr.address_line1 as 'Straße',
-			adr.pincode as 'Postleitzahl',
-			adr.city as 'Ort',
-			UPPER(country.code) as 'Land',
-			adr.address_line2 as 'Adresszusatz',
-			adr.email_id as 'E-Mail',
-			adr.phone as 'Telefon',
-			adr.fax as 'Fax',
-			sup.website as 'Internet',
-			sup.tax_id as 'Steuernummer',
-			case sup.on_hold when 1 then sup.release_date else null end as 'Zahlungssperre bis'
-
-		FROM `tabSupplier` sup
-
-			left join `tabParty Account` par
-			on par.parent = sup.name
-			and par.parenttype = 'Supplier'
-			and par.company = %(company)s
-
-			left join `tabDynamic Link` dyn_adr
-			on dyn_adr.link_name = sup.name
-			and dyn_adr.link_doctype = 'Supplier'
-			and dyn_adr.parenttype = 'Address'
-
-			left join `tabAddress` adr
-			on adr.name = dyn_adr.parent
-			and adr.is_primary_address = '1'
-
-			left join `tabCountry` country
-			on country.name = adr.country
-
-		WHERE adr.is_primary_address = '1'
-		""",
-		filters,
-		as_dict=1,
-	)
-
-
-def get_account_names(filters):
-	return frappe.db.sql(
-		"""
-		SELECT
-
-			account_number as 'Konto',
-			LEFT(account_name, 40) as 'Kontenbeschriftung',
-			'de-DE' as 'Sprach-ID'
-
-		FROM `tabAccount`
-		WHERE company = %(company)s
-		AND is_group = 0
-		AND account_number != ''
-	""",
-		filters,
-		as_dict=1,
-	)
-
-
-@frappe.whitelist()
-def download_datev_csv(filters):
-	"""
-	Provide accounting entries for download in DATEV format.
-
-	Validate the filters, get the data, produce the CSV file and provide it for
-	download. Can be called like this:
-
-	GET /api/method/erpnext.regional.report.datev.datev.download_datev_csv
-
-	Arguments / Params:
-	filters -- dict of filters to be passed to the sql query
-	"""
-	if isinstance(filters, str):
-		filters = json.loads(filters)
-
-	validate(filters)
-	company = filters.get("company")
-
-	fiscal_year = get_fiscal_year(date=filters.get("from_date"), company=company)
-	filters["fiscal_year_start"] = fiscal_year[1]
-
-	# set chart of accounts used
-	coa = frappe.get_value("Company", company, "chart_of_accounts")
-	filters["skr"] = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "")
-
-	datev_settings = frappe.get_doc("DATEV Settings", company)
-	filters["account_number_length"] = datev_settings.account_number_length
-	filters["temporary_against_account_number"] = datev_settings.temporary_against_account_number
-
-	transactions = get_transactions(filters)
-	account_names = get_account_names(filters)
-	customers = get_customers(filters)
-	suppliers = get_suppliers(filters)
-
-	zip_name = "{} DATEV.zip".format(frappe.utils.datetime.date.today())
-	zip_and_download(
-		zip_name,
-		[
-			{
-				"file_name": "EXTF_Buchungsstapel.csv",
-				"csv_data": get_datev_csv(transactions, filters, csv_class=Transactions),
-			},
-			{
-				"file_name": "EXTF_Kontenbeschriftungen.csv",
-				"csv_data": get_datev_csv(account_names, filters, csv_class=AccountNames),
-			},
-			{
-				"file_name": "EXTF_Kunden.csv",
-				"csv_data": get_datev_csv(customers, filters, csv_class=DebtorsCreditors),
-			},
-			{
-				"file_name": "EXTF_Lieferanten.csv",
-				"csv_data": get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors),
-			},
-		],
-	)
diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py
deleted file mode 100644
index 0df8c06..0000000
--- a/erpnext/regional/report/datev/test_datev.py
+++ /dev/null
@@ -1,252 +0,0 @@
-import zipfile
-from io import BytesIO
-from unittest import TestCase
-
-import frappe
-from frappe.utils import cstr, now_datetime, today
-
-from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-from erpnext.regional.germany.utils.datev.datev_constants import (
-	AccountNames,
-	DebtorsCreditors,
-	Transactions,
-)
-from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, get_header
-from erpnext.regional.report.datev.datev import (
-	download_datev_csv,
-	get_account_names,
-	get_customers,
-	get_suppliers,
-	get_transactions,
-)
-
-
-def make_company(company_name, abbr):
-	if not frappe.db.exists("Company", company_name):
-		company = frappe.get_doc(
-			{
-				"doctype": "Company",
-				"company_name": company_name,
-				"abbr": abbr,
-				"default_currency": "EUR",
-				"country": "Germany",
-				"create_chart_of_accounts_based_on": "Standard Template",
-				"chart_of_accounts": "SKR04 mit Kontonummern",
-			}
-		)
-		company.insert()
-	else:
-		company = frappe.get_doc("Company", company_name)
-
-	# indempotent
-	company.create_default_warehouses()
-
-	if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}):
-		company.create_default_cost_center()
-
-	company.save()
-	return company
-
-
-def setup_fiscal_year():
-	fiscal_year = None
-	year = cstr(now_datetime().year)
-	if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"):
-		try:
-			fiscal_year = frappe.get_doc(
-				{
-					"doctype": "Fiscal Year",
-					"year": year,
-					"year_start_date": "{0}-01-01".format(year),
-					"year_end_date": "{0}-12-31".format(year),
-				}
-			)
-			fiscal_year.insert()
-		except frappe.NameError:
-			pass
-
-	if fiscal_year:
-		fiscal_year.set_as_default()
-
-
-def make_customer_with_account(customer_name, company):
-	acc_name = frappe.db.get_value(
-		"Account", {"account_name": customer_name, "company": company.name}, "name"
-	)
-
-	if not acc_name:
-		acc = frappe.get_doc(
-			{
-				"doctype": "Account",
-				"parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG",
-				"account_name": customer_name,
-				"company": company.name,
-				"account_type": "Receivable",
-				"account_number": "10001",
-			}
-		)
-		acc.insert()
-		acc_name = acc.name
-
-	if not frappe.db.exists("Customer", customer_name):
-		customer = frappe.get_doc(
-			{
-				"doctype": "Customer",
-				"customer_name": customer_name,
-				"customer_type": "Company",
-				"accounts": [{"company": company.name, "account": acc_name}],
-			}
-		)
-		customer.insert()
-	else:
-		customer = frappe.get_doc("Customer", customer_name)
-
-	return customer
-
-
-def make_item(item_code, company):
-	warehouse_name = frappe.db.get_value(
-		"Warehouse", {"warehouse_name": "Stores", "company": company.name}, "name"
-	)
-
-	if not frappe.db.exists("Item", item_code):
-		item = frappe.get_doc(
-			{
-				"doctype": "Item",
-				"item_code": item_code,
-				"item_name": item_code,
-				"description": item_code,
-				"item_group": "All Item Groups",
-				"is_stock_item": 0,
-				"is_purchase_item": 0,
-				"is_customer_provided_item": 0,
-				"item_defaults": [{"default_warehouse": warehouse_name, "company": company.name}],
-			}
-		)
-		item.insert()
-	else:
-		item = frappe.get_doc("Item", item_code)
-	return item
-
-
-def make_datev_settings(company):
-	if not frappe.db.exists("DATEV Settings", company.name):
-		frappe.get_doc(
-			{
-				"doctype": "DATEV Settings",
-				"client": company.name,
-				"client_number": "12345",
-				"consultant_number": "67890",
-				"temporary_against_account_number": "9999",
-			}
-		).insert()
-
-
-class TestDatev(TestCase):
-	def setUp(self):
-		self.company = make_company("_Test GmbH", "_TG")
-		self.customer = make_customer_with_account("_Test Kunde GmbH", self.company)
-		self.filters = {
-			"company": self.company.name,
-			"from_date": today(),
-			"to_date": today(),
-			"temporary_against_account_number": "9999",
-		}
-
-		make_datev_settings(self.company)
-		item = make_item("_Test Item", self.company)
-		setup_fiscal_year()
-
-		warehouse = frappe.db.get_value(
-			"Item Default", {"parent": item.name, "company": self.company.name}, "default_warehouse"
-		)
-
-		income_account = frappe.db.get_value(
-			"Account", {"account_number": "4200", "company": self.company.name}, "name"
-		)
-
-		tax_account = frappe.db.get_value(
-			"Account", {"account_number": "3806", "company": self.company.name}, "name"
-		)
-
-		si = create_sales_invoice(
-			company=self.company.name,
-			customer=self.customer.name,
-			currency=self.company.default_currency,
-			debit_to=self.customer.accounts[0].account,
-			income_account="4200 - Erlöse - _TG",
-			expense_account="6990 - Herstellungskosten - _TG",
-			cost_center=self.company.cost_center,
-			warehouse=warehouse,
-			item=item.name,
-			do_not_save=1,
-		)
-
-		si.append(
-			"taxes",
-			{
-				"charge_type": "On Net Total",
-				"account_head": tax_account,
-				"description": "Umsatzsteuer 19 %",
-				"rate": 19,
-				"cost_center": self.company.cost_center,
-			},
-		)
-
-		si.cost_center = self.company.cost_center
-
-		si.save()
-		si.submit()
-
-	def test_columns(self):
-		def is_subset(get_data, allowed_keys):
-			"""
-			Validate that the dict contains only allowed keys.
-
-			Params:
-			get_data -- Function that returns a list of dicts.
-			allowed_keys -- List of allowed keys
-			"""
-			data = get_data(self.filters)
-			if data == []:
-				# No data and, therefore, no columns is okay
-				return True
-			actual_set = set(data[0].keys())
-			# allowed set must be interpreted as unicode to match the actual set
-			allowed_set = set({frappe.as_unicode(key) for key in allowed_keys})
-			return actual_set.issubset(allowed_set)
-
-		self.assertTrue(is_subset(get_transactions, Transactions.COLUMNS))
-		self.assertTrue(is_subset(get_customers, DebtorsCreditors.COLUMNS))
-		self.assertTrue(is_subset(get_suppliers, DebtorsCreditors.COLUMNS))
-		self.assertTrue(is_subset(get_account_names, AccountNames.COLUMNS))
-
-	def test_header(self):
-		self.assertTrue(Transactions.DATA_CATEGORY in get_header(self.filters, Transactions))
-		self.assertTrue(AccountNames.DATA_CATEGORY in get_header(self.filters, AccountNames))
-		self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors))
-
-	def test_csv(self):
-		test_data = [
-			{
-				"Umsatz (ohne Soll/Haben-Kz)": 100,
-				"Soll/Haben-Kennzeichen": "H",
-				"Kontonummer": "4200",
-				"Gegenkonto (ohne BU-Schlüssel)": "10000",
-				"Belegdatum": today(),
-				"Buchungstext": "No remark",
-				"Beleginfo - Art 1": "Sales Invoice",
-				"Beleginfo - Inhalt 1": "SINV-0001",
-			}
-		]
-		get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions)
-
-	def test_download(self):
-		"""Assert that the returned file is a ZIP file."""
-		download_datev_csv(self.filters)
-
-		# zipfile.is_zipfile() expects a file-like object
-		zip_buffer = BytesIO()
-		zip_buffer.write(frappe.response["filecontent"])
-
-		self.assertTrue(zipfile.is_zipfile(zip_buffer))
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index dd185fc..0de5b2d 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -233,7 +233,8 @@
 		["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
 		["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
 		["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
-		["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}]
+		["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}],
+		["default_provisional_account", {"root_type": ["in", ["Liability", "Asset"]]}]
 	], function(i, v) {
 		erpnext.company.set_custom_query(frm, v);
 	});
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 23301a6..ae8b488 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -377,6 +377,17 @@
 			}
 		}
 
+		frm.set_query('default_provisional_account', 'item_defaults', (doc, cdt, cdn) => {
+			let row = locals[cdt][cdn];
+			return {
+				filters: {
+					"company": row.company,
+					"root_type": ["in", ["Liability", "Asset"]],
+					"is_group": 0
+				}
+			};
+		});
+
 	},
 
 	make_dashboard: function(frm) {
diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json
index bc17160..042d398 100644
--- a/erpnext/stock/doctype/item_default/item_default.json
+++ b/erpnext/stock/doctype/item_default/item_default.json
@@ -15,6 +15,7 @@
   "default_supplier",
   "column_break_8",
   "expense_account",
+  "default_provisional_account",
   "selling_defaults",
   "selling_cost_center",
   "column_break_12",
@@ -101,11 +102,17 @@
    "fieldtype": "Link",
    "label": "Default Discount Account",
    "options": "Account"
+  },
+  {
+   "fieldname": "default_provisional_account",
+   "fieldtype": "Link",
+   "label": "Default Provisional Account",
+   "options": "Account"
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2021-07-13 01:26:03.860065",
+ "modified": "2022-04-10 20:18:54.148195",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item Default",
@@ -114,5 +121,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 6e5f6f5..19c490d 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -106,8 +106,6 @@
   "terms",
   "bill_no",
   "bill_date",
-  "accounting_details_section",
-  "provisional_expense_account",
   "more_info",
   "project",
   "status",
@@ -1145,26 +1143,13 @@
    "label": "Represents Company",
    "options": "Company",
    "read_only": 1
-  },
-  {
-   "collapsible": 1,
-   "fieldname": "accounting_details_section",
-   "fieldtype": "Section Break",
-   "label": "Accounting Details"
-  },
-  {
-   "fieldname": "provisional_expense_account",
-   "fieldtype": "Link",
-   "hidden": 1,
-   "label": "Provisional Expense Account",
-   "options": "Account"
   }
  ],
  "icon": "fa fa-truck",
  "idx": 261,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-03-10 11:40:52.690984",
+ "modified": "2022-04-10 22:50:37.761362",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 1e1c0b9..ec0e809 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -145,10 +145,13 @@
 			)
 		)
 
-		if provisional_accounting_for_non_stock_items:
-			default_provisional_account = self.get_company_default("default_provisional_account")
-			if not self.provisional_expense_account:
-				self.provisional_expense_account = default_provisional_account
+		if not provisional_accounting_for_non_stock_items:
+			return
+
+		default_provisional_account = self.get_company_default("default_provisional_account")
+		for item in self.get("items"):
+			if not item.get("provisional_expense_account"):
+				item.provisional_expense_account = default_provisional_account
 
 	def validate_with_previous_doc(self):
 		super(PurchaseReceipt, self).validate_with_previous_doc(
@@ -509,7 +512,9 @@
 				and flt(d.qty)
 				and provisional_accounting_for_non_stock_items
 			):
-				self.add_provisional_gl_entry(d, gl_entries, self.posting_date)
+				self.add_provisional_gl_entry(
+					d, gl_entries, self.posting_date, d.get("provisional_expense_account")
+				)
 
 		if warehouse_with_no_account:
 			frappe.msgprint(
@@ -518,9 +523,10 @@
 				+ "\n".join(warehouse_with_no_account)
 			)
 
-	def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0):
-		provisional_expense_account = self.get("provisional_expense_account")
-		credit_currency = get_account_currency(provisional_expense_account)
+	def add_provisional_gl_entry(
+		self, item, gl_entries, posting_date, provisional_account, reverse=0
+	):
+		credit_currency = get_account_currency(provisional_account)
 		debit_currency = get_account_currency(item.expense_account)
 		expense_account = item.expense_account
 		remarks = self.get("remarks") or _("Accounting Entry for Service")
@@ -534,7 +540,7 @@
 
 		self.add_gl_entry(
 			gl_entries=gl_entries,
-			account=provisional_expense_account,
+			account=provisional_account,
 			cost_center=item.cost_center,
 			debit=0.0,
 			credit=multiplication_factor * item.amount,
@@ -554,7 +560,7 @@
 			debit=multiplication_factor * item.amount,
 			credit=0.0,
 			remarks=remarks,
-			against_account=provisional_expense_account,
+			against_account=provisional_account,
 			account_currency=debit_currency,
 			project=item.project,
 			voucher_detail_no=item.name,
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index f3faba4..ce3bd56 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -747,14 +747,13 @@
 			update_purchase_receipt_status,
 		)
 
-		pr = make_purchase_receipt()
+		item = make_item()
+
+		pr = make_purchase_receipt(item_code=item.name)
 
 		update_purchase_receipt_status(pr.name, "Closed")
 		self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
 
-		pr.reload()
-		pr.cancel()
-
 	def test_pr_billing_status(self):
 		"""Flow:
 		1. PO -> PR1 -> PI
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 03a4201..1c65ac8 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -96,7 +96,6 @@
   "include_exploded_items",
   "batch_no",
   "rejected_serial_no",
-  "expense_account",
   "item_tax_rate",
   "item_weight_details",
   "weight_per_unit",
@@ -107,6 +106,10 @@
   "manufacturer",
   "column_break_16",
   "manufacturer_part_no",
+  "accounting_details_section",
+  "expense_account",
+  "column_break_102",
+  "provisional_expense_account",
   "accounting_dimensions_section",
   "project",
   "dimension_col_break",
@@ -971,12 +974,27 @@
    "label": "Product Bundle",
    "options": "Product Bundle",
    "read_only": 1
+  },
+  {
+   "fieldname": "provisional_expense_account",
+   "fieldtype": "Link",
+   "label": "Provisional Expense Account",
+   "options": "Account"
+  },
+  {
+   "fieldname": "accounting_details_section",
+   "fieldtype": "Section Break",
+   "label": "Accounting Details"
+  },
+  {
+   "fieldname": "column_break_102",
+   "fieldtype": "Column Break"
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-02-01 11:32:27.980524",
+ "modified": "2022-04-11 13:07:32.061402",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Purchase Receipt Item",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index d3a230e..324ff4f 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -345,6 +345,7 @@
 			"expense_account": expense_account
 			or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults),
 			"discount_account": get_default_discount_account(args, item_defaults),
+			"provisional_expense_account": get_provisional_account(args, item_defaults),
 			"cost_center": get_default_cost_center(
 				args, item_defaults, item_group_defaults, brand_defaults
 			),
@@ -699,6 +700,10 @@
 	)
 
 
+def get_provisional_account(args, item):
+	return item.get("default_provisional_account") or args.default_provisional_account
+
+
 def get_default_discount_account(args, item):
 	return item.get("default_discount_account") or args.discount_account
 
diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json
index 1d6c39e..a41ddb1 100644
--- a/erpnext/telephony/doctype/call_log/call_log.json
+++ b/erpnext/telephony/doctype/call_log/call_log.json
@@ -1,7 +1,7 @@
 {
  "actions": [],
  "autoname": "field:id",
- "creation": "2019-06-05 12:07:02.634534",
+ "creation": "2022-02-21 11:54:58.414784",
  "doctype": "DocType",
  "engine": "InnoDB",
  "field_order": [
@@ -9,6 +9,8 @@
   "id",
   "from",
   "to",
+  "call_received_by",
+  "employee_user_id",
   "medium",
   "start_time",
   "end_time",
@@ -20,6 +22,7 @@
   "recording_url",
   "recording_html",
   "section_break_11",
+  "type_of_call",
   "summary",
   "section_break_19",
   "links"
@@ -103,7 +106,8 @@
   },
   {
    "fieldname": "summary",
-   "fieldtype": "Small Text"
+   "fieldtype": "Small Text",
+   "label": "Summary"
   },
   {
    "fieldname": "section_break_11",
@@ -134,15 +138,37 @@
    "fieldname": "call_details_section",
    "fieldtype": "Section Break",
    "label": "Call Details"
+  },
+  {
+   "fieldname": "employee_user_id",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "label": "Employee User Id",
+   "options": "User"
+  },
+  {
+   "fieldname": "type_of_call",
+   "fieldtype": "Link",
+   "label": "Type Of Call",
+   "options": "Telephony Call Type"
+  },
+  {
+   "depends_on": "to",
+   "fieldname": "call_received_by",
+   "fieldtype": "Link",
+   "label": "Call Received By",
+   "options": "Employee",
+   "read_only": 1
   }
  ],
  "in_create": 1,
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-02-08 14:23:28.744844",
+ "modified": "2022-04-14 02:59:22.503202",
  "modified_by": "Administrator",
  "module": "Telephony",
  "name": "Call Log",
+ "naming_rule": "By fieldname",
  "owner": "Administrator",
  "permissions": [
   {
@@ -164,6 +190,7 @@
  ],
  "sort_field": "creation",
  "sort_order": "DESC",
+ "states": [],
  "title_field": "from",
  "track_changes": 1,
  "track_views": 1
diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 1c88883..7725e71 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -32,6 +32,10 @@
 		if lead:
 			self.add_link(link_type="Lead", link_name=lead)
 
+		# Add Employee Name
+		if self.is_incoming_call():
+			self.update_received_by()
+
 	def after_insert(self):
 		self.trigger_call_popup()
 
@@ -49,6 +53,9 @@
 		if not doc_before_save:
 			return
 
+		if self.is_incoming_call() and self.has_value_changed("to"):
+			self.update_received_by()
+
 		if _is_call_missed(doc_before_save, self):
 			frappe.publish_realtime("call_{id}_missed".format(id=self.id), self)
 			self.trigger_call_popup()
@@ -65,7 +72,8 @@
 	def trigger_call_popup(self):
 		if self.is_incoming_call():
 			scheduled_employees = get_scheduled_employees_for_popup(self.medium)
-			employee_emails = get_employees_with_number(self.to)
+			employees = get_employees_with_number(self.to)
+			employee_emails = [employee.get("user_id") for employee in employees]
 
 			# check if employees with matched number are scheduled to receive popup
 			emails = set(scheduled_employees).intersection(employee_emails)
@@ -85,10 +93,17 @@
 			for email in emails:
 				frappe.publish_realtime("show_call_popup", self, user=email)
 
+	def update_received_by(self):
+		if employees := get_employees_with_number(self.get("to")):
+			self.call_received_by = employees[0].get("name")
+			self.employee_user_id = employees[0].get("user_id")
+
 
 @frappe.whitelist()
-def add_call_summary(call_log, summary):
+def add_call_summary_and_call_type(call_log, summary, call_type):
 	doc = frappe.get_doc("Call Log", call_log)
+	doc.type_of_call = call_type
+	doc.save()
 	doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "<br><br>" + summary)
 
 
@@ -97,20 +112,19 @@
 	if not number:
 		return []
 
-	employee_emails = frappe.cache().hget("employees_with_number", number)
-	if employee_emails:
-		return employee_emails
+	employee_doc_name_and_emails = frappe.cache().hget("employees_with_number", number)
+	if employee_doc_name_and_emails:
+		return employee_doc_name_and_emails
 
-	employees = frappe.get_all(
+	employee_doc_name_and_emails = frappe.get_all(
 		"Employee",
-		filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]},
-		fields=["user_id"],
+		filters={"cell_number": ["like", f"%{number}%"], "user_id": ["!=", ""]},
+		fields=["name", "user_id"],
 	)
 
-	employee_emails = [employee.user_id for employee in employees]
-	frappe.cache().hset("employees_with_number", number, employee_emails)
+	frappe.cache().hset("employees_with_number", number, employee_doc_name_and_emails)
 
-	return employee_emails
+	return employee_doc_name_and_emails
 
 
 def link_existing_conversations(doc, state):
diff --git a/erpnext/regional/doctype/datev_settings/__init__.py b/erpnext/telephony/doctype/telephony_call_type/__init__.py
similarity index 100%
copy from erpnext/regional/doctype/datev_settings/__init__.py
copy to erpnext/telephony/doctype/telephony_call_type/__init__.py
diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js
new file mode 100644
index 0000000..efba2b8
--- /dev/null
+++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Telephony Call Type', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json
new file mode 100644
index 0000000..603709e
--- /dev/null
+++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json
@@ -0,0 +1,58 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:call_type",
+ "creation": "2022-02-25 16:13:37.321312",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "call_type",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "call_type",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Call Type",
+   "reqd": 1,
+   "unique": 1
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Telephony Call Type",
+   "print_hide": 1,
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-02-25 16:14:07.087461",
+ "modified_by": "Administrator",
+ "module": "Telephony",
+ "name": "Telephony Call Type",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py
new file mode 100644
index 0000000..944ffef
--- /dev/null
+++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class TelephonyCallType(Document):
+	pass
diff --git a/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py
new file mode 100644
index 0000000..b3c19c3
--- /dev/null
+++ b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestTelephonyCallType(unittest.TestCase):
+	pass
diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py
new file mode 100644
index 0000000..3ad2575
--- /dev/null
+++ b/erpnext/tests/exotel_test_data.py
@@ -0,0 +1,122 @@
+import frappe
+
+call_initiation_data = frappe._dict(
+	{
+		"CallSid": "23c162077629863c1a2d7f29263a162m",
+		"CallFrom": "09999999991",
+		"CallTo": "09999999980",
+		"Direction": "incoming",
+		"Created": "Wed, 23 Feb 2022 12:31:59",
+		"From": "09999999991",
+		"To": "09999999988",
+		"CurrentTime": "2022-02-23 12:32:02",
+		"DialWhomNumber": "09999999999",
+		"Status": "busy",
+		"EventType": "Dial",
+		"AgentEmail": "test_employee_exotel@company.com",
+	}
+)
+
+call_end_data = frappe._dict(
+	{
+		"CallSid": "23c162077629863c1a2d7f29263a162m",
+		"CallFrom": "09999999991",
+		"CallTo": "09999999980",
+		"Direction": "incoming",
+		"ForwardedFrom": "null",
+		"Created": "Wed, 23 Feb 2022 12:31:59",
+		"DialCallDuration": "17",
+		"RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3",
+		"StartTime": "2022-02-23 12:31:58",
+		"EndTime": "1970-01-01 05:30:00",
+		"DialCallStatus": "completed",
+		"CallType": "completed",
+		"DialWhomNumber": "09999999999",
+		"ProcessStatus": "null",
+		"flow_id": "228040",
+		"tenant_id": "67291",
+		"From": "09999999991",
+		"To": "09999999988",
+		"RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25",
+		"CurrentTime": "2022-02-23 12:32:25",
+		"OutgoingPhoneNumber": "09999999988",
+		"Legs": [
+			{
+				"Number": "09999999999",
+				"Type": "single",
+				"OnCallDuration": "10",
+				"CallerId": "09999999980",
+				"CauseCode": "NORMAL_CLEARING",
+				"Cause": "16",
+			}
+		],
+	}
+)
+
+call_disconnected_data = frappe._dict(
+	{
+		"CallSid": "d96421addce69e24bdc7ce5880d1162l",
+		"CallFrom": "09999999991",
+		"CallTo": "09999999980",
+		"Direction": "incoming",
+		"ForwardedFrom": "null",
+		"Created": "Mon, 21 Feb 2022 15:58:12",
+		"DialCallDuration": "0",
+		"StartTime": "2022-02-21 15:58:12",
+		"EndTime": "1970-01-01 05:30:00",
+		"DialCallStatus": "canceled",
+		"CallType": "client-hangup",
+		"DialWhomNumber": "09999999999",
+		"ProcessStatus": "null",
+		"flow_id": "228040",
+		"tenant_id": "67291",
+		"From": "09999999991",
+		"To": "09999999988",
+		"CurrentTime": "2022-02-21 15:58:47",
+		"OutgoingPhoneNumber": "09999999988",
+		"Legs": [
+			{
+				"Number": "09999999999",
+				"Type": "single",
+				"OnCallDuration": "0",
+				"CallerId": "09999999980",
+				"CauseCode": "RING_TIMEOUT",
+				"Cause": "1003",
+			}
+		],
+	}
+)
+
+call_not_answered_data = frappe._dict(
+	{
+		"CallSid": "fdb67a2b4b2d057b610a52ef43f81622",
+		"CallFrom": "09999999991",
+		"CallTo": "09999999980",
+		"Direction": "incoming",
+		"ForwardedFrom": "null",
+		"Created": "Mon, 21 Feb 2022 15:47:02",
+		"DialCallDuration": "0",
+		"StartTime": "2022-02-21 15:47:02",
+		"EndTime": "1970-01-01 05:30:00",
+		"DialCallStatus": "no-answer",
+		"CallType": "incomplete",
+		"DialWhomNumber": "09999999999",
+		"ProcessStatus": "null",
+		"flow_id": "228040",
+		"tenant_id": "67291",
+		"From": "09999999991",
+		"To": "09999999988",
+		"CurrentTime": "2022-02-21 15:47:40",
+		"OutgoingPhoneNumber": "09999999988",
+		"Legs": [
+			{
+				"Number": "09999999999",
+				"Type": "single",
+				"OnCallDuration": "0",
+				"CallerId": "09999999980",
+				"CauseCode": "RING_TIMEOUT",
+				"Cause": "1003",
+			}
+		],
+	}
+)
diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py
new file mode 100644
index 0000000..76bbb3e
--- /dev/null
+++ b/erpnext/tests/test_exotel.py
@@ -0,0 +1,69 @@
+import frappe
+from frappe.contacts.doctype.contact.test_contact import create_contact
+from frappe.tests.test_api import FrappeAPITestCase
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+
+
+class TestExotel(FrappeAPITestCase):
+	@classmethod
+	def setUpClass(cls):
+		cls.CURRENT_DB_CONNECTION = frappe.db
+		cls.test_employee_name = make_employee(
+			user="test_employee_exotel@company.com", cell_number="9999999999"
+		)
+		frappe.db.set_value("Exotel Settings", "Exotel Settings", "enabled", 1)
+		phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}]
+		create_contact(name="Test Contact", salutation="Mr", phones=phones)
+		frappe.db.commit()
+
+	def test_for_successful_call(self):
+		from .exotel_test_data import call_end_data, call_initiation_data
+
+		api_method = "handle_incoming_call"
+		end_call_api_method = "handle_end_call"
+
+		self.emulate_api_call_from_exotel(api_method, call_initiation_data)
+		self.emulate_api_call_from_exotel(end_call_api_method, call_end_data)
+		call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid)
+
+		self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom)
+		self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber)
+		self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
+		self.assertEqual(call_log.get("status"), "Completed")
+
+	def test_for_disconnected_call(self):
+		from .exotel_test_data import call_disconnected_data
+
+		api_method = "handle_missed_call"
+		self.emulate_api_call_from_exotel(api_method, call_disconnected_data)
+		call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid)
+		self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom)
+		self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber)
+		self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
+		self.assertEqual(call_log.get("status"), "Canceled")
+
+	def test_for_call_not_answered(self):
+		from .exotel_test_data import call_not_answered_data
+
+		api_method = "handle_missed_call"
+		self.emulate_api_call_from_exotel(api_method, call_not_answered_data)
+		call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid)
+		self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom)
+		self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber)
+		self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
+		self.assertEqual(call_log.get("status"), "No Answer")
+
+	def emulate_api_call_from_exotel(self, api_method, data):
+		self.post(
+			f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}",
+			data=frappe.as_json(data),
+			content_type="application/json",
+			as_tuple=True,
+		)
+		# restart db connection to get latest data
+		frappe.connect()
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db = cls.CURRENT_DB_CONNECTION
diff --git a/requirements.txt b/requirements.txt
index 657054f..85ff515 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
 # frappe   # https://github.com/frappe/frappe is installed during bench-init
 gocardless-pro~=1.22.0
 googlemaps
-pandas>=1.1.5,<2.0.0
 plaid-python~=7.2.1
 pycountry~=20.7.3
 PyGithub~=1.55