test: HRA Exemption in Employee Tax Exemption Declaration
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
index 1d90e73..6986bce 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
@@ -4,13 +4,15 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_months, getdate
 
 import erpnext
 from erpnext.hr.doctype.employee.test_employee import make_employee
 from erpnext.hr.utils import DuplicateDeclarationError
 
 
-class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
+class TestEmployeeTaxExemptionDeclaration(FrappeTestCase):
 	def setUp(self):
 		make_employee("employee@taxexepmtion.com")
 		make_employee("employee1@taxexepmtion.com")
@@ -112,6 +114,257 @@
 
 		self.assertEqual(declaration.total_exemption_amount, 100000)
 
+	def test_india_hra_exemption(self):
+		setup_hra_exemption_prerequisites("Monthly")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "Test Company",
+				"payroll_period": "_Test Payroll Period 1",
+				"currency": "INR",
+				"monthly_house_rent": 50000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test Sub Category",
+						exemption_category="_Test Category",
+						amount=80000,
+					),
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Monthly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 3000)
+		self.assertEqual(declaration.annual_hra_exemption, 36000)
+		# 100000 Standard Exemption + 36000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 136000)
+
+	def test_india_hra_exemption_with_daily_payroll_frequency(self):
+		setup_hra_exemption_prerequisites("Daily")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "Test Company",
+				"payroll_period": "_Test Payroll Period 1",
+				"currency": "INR",
+				"monthly_house_rent": 170000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Daily HRA received = 3000
+		# should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 17916.67)
+		self.assertEqual(declaration.annual_hra_exemption, 215000)
+		# 50000 Standard Exemption + 215000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 265000)
+
+	def test_india_hra_exemption_with_weekly_payroll_frequency(self):
+		setup_hra_exemption_prerequisites("Weekly")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "Test Company",
+				"payroll_period": "_Test Payroll Period 1",
+				"currency": "INR",
+				"monthly_house_rent": 170000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Weekly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 13000)
+		self.assertEqual(declaration.annual_hra_exemption, 156000)
+		# 50000 Standard Exemption + 156000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 206000)
+
+	def test_india_hra_exemption_with_fortnightly_payroll_frequency(self):
+		setup_hra_exemption_prerequisites("Fortnightly")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "Test Company",
+				"payroll_period": "_Test Payroll Period 1",
+				"currency": "INR",
+				"monthly_house_rent": 170000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Fortnightly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 6500)
+		self.assertEqual(declaration.annual_hra_exemption, 78000)
+		# 50000 Standard Exemption + 78000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 128000)
+
+	def test_india_hra_exemption_with_bimonthly_payroll_frequency(self):
+		setup_hra_exemption_prerequisites("Bimonthly")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "Test Company",
+				"payroll_period": "_Test Payroll Period 1",
+				"currency": "INR",
+				"monthly_house_rent": 50000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test Sub Category",
+						exemption_category="_Test Category",
+						amount=80000,
+					),
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Bimonthly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 1500)
+		self.assertEqual(declaration.annual_hra_exemption, 18000)
+		# 100000 Standard Exemption + 18000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 118000)
+
+	def test_india_hra_exemption_with_multiple_salary_structure_assignments(self):
+		from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
+		from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
+			create_salary_structure_assignment,
+			make_salary_structure,
+		)
+
+		payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
+
+		create_tax_slab(
+			payroll_period,
+			allow_tax_exemption=True,
+			currency="INR",
+			effective_date=getdate("2019-04-01"),
+			company="_Test Company",
+		)
+
+		frappe.db.set_value(
+			"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
+		)
+
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name")
+
+		# salary structure with base 50000, HRA 3000
+		make_salary_structure(
+			"Monthly Structure for HRA Exemption 1",
+			"Monthly",
+			employee=employee,
+			company="_Test Company",
+			currency="INR",
+			payroll_period=payroll_period.name,
+			from_date=payroll_period.start_date,
+		)
+
+		# salary structure with base 70000, HRA = base * 0.2 = 14000
+		salary_structure = make_salary_structure(
+			"Monthly Structure for HRA Exemption 2",
+			"Monthly",
+			employee=employee,
+			company="_Test Company",
+			currency="INR",
+			payroll_period=payroll_period.name,
+			from_date=payroll_period.start_date,
+			dont_submit=True,
+		)
+		for component_row in salary_structure.earnings:
+			if component_row.salary_component == "HRA":
+				component_row.amount = 0
+				component_row.amount_based_on_formula = 1
+				component_row.formula = "base * 0.2"
+				break
+
+		salary_structure.submit()
+
+		create_salary_structure_assignment(
+			employee,
+			salary_structure.name,
+			from_date=add_months(payroll_period.start_date, 6),
+			company="_Test Company",
+			currency="INR",
+			payroll_period=payroll_period.name,
+			base=70000,
+			allow_duplicate=True,
+		)
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "Test Company",
+				"payroll_period": "_Test Payroll Period 1",
+				"currency": "INR",
+				"monthly_house_rent": 50000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Monthly HRA received = 50000 * 6 months + 70000 * 6 months
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 8500)
+		self.assertEqual(declaration.annual_hra_exemption, 102000)
+		# 50000 Standard Exemption + 102000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 152000)
+
 
 def create_payroll_period(**args):
 	args = frappe._dict(args)
@@ -163,3 +416,31 @@
 				"is_active": 1,
 			}
 		).insert()
+
+
+def setup_hra_exemption_prerequisites(frequency):
+	from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
+	from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+	payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
+
+	create_tax_slab(
+		payroll_period,
+		allow_tax_exemption=True,
+		currency="INR",
+		effective_date=getdate("2019-04-01"),
+		company="_Test Company",
+	)
+
+	make_salary_structure(
+		f"{frequency} Structure for HRA Exemption",
+		frequency,
+		employee=frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
+		company="_Test Company",
+		currency="INR",
+		payroll_period=payroll_period,
+	)
+
+	frappe.db.set_value(
+		"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
+	)
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index e9b5ed2..5c78e8f 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -230,9 +230,12 @@
 	company=None,
 	currency=erpnext.get_default_currency(),
 	payroll_period=None,
+	base=None,
+	allow_duplicate=False,
 ):
-
-	if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
+	if not allow_duplicate and frappe.db.exists(
+		"Salary Structure Assignment", {"employee": employee}
+	):
 		frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee))
 
 	if not payroll_period:
@@ -245,7 +248,7 @@
 
 	salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
 	salary_structure_assignment.employee = employee
-	salary_structure_assignment.base = 50000
+	salary_structure_assignment.base = base or 50000
 	salary_structure_assignment.variable = 5000
 
 	if not from_date: