Merge pull request #24824 from resilient-tech/fix-salary

refactor(payroll): simplified logic for additional salary
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py
index f5af677..029e11f 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -89,10 +89,11 @@
 		no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
 		return amount_per_day * no_of_days
 
-@frappe.whitelist()
-def get_additional_salary_component(employee, start_date, end_date, component_type):
-	additional_salaries = frappe.db.sql("""
-		select name, salary_component, type, amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date
+def get_additional_salaries(employee, start_date, end_date, component_type):
+	additional_salary_list = frappe.db.sql("""
+		select name, salary_component as component, type, amount,
+		overwrite_salary_structure_amount as overwrite,
+		deduct_full_tax_on_selected_payroll_date
 		from `tabAdditional Salary`
 		where employee=%(employee)s
 			and docstatus = 1
@@ -102,7 +103,7 @@
 					from_date <= %(to_date)s and to_date >= %(to_date)s
 				)
 		and type = %(component_type)s
-		order by salary_component, overwrite_salary_structure_amount DESC
+		order by salary_component, overwrite ASC
 	""", {
 		'employee': employee,
 		'from_date': start_date,
@@ -110,38 +111,18 @@
 		'component_type': "Earning" if component_type == "earnings" else "Deduction"
 	}, as_dict=1)
 
-	existing_salary_components= []
-	salary_components_details = {}
-	additional_salary_details = []
+	additional_salaries = []
+	components_to_overwrite = []
 
-	overwrites_components = [ele.salary_component for ele in additional_salaries if ele.overwrite_salary_structure_amount == 1]
+	for d in additional_salary_list:
+		if d.overwrite:
+			if d.component in components_to_overwrite:
+				frappe.throw(_("Multiple Additional Salaries with overwrite "
+					"property exist for Salary Component {0} between {1} and {2}.").format(
+					frappe.bold(d.component), start_date, end_date), title=_("Error"))
 
-	component_fields = ["depends_on_payment_days", "salary_component_abbr", "is_tax_applicable", "variable_based_on_taxable_salary", 'type']
-	for d in additional_salaries:
+			components_to_overwrite.append(d.component)
 
-		if d.salary_component not in existing_salary_components:
-			component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields)
-			struct_row = frappe._dict({'salary_component': d.salary_component})
-			if component:
-				struct_row.update(component[0])
+		additional_salaries.append(d)
 
-			struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date
-			struct_row['is_additional_component'] = 1
-
-			salary_components_details[d.salary_component] = struct_row
-
-
-		if overwrites_components.count(d.salary_component) > 1:
-			frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component: {0} between {1} and {2}.".format(d.salary_component, start_date, end_date)), title=_("Error"))
-		else:
-			additional_salary_details.append({
-				'name': d.name,
-				'component': d.salary_component,
-				'amount': d.amount,
-				'type': d.type,
-				'overwrite': d.overwrite_salary_structure_amount,
-			})
-
-		existing_salary_components.append(d.salary_component)
-
-	return salary_components_details, additional_salary_details
+	return additional_salaries
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 595d697..a04a635 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -13,7 +13,7 @@
 from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
 from erpnext.utilities.transaction_base import TransactionBase
 from frappe.utils.background_jobs import enqueue
-from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salary_component
+from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
 from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
 from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
 from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
@@ -524,7 +524,7 @@
 
 		except NameError as err:
 			frappe.throw(_("{0} <br> This error can be due to missing or deleted field.").format(err),
-			    title=_("Name error"))
+				title=_("Name error"))
 		except SyntaxError as err:
 			frappe.throw(_("Syntax error in formula or condition: {0}").format(err))
 		except Exception as e:
@@ -558,15 +558,16 @@
 						self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings")
 
 	def add_additional_salary_components(self, component_type):
-		salary_components_details, additional_salary_details = get_additional_salary_component(self.employee,
+		additional_salaries = get_additional_salaries(self.employee,
 			self.start_date, self.end_date, component_type)
-		if salary_components_details and additional_salary_details:
-			for additional_salary in additional_salary_details:
-				additional_salary =frappe._dict(additional_salary)
-				amount = additional_salary.amount
-				overwrite = additional_salary.overwrite
-				self.update_component_row(frappe._dict(salary_components_details[additional_salary.component]), amount,
-					component_type, overwrite=overwrite, additional_salary=additional_salary.name)
+
+		for additional_salary in additional_salaries:
+			self.update_component_row(
+				get_salary_component_data(additional_salary.component),
+				additional_salary.amount,
+				component_type,
+				additional_salary
+			)
 
 	def add_tax_components(self, payroll_period):
 		# Calculate variable_based_on_taxable_salary after all components updated in salary slip
@@ -583,47 +584,59 @@
 
 		for d in tax_components:
 			tax_amount = self.calculate_variable_based_on_taxable_salary(d, payroll_period)
-			tax_row = self.get_salary_slip_row(d)
+			tax_row = get_salary_component_data(d)
 			self.update_component_row(tax_row, tax_amount, "deductions")
 
-	def update_component_row(self, struct_row, amount, key, overwrite=1, additional_salary = ''):
+	def update_component_row(self, component_data, amount, component_type, additional_salary=None):
 		component_row = None
-		for d in self.get(key):
-			if d.salary_component == struct_row.salary_component:
+		for d in self.get(component_type):
+			if d.salary_component != component_data.salary_component:
+				continue
+
+			if (
+				not d.additional_salary
+				and (not additional_salary or additional_salary.overwrite)
+				or additional_salary
+				and additional_salary.name == d.additional_salary
+			):
 				component_row = d
+				break
 
-		if not component_row or (struct_row.get("is_additional_component") and not overwrite):
-			if amount:
-				self.append(key, {
-					'amount': amount,
-					'default_amount': amount if not struct_row.get("is_additional_component") else 0,
-					'depends_on_payment_days' : struct_row.depends_on_payment_days,
-					'salary_component' : struct_row.salary_component,
-					'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"),
-					'additional_salary': additional_salary,
-					'do_not_include_in_total' : struct_row.do_not_include_in_total,
-					'is_tax_applicable': struct_row.is_tax_applicable,
-					'is_flexible_benefit': struct_row.is_flexible_benefit,
-					'variable_based_on_taxable_salary': struct_row.variable_based_on_taxable_salary,
-					'deduct_full_tax_on_selected_payroll_date': struct_row.deduct_full_tax_on_selected_payroll_date,
-					'additional_amount': amount if struct_row.get("is_additional_component") else 0,
-					'exempted_from_income_tax': struct_row.exempted_from_income_tax
-				})
+		if additional_salary and additional_salary.overwrite:
+			# Additional Salary with overwrite checked, remove default rows of same component
+			self.set(component_type, [
+				d for d in self.get(component_type)
+				if d.salary_component != component_data.salary_component
+				or d.additional_salary and additional_salary.name != d.additional_salary
+				or d == component_row
+			])
+
+		if not component_row:
+			if not amount:
+				return
+
+			component_row = self.append(component_type)
+			for attr in (
+				'depends_on_payment_days', 'salary_component', 'abbr'
+				'do_not_include_in_total', 'is_tax_applicable',
+				'is_flexible_benefit', 'variable_based_on_taxable_salary',
+				'exempted_from_income_tax'
+			):
+				component_row.set(attr, component_data.get(attr))
+
+		if additional_salary:
+			component_row.default_amount = 0
+			component_row.additional_amount = amount
+			component_row.additional_salary = additional_salary.name
+			component_row.deduct_full_tax_on_selected_payroll_date = \
+				additional_salary.deduct_full_tax_on_selected_payroll_date
 		else:
-			if struct_row.get("is_additional_component"):
-				if overwrite:
-					component_row.additional_amount = amount - component_row.get("default_amount", 0)
-					component_row.additional_salary = additional_salary
-				else:
-					component_row.additional_amount = amount
+			component_row.default_amount = amount
+			component_row.additional_amount = 0
+			component_row.deduct_full_tax_on_selected_payroll_date = \
+				component_data.deduct_full_tax_on_selected_payroll_date
 
-				if not overwrite and component_row.default_amount:
-					amount += component_row.default_amount
-			else:
-				component_row.default_amount = amount
-
-			component_row.amount = amount
-			component_row.deduct_full_tax_on_selected_payroll_date = struct_row.deduct_full_tax_on_selected_payroll_date
+		component_row.amount = amount
 
 	def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period):
 		if not payroll_period:
@@ -950,26 +963,13 @@
 				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"))
+				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_salary_slip_row(self, salary_component):
-		component = frappe.get_doc("Salary Component", salary_component)
-		# Data for update_component_row
-		struct_row = frappe._dict()
-		struct_row['depends_on_payment_days'] = component.depends_on_payment_days
-		struct_row['salary_component'] = component.name
-		struct_row['abbr'] = component.salary_component_abbr
-		struct_row['do_not_include_in_total'] = component.do_not_include_in_total
-		struct_row['is_tax_applicable'] = component.is_tax_applicable
-		struct_row['is_flexible_benefit'] = component.is_flexible_benefit
-		struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
-		return struct_row
-
 	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"])
@@ -1032,7 +1032,6 @@
 			self.total_loan_repayment += payment.total_payment
 
 	def get_loan_details(self):
-
 		return frappe.get_all("Loan",
 			fields=["name", "interest_income_account", "loan_account", "loan_type"],
 			filters = {
@@ -1263,3 +1262,19 @@
 def generate_password_for_pdf(policy_template, employee):
 	employee = frappe.get_doc("Employee", employee)
 	return policy_template.format(**employee.as_dict())
+
+def get_salary_component_data(component):
+	return frappe.get_value(
+		"Salary Component",
+		component,
+		[
+			"name as salary_component",
+			"depends_on_payment_days",
+			"salary_component_abbr as abbr",
+			"do_not_include_in_total",
+			"is_tax_applicable",
+			"is_flexible_benefit",
+			"variable_based_on_taxable_salary",
+		],
+		as_dict=1,
+	)