# Copyright (c) 2017, Xhive LLC
# For license information, please see license.txt

import json

from dateutil.relativedelta import relativedelta

import xhiveframework
from xhiveframework import _
from xhiveframework.desk.reportview import get_filters_cond, get_match_cond
from xhiveframework.model.document import Document
from xhiveframework.query_builder.functions import Coalesce
from xhiveframework.utils import (
	DATE_FORMAT,
	add_days,
	add_to_date,
	cint,
	comma_and,
	date_diff,
	flt,
	get_link_to_form,
	getdate,
)

import xhiveerp
from xhiveerp.accounts.doctype.accounting_dimension.accounting_dimension import (
	get_accounting_dimensions,
)
from xhiveerp.accounts.utils import get_fiscal_year
from xhiveerp.setup.doctype.employee.employee import get_holiday_list_for_employee


class PayrollEntry(Document):
	def onload(self):
		if not self.docstatus == 1 or self.salary_slips_submitted:
			return

		# check if salary slips were manually submitted
		entries = xhiveframework.db.count("Salary Slip", {"payroll_entry": self.name, "docstatus": 1}, ["name"])
		if cint(entries) == len(self.employees):
			self.set_onload("submitted_ss", True)

	def validate(self):
		self.number_of_employees = len(self.employees)
		self.set_status()

	def on_submit(self):
		self.set_status(update=True, status="Submitted")
		self.create_salary_slips()

	def before_submit(self):
		self.validate_employee_details()
		self.validate_payroll_payable_account()
		if self.validate_attendance:
			if self.validate_employee_attendance():
				xhiveframework.throw(_("Cannot Submit, Employees left to mark attendance"))

	def set_status(self, status=None, update=False):
		if not status:
			status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]

		if update:
			self.db_set("status", status)
		else:
			self.status = status

	def validate_employee_details(self):
		emp_with_sal_slip = []
		for employee_details in self.employees:
			if xhiveframework.db.exists(
				"Salary Slip",
				{
					"employee": employee_details.employee,
					"start_date": self.start_date,
					"end_date": self.end_date,
					"docstatus": 1,
				},
			):
				emp_with_sal_slip.append(employee_details.employee)

		if len(emp_with_sal_slip):
			xhiveframework.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))

	def validate_payroll_payable_account(self):
		if xhiveframework.db.get_value("Account", self.payroll_payable_account, "account_type"):
			xhiveframework.throw(
				_(
					"Account type cannot be set for payroll payable account {0}, please remove and try again"
				).format(xhiveframework.bold(get_link_to_form("Account", self.payroll_payable_account)))
			)

	def on_cancel(self):
		self.ignore_linked_doctypes = "GL Entry"

		xhiveframework.delete_doc(
			"Salary Slip",
			xhiveframework.db.sql_list(
				"""select name from `tabSalary Slip`
			where payroll_entry=%s """,
				(self.name),
			),
		)
		self.db_set("salary_slips_created", 0)
		self.db_set("salary_slips_submitted", 0)
		self.set_status(update=True, status="Cancelled")
		self.db_set("error_message", "")

	def get_emp_list(self):
		"""
		Returns list of active employees based on selected criteria
		and for which salary structure exists
		"""
		self.check_mandatory()
		filters = self.make_filters()
		cond = get_filter_condition(filters)
		cond += get_joining_relieving_condition(self.start_date, self.end_date)

		condition = ""
		if self.payroll_frequency:
			condition = """and payroll_frequency = '%(payroll_frequency)s'""" % {
				"payroll_frequency": self.payroll_frequency
			}

		sal_struct = get_sal_struct(
			self.company, self.currency, self.salary_slip_based_on_timesheet, condition
		)
		if sal_struct:
			cond += "and t2.salary_structure IN %(sal_struct)s "
			cond += "and t2.payroll_payable_account = %(payroll_payable_account)s "
			cond += "and %(from_date)s >= t2.from_date"
			emp_list = get_emp_list(sal_struct, cond, self.end_date, self.payroll_payable_account)
			emp_list = remove_payrolled_employees(emp_list, self.start_date, self.end_date)
			return emp_list

	def make_filters(self):
		filters = xhiveframework._dict()
		filters["company"] = self.company
		filters["branch"] = self.branch
		filters["department"] = self.department
		filters["designation"] = self.designation

		return filters

	@xhiveframework.whitelist()
	def fill_employee_details(self):
		self.set("employees", [])
		employees = self.get_emp_list()
		if not employees:
			error_msg = _(
				"No employees found for the mentioned criteria:<br>Company: {0}<br> Currency: {1}<br>Payroll Payable Account: {2}"
			).format(
				xhiveframework.bold(self.company),
				xhiveframework.bold(self.currency),
				xhiveframework.bold(self.payroll_payable_account),
			)
			if self.branch:
				error_msg += "<br>" + _("Branch: {0}").format(xhiveframework.bold(self.branch))
			if self.department:
				error_msg += "<br>" + _("Department: {0}").format(xhiveframework.bold(self.department))
			if self.designation:
				error_msg += "<br>" + _("Designation: {0}").format(xhiveframework.bold(self.designation))
			if self.start_date:
				error_msg += "<br>" + _("Start date: {0}").format(xhiveframework.bold(self.start_date))
			if self.end_date:
				error_msg += "<br>" + _("End date: {0}").format(xhiveframework.bold(self.end_date))
			xhiveframework.throw(error_msg, title=_("No employees found"))

		for d in employees:
			self.append("employees", d)

		self.number_of_employees = len(self.employees)
		if self.validate_attendance:
			return self.validate_employee_attendance()

	def check_mandatory(self):
		for fieldname in ["company", "start_date", "end_date"]:
			if not self.get(fieldname):
				xhiveframework.throw(_("Please set {0}").format(self.meta.get_label(fieldname)))

	@xhiveframework.whitelist()
	def create_salary_slips(self):
		"""
		Creates salary slip for selected employees if already not created
		"""
		self.check_permission("write")
		employees = [emp.employee for emp in self.employees]
		if employees:
			args = xhiveframework._dict(
				{
					"salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet,
					"payroll_frequency": self.payroll_frequency,
					"start_date": self.start_date,
					"end_date": self.end_date,
					"company": self.company,
					"posting_date": self.posting_date,
					"deduct_tax_for_unclaimed_employee_benefits": self.deduct_tax_for_unclaimed_employee_benefits,
					"deduct_tax_for_unsubmitted_tax_exemption_proof": self.deduct_tax_for_unsubmitted_tax_exemption_proof,
					"payroll_entry": self.name,
					"exchange_rate": self.exchange_rate,
					"currency": self.currency,
				}
			)
			if len(employees) > 30 or xhiveframework.flags.enqueue_payroll_entry:
				self.db_set("status", "Queued")
				xhiveframework.enqueue(
					create_salary_slips_for_employees,
					timeout=600,
					employees=employees,
					args=args,
					publish_progress=False,
				)
				xhiveframework.msgprint(
					_("Salary Slip creation is queued. It may take a few minutes"),
					alert=True,
					indicator="blue",
				)
			else:
				create_salary_slips_for_employees(employees, args, publish_progress=False)
				# since this method is called via frm.call this doc needs to be updated manually
				self.reload()

	def get_sal_slip_list(self, ss_status, as_dict=False):
		"""
		Returns list of salary slips based on selected criteria
		"""

		ss = xhiveframework.qb.DocType("Salary Slip")
		ss_list = (
			xhiveframework.qb.from_(ss)
			.select(ss.name, ss.salary_structure)
			.where(
				(ss.docstatus == ss_status)
				& (ss.start_date >= self.start_date)
				& (ss.end_date <= self.end_date)
				& (ss.payroll_entry == self.name)
				& ((ss.journal_entry.isnull()) | (ss.journal_entry == ""))
				& (Coalesce(ss.salary_slip_based_on_timesheet, 0) == self.salary_slip_based_on_timesheet)
			)
		).run(as_dict=as_dict)

		return ss_list

	@xhiveframework.whitelist()
	def submit_salary_slips(self):
		self.check_permission("write")
		salary_slips = self.get_sal_slip_list(ss_status=0)
		if len(salary_slips) > 30 or xhiveframework.flags.enqueue_payroll_entry:
			self.db_set("status", "Queued")
			xhiveframework.enqueue(
				submit_salary_slips_for_employees,
				timeout=600,
				payroll_entry=self,
				salary_slips=salary_slips,
				publish_progress=False,
			)
			xhiveframework.msgprint(
				_("Salary Slip submission is queued. It may take a few minutes"),
				alert=True,
				indicator="blue",
			)
		else:
			submit_salary_slips_for_employees(self, salary_slips, publish_progress=False)

	def email_salary_slip(self, submitted_ss):
		if xhiveframework.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee"):
			for ss in submitted_ss:
				ss.email_salary_slip()

	def get_salary_component_account(self, salary_component):
		account = xhiveframework.db.get_value(
			"Salary Component Account", {"parent": salary_component, "company": self.company}, "account"
		)

		if not account:
			xhiveframework.throw(
				_("Please set account in Salary Component {0}").format(
					get_link_to_form("Salary Component", salary_component)
				)
			)

		return account

	def get_salary_components(self, component_type):
		salary_slips = self.get_sal_slip_list(ss_status=1, as_dict=True)

		if salary_slips:
			ss = xhiveframework.qb.DocType("Salary Slip")
			ssd = xhiveframework.qb.DocType("Salary Detail")
			salary_components = (
				xhiveframework.qb.from_(ss)
				.join(ssd)
				.on(ss.name == ssd.parent)
				.select(ssd.salary_component, ssd.amount, ssd.parentfield, ss.salary_structure, ss.employee)
				.where(
					(ssd.parentfield == component_type) & (ss.name.isin(tuple([d.name for d in salary_slips])))
				)
			).run(as_dict=True)

			return salary_components

	def get_salary_component_total(
		self,
		component_type=None,
		process_payroll_accounting_entry_based_on_employee=False,
	):
		salary_components = self.get_salary_components(component_type)
		if salary_components:
			component_dict = {}
			self.employee_cost_centers = {}
			for item in salary_components:
				employee_cost_centers = self.get_payroll_cost_centers_for_employee(
					item.employee, item.salary_structure
				)
				add_component_to_accrual_jv_entry = True
				if component_type == "earnings":
					is_flexible_benefit, only_tax_impact = xhiveframework.get_cached_value(
						"Salary Component", item["salary_component"], ["is_flexible_benefit", "only_tax_impact"]
					)
					if is_flexible_benefit == 1 and only_tax_impact == 1:
						add_component_to_accrual_jv_entry = False

				if add_component_to_accrual_jv_entry:
					for cost_center, percentage in employee_cost_centers.items():
						amount_against_cost_center = flt(item.amount) * percentage / 100
						key = (item.salary_component, cost_center)
						component_dict[key] = component_dict.get(key, 0) + amount_against_cost_center

						if process_payroll_accounting_entry_based_on_employee:
							self.set_employee_based_payroll_payable_entries(
								component_type, item.employee, amount_against_cost_center
							)

			account_details = self.get_account(component_dict=component_dict)

			return account_details

	def set_employee_based_payroll_payable_entries(self, component_type, employee, amount):
		self.employee_based_payroll_payable_entries.setdefault(employee, {})
		self.employee_based_payroll_payable_entries[employee].setdefault(component_type, 0)
		self.employee_based_payroll_payable_entries[employee][component_type] += amount

	def get_payroll_cost_centers_for_employee(self, employee, salary_structure):
		if not self.employee_cost_centers.get(employee):
			ss_assignment_name = xhiveframework.db.get_value(
				"Salary Structure Assignment",
				{"employee": employee, "salary_structure": salary_structure, "docstatus": 1},
				"name",
			)

			if ss_assignment_name:
				cost_centers = dict(
					xhiveframework.get_all(
						"Employee Cost Center",
						{"parent": ss_assignment_name},
						["cost_center", "percentage"],
						as_list=1,
					)
				)
				if not cost_centers:
					default_cost_center, department = xhiveframework.get_cached_value(
						"Employee", employee, ["payroll_cost_center", "department"]
					)
					if not default_cost_center and department:
						default_cost_center = xhiveframework.get_cached_value(
							"Department", department, "payroll_cost_center"
						)
					if not default_cost_center:
						default_cost_center = self.cost_center

					cost_centers = {default_cost_center: 100}

				self.employee_cost_centers.setdefault(employee, cost_centers)

		return self.employee_cost_centers.get(employee, {})

	def get_account(self, component_dict=None):
		account_dict = {}
		for key, amount in component_dict.items():
			account = self.get_salary_component_account(key[0])
			accouting_key = (account, key[1])

			account_dict[accouting_key] = account_dict.get(accouting_key, 0) + amount

		return account_dict

	def make_accrual_jv_entry(self):
		self.check_permission("write")
		process_payroll_accounting_entry_based_on_employee = xhiveframework.db.get_single_value(
			"Payroll Settings", "process_payroll_accounting_entry_based_on_employee"
		)
		self.employee_based_payroll_payable_entries = {}

		earnings = (
			self.get_salary_component_total(
				component_type="earnings",
				process_payroll_accounting_entry_based_on_employee=process_payroll_accounting_entry_based_on_employee,
			)
			or {}
		)

		deductions = (
			self.get_salary_component_total(
				component_type="deductions",
				process_payroll_accounting_entry_based_on_employee=process_payroll_accounting_entry_based_on_employee,
			)
			or {}
		)

		payroll_payable_account = self.payroll_payable_account
		jv_name = ""
		precision = xhiveframework.get_precision("Journal Entry Account", "debit_in_account_currency")

		if earnings or deductions:
			journal_entry = xhiveframework.new_doc("Journal Entry")
			journal_entry.voucher_type = "Journal Entry"
			journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}").format(
				self.start_date, self.end_date
			)
			journal_entry.company = self.company
			journal_entry.posting_date = self.posting_date
			accounting_dimensions = get_accounting_dimensions() or []

			accounts = []
			currencies = []
			payable_amount = 0
			multi_currency = 0
			company_currency = xhiveerp.get_company_currency(self.company)

			# Earnings
			for acc_cc, amount in earnings.items():
				payable_amount = self.get_accounting_entries_and_payable_amount(
					acc_cc[0],
					acc_cc[1] or self.cost_center,
					amount,
					currencies,
					company_currency,
					payable_amount,
					accounting_dimensions,
					precision,
					entry_type="debit",
					accounts=accounts,
				)

			# Deductions
			for acc_cc, amount in deductions.items():
				payable_amount = self.get_accounting_entries_and_payable_amount(
					acc_cc[0],
					acc_cc[1] or self.cost_center,
					amount,
					currencies,
					company_currency,
					payable_amount,
					accounting_dimensions,
					precision,
					entry_type="credit",
					accounts=accounts,
				)

			# Payable amount
			if process_payroll_accounting_entry_based_on_employee:
				"""
				employee_based_payroll_payable_entries = {
				        'HR-EMP-00004': {
				                        'earnings': 83332.0,
				                        'deductions': 2000.0
				                },
				        'HR-EMP-00005': {
				                'earnings': 50000.0,
				                'deductions': 2000.0
				        }
				}
				"""
				for employee, employee_details in self.employee_based_payroll_payable_entries.items():
					payable_amount = employee_details.get("earnings") - (employee_details.get("deductions") or 0)

					payable_amount = self.get_accounting_entries_and_payable_amount(
						payroll_payable_account,
						self.cost_center,
						payable_amount,
						currencies,
						company_currency,
						0,
						accounting_dimensions,
						precision,
						entry_type="payable",
						party=employee,
						accounts=accounts,
					)

			else:
				payable_amount = self.get_accounting_entries_and_payable_amount(
					payroll_payable_account,
					self.cost_center,
					payable_amount,
					currencies,
					company_currency,
					0,
					accounting_dimensions,
					precision,
					entry_type="payable",
					accounts=accounts,
				)

			journal_entry.set("accounts", accounts)
			if len(currencies) > 1:
				multi_currency = 1
			journal_entry.multi_currency = multi_currency
			journal_entry.title = payroll_payable_account
			journal_entry.save()

			try:
				journal_entry.submit()
				jv_name = journal_entry.name
				self.update_salary_slip_status(jv_name=jv_name)
			except Exception as e:
				if type(e) in (str, list, tuple):
					xhiveframework.msgprint(e)
				raise

		return jv_name

	def get_accounting_entries_and_payable_amount(
		self,
		account,
		cost_center,
		amount,
		currencies,
		company_currency,
		payable_amount,
		accounting_dimensions,
		precision,
		entry_type="credit",
		party=None,
		accounts=None,
	):
		exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(
			account, amount, company_currency, currencies
		)

		row = {
			"account": account,
			"exchange_rate": flt(exchange_rate),
			"cost_center": cost_center,
			"project": self.project,
		}

		if entry_type == "debit":
			payable_amount += flt(amount, precision)
			row.update(
				{
					"debit_in_account_currency": flt(amt, precision),
				}
			)
		elif entry_type == "credit":
			payable_amount -= flt(amount, precision)
			row.update(
				{
					"credit_in_account_currency": flt(amt, precision),
				}
			)
		else:
			row.update(
				{
					"credit_in_account_currency": flt(amt, precision),
					"reference_type": self.doctype,
					"reference_name": self.name,
				}
			)

		if party:
			row.update(
				{
					"party_type": "Employee",
					"party": party,
				}
			)

		self.update_accounting_dimensions(
			row,
			accounting_dimensions,
		)

		if amt:
			accounts.append(row)

		return payable_amount

	def update_accounting_dimensions(self, row, accounting_dimensions):
		for dimension in accounting_dimensions:
			row.update({dimension: self.get(dimension)})

		return row

	def get_amount_and_exchange_rate_for_journal_entry(
		self, account, amount, company_currency, currencies
	):
		conversion_rate = 1
		exchange_rate = self.exchange_rate
		account_currency = xhiveframework.db.get_value("Account", account, "account_currency")
		if account_currency not in currencies:
			currencies.append(account_currency)
		if account_currency == company_currency:
			conversion_rate = self.exchange_rate
			exchange_rate = 1
		amount = flt(amount) * flt(conversion_rate)

		return exchange_rate, amount

	@xhiveframework.whitelist()
	def make_payment_entry(self):
		self.check_permission("write")
		self.employee_based_payroll_payable_entries = {}
		process_payroll_accounting_entry_based_on_employee = xhiveframework.db.get_single_value(
			"Payroll Settings", "process_payroll_accounting_entry_based_on_employee"
		)

		salary_slip_name_list = xhiveframework.db.sql(
			""" select t1.name from `tabSalary Slip` t1
			where t1.docstatus = 1 and start_date >= %s and end_date <= %s and t1.payroll_entry = %s
			""",
			(self.start_date, self.end_date, self.name),
			as_list=True,
		)

		if salary_slip_name_list and len(salary_slip_name_list) > 0:
			salary_slip_total = 0
			for salary_slip_name in salary_slip_name_list:
				salary_slip = xhiveframework.get_doc("Salary Slip", salary_slip_name[0])

				for sal_detail in salary_slip.earnings:
					(
						is_flexible_benefit,
						only_tax_impact,
						creat_separate_je,
						statistical_component,
					) = xhiveframework.db.get_value(
						"Salary Component",
						sal_detail.salary_component,
						[
							"is_flexible_benefit",
							"only_tax_impact",
							"create_separate_payment_entry_against_benefit_claim",
							"statistical_component",
						],
					)
					if only_tax_impact != 1 and statistical_component != 1:
						if is_flexible_benefit == 1 and creat_separate_je == 1:
							self.create_journal_entry(sal_detail.amount, sal_detail.salary_component)
						else:
							if process_payroll_accounting_entry_based_on_employee:
								self.set_employee_based_payroll_payable_entries(
									"earnings", salary_slip.employee, sal_detail.amount
								)
							salary_slip_total += sal_detail.amount

				for sal_detail in salary_slip.deductions:
					statistical_component = xhiveframework.db.get_value(
						"Salary Component", sal_detail.salary_component, "statistical_component"
					)
					if statistical_component != 1:
						if process_payroll_accounting_entry_based_on_employee:
							self.set_employee_based_payroll_payable_entries(
								"deductions", salary_slip.employee, sal_detail.amount
							)

						salary_slip_total -= sal_detail.amount

			if salary_slip_total > 0:
				self.create_journal_entry(salary_slip_total, "salary")

	def create_journal_entry(self, je_payment_amount, user_remark):
		payroll_payable_account = self.payroll_payable_account
		precision = xhiveframework.get_precision("Journal Entry Account", "debit_in_account_currency")

		accounts = []
		currencies = []
		multi_currency = 0
		company_currency = xhiveerp.get_company_currency(self.company)
		accounting_dimensions = get_accounting_dimensions() or []

		exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(
			self.payment_account, je_payment_amount, company_currency, currencies
		)
		accounts.append(
			self.update_accounting_dimensions(
				{
					"account": self.payment_account,
					"bank_account": self.bank_account,
					"credit_in_account_currency": flt(amount, precision),
					"exchange_rate": flt(exchange_rate),
				},
				accounting_dimensions,
			)
		)

		if self.employee_based_payroll_payable_entries:
			for employee, employee_details in self.employee_based_payroll_payable_entries.items():
				je_payment_amount = employee_details.get("earnings") - (
					employee_details.get("deductions") or 0
				)
				exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(
					self.payment_account, je_payment_amount, company_currency, currencies
				)

				accounts.append(
					self.update_accounting_dimensions(
						{
							"account": payroll_payable_account,
							"debit_in_account_currency": flt(amount, precision),
							"exchange_rate": flt(exchange_rate),
							"reference_type": self.doctype,
							"reference_name": self.name,
							"party_type": "Employee",
							"party": employee,
						},
						accounting_dimensions,
					)
				)
		else:
			exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(
				payroll_payable_account, je_payment_amount, company_currency, currencies
			)
			accounts.append(
				self.update_accounting_dimensions(
					{
						"account": payroll_payable_account,
						"debit_in_account_currency": flt(amount, precision),
						"exchange_rate": flt(exchange_rate),
						"reference_type": self.doctype,
						"reference_name": self.name,
					},
					accounting_dimensions,
				)
			)

		if len(currencies) > 1:
			multi_currency = 1

		journal_entry = xhiveframework.new_doc("Journal Entry")
		journal_entry.voucher_type = "Bank Entry"
		journal_entry.user_remark = _("Payment of {0} from {1} to {2}").format(
			user_remark, self.start_date, self.end_date
		)
		journal_entry.company = self.company
		journal_entry.posting_date = self.posting_date
		journal_entry.multi_currency = multi_currency

		journal_entry.set("accounts", accounts)
		journal_entry.save(ignore_permissions=True)

	def update_salary_slip_status(self, jv_name=None):
		ss_list = self.get_sal_slip_list(ss_status=1)
		for ss in ss_list:
			ss_obj = xhiveframework.get_doc("Salary Slip", ss[0])
			xhiveframework.db.set_value("Salary Slip", ss_obj.name, "journal_entry", jv_name)

	def set_start_end_dates(self):
		self.update(
			get_start_end_dates(self.payroll_frequency, self.start_date or self.posting_date, self.company)
		)

	@xhiveframework.whitelist()
	def validate_employee_attendance(self):
		employees_to_mark_attendance = []
		days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0
		for employee_detail in self.employees:
			employee_joining_date = xhiveframework.db.get_value(
				"Employee", employee_detail.employee, "date_of_joining"
			)
			start_date = self.start_date

			if employee_joining_date > getdate(self.start_date):
				start_date = employee_joining_date

			days_holiday = self.get_count_holidays_of_employee(employee_detail.employee, start_date)
			days_attendance_marked = self.get_count_employee_attendance(
				employee_detail.employee, start_date
			)
			days_in_payroll = date_diff(self.end_date, start_date) + 1

			if days_in_payroll > days_holiday + days_attendance_marked:
				employees_to_mark_attendance.append(
					{"employee": employee_detail.employee, "employee_name": employee_detail.employee_name}
				)

		return employees_to_mark_attendance

	def get_count_holidays_of_employee(self, employee, start_date):
		holiday_list = get_holiday_list_for_employee(employee)
		holidays = 0
		if holiday_list:
			days = xhiveframework.db.sql(
				"""select count(*) from tabHoliday where
				parent=%s and holiday_date between %s and %s""",
				(holiday_list, start_date, self.end_date),
			)
			if days and days[0][0]:
				holidays = days[0][0]
		return holidays

	def get_count_employee_attendance(self, employee, start_date):
		marked_days = 0
		attendances = xhiveframework.get_all(
			"Attendance",
			fields=["count(*)"],
			filters={"employee": employee, "attendance_date": ("between", [start_date, self.end_date])},
			as_list=1,
		)
		if attendances and attendances[0][0]:
			marked_days = attendances[0][0]
		return marked_days


def get_sal_struct(
	company: str, currency: str, salary_slip_based_on_timesheet: int, condition: str
):
	return xhiveframework.db.sql_list(
		"""
		select
			name from `tabSalary Structure`
		where
			docstatus = 1 and
			is_active = 'Yes'
			and company = %(company)s
			and currency = %(currency)s and
			ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s
			{condition}""".format(
			condition=condition
		),
		{
			"company": company,
			"currency": currency,
			"salary_slip_based_on_timesheet": salary_slip_based_on_timesheet,
		},
	)


def get_filter_condition(filters):
	cond = ""
	for f in ["company", "branch", "department", "designation"]:
		if filters.get(f):
			cond += " and t1." + f + " = " + xhiveframework.db.escape(filters.get(f))

	return cond


def get_joining_relieving_condition(start_date, end_date):
	cond = """
		and ifnull(t1.date_of_joining, '1900-01-01') <= '%(end_date)s'
		and ifnull(t1.relieving_date, '2199-12-31') >= '%(start_date)s'
	""" % {
		"start_date": start_date,
		"end_date": end_date,
	}
	return cond


def get_emp_list(sal_struct, cond, end_date, payroll_payable_account):
	return xhiveframework.db.sql(
		"""
			select
				distinct t1.name as employee, t1.employee_name, t1.department, t1.designation
			from
				`tabEmployee` t1, `tabSalary Structure Assignment` t2
			where
				t1.name = t2.employee
				and t2.docstatus = 1
				and t1.status != 'Inactive'
		%s order by t2.from_date desc
		"""
		% cond,
		{
			"sal_struct": tuple(sal_struct),
			"from_date": end_date,
			"payroll_payable_account": payroll_payable_account,
		},
		as_dict=True,
	)


def remove_payrolled_employees(emp_list, start_date, end_date):
	new_emp_list = []
	for employee_details in emp_list:
		if not xhiveframework.db.exists(
			"Salary Slip",
			{
				"employee": employee_details.employee,
				"start_date": start_date,
				"end_date": end_date,
				"docstatus": 1,
			},
		):
			new_emp_list.append(employee_details)

	return new_emp_list


@xhiveframework.whitelist()
def get_start_end_dates(payroll_frequency, start_date=None, company=None):
	"""Returns dict of start and end dates for given payroll frequency based on start_date"""

	if payroll_frequency == "Monthly" or payroll_frequency == "Bimonthly" or payroll_frequency == "":
		fiscal_year = get_fiscal_year(start_date, company=company)[0]
		month = "%02d" % getdate(start_date).month
		m = get_month_details(fiscal_year, month)
		if payroll_frequency == "Bimonthly":
			if getdate(start_date).day <= 15:
				start_date = m["month_start_date"]
				end_date = m["month_mid_end_date"]
			else:
				start_date = m["month_mid_start_date"]
				end_date = m["month_end_date"]
		else:
			start_date = m["month_start_date"]
			end_date = m["month_end_date"]

	if payroll_frequency == "Weekly":
		end_date = add_days(start_date, 6)

	if payroll_frequency == "Fortnightly":
		end_date = add_days(start_date, 13)

	if payroll_frequency == "Daily":
		end_date = start_date

	return xhiveframework._dict({"start_date": start_date, "end_date": end_date})


def get_frequency_kwargs(frequency_name):
	frequency_dict = {
		"monthly": {"months": 1},
		"fortnightly": {"days": 14},
		"weekly": {"days": 7},
		"daily": {"days": 1},
	}
	return frequency_dict.get(frequency_name)


@xhiveframework.whitelist()
def get_end_date(start_date, frequency):
	start_date = getdate(start_date)
	frequency = frequency.lower() if frequency else "monthly"
	kwargs = (
		get_frequency_kwargs(frequency) if frequency != "bimonthly" else get_frequency_kwargs("monthly")
	)

	# weekly, fortnightly and daily intervals have fixed days so no problems
	end_date = add_to_date(start_date, **kwargs) - relativedelta(days=1)
	if frequency != "bimonthly":
		return dict(end_date=end_date.strftime(DATE_FORMAT))

	else:
		return dict(end_date="")


def get_month_details(year, month):
	ysd = xhiveframework.db.get_value("Fiscal Year", year, "year_start_date")
	if ysd:
		import calendar
		import datetime

		diff_mnt = cint(month) - cint(ysd.month)
		if diff_mnt < 0:
			diff_mnt = 12 - int(ysd.month) + cint(month)
		msd = ysd + relativedelta(months=diff_mnt)  # month start date
		month_days = cint(calendar.monthrange(cint(msd.year), cint(month))[1])  # days in month
		mid_start = datetime.date(msd.year, cint(month), 16)  # month mid start date
		mid_end = datetime.date(msd.year, cint(month), 15)  # month mid end date
		med = datetime.date(msd.year, cint(month), month_days)  # month end date
		return xhiveframework._dict(
			{
				"year": msd.year,
				"month_start_date": msd,
				"month_end_date": med,
				"month_mid_start_date": mid_start,
				"month_mid_end_date": mid_end,
				"month_days": month_days,
			}
		)
	else:
		xhiveframework.throw(_("Fiscal Year {0} not found").format(year))


def get_payroll_entry_bank_entries(payroll_entry_name):
	je = xhiveframework.qb.DocType("Journal Entry")
	jea = xhiveframework.qb.DocType("Journal Entry Account")

	journal_entries = (
		xhiveframework.qb.from_(je)
		.from_(jea)
		.select(je.name)
		.where(
			(je.name == jea.parent)
			& (je.voucher_type == "Bank Entry")
			& (jea.reference_name == payroll_entry_name)
			& (jea.reference_type == "Payroll Entry")
		)
	).run(as_dict=True)

	return journal_entries


@xhiveframework.whitelist()
def payroll_entry_has_bank_entries(name: str):
	response = {}
	bank_entries = get_payroll_entry_bank_entries(name)
	response["submitted"] = 1 if bank_entries else 0

	return response


def log_payroll_failure(process, payroll_entry, error):
	error_log = xhiveframework.log_error(
		title=_("Salary Slip {0} failed for Payroll Entry {1}").format(process, payroll_entry.name)
	)
	message_log = xhiveframework.message_log.pop() if xhiveframework.message_log else str(error)

	try:
		error_message = json.loads(message_log).get("message")
	except Exception:
		error_message = message_log

	error_message += "\n" + _("Check Error Log {0} for more details.").format(
		get_link_to_form("Error Log", error_log.name)
	)

	payroll_entry.db_set({"error_message": error_message, "status": "Failed"})


def create_salary_slips_for_employees(employees, args, publish_progress=True):
	try:
		payroll_entry = xhiveframework.get_doc("Payroll Entry", args.payroll_entry)
		salary_slips_exist_for = get_existing_salary_slips(employees, args)
		count = 0

		for emp in employees:
			if emp not in salary_slips_exist_for:
				args.update({"doctype": "Salary Slip", "employee": emp})
				xhiveframework.get_doc(args).insert()

				count += 1
				if publish_progress:
					xhiveframework.publish_progress(
						count * 100 / len(set(employees) - set(salary_slips_exist_for)),
						title=_("Creating Salary Slips..."),
					)

		payroll_entry.db_set({"status": "Submitted", "salary_slips_created": 1, "error_message": ""})

		if salary_slips_exist_for:
			xhiveframework.msgprint(
				_(
					"Salary Slips already exist for employees {}, and will not be processed by this payroll."
				).format(xhiveframework.bold(", ".join(emp for emp in salary_slips_exist_for))),
				title=_("Message"),
				indicator="orange",
			)

	except Exception as e:
		xhiveframework.db.rollback()
		log_payroll_failure("creation", payroll_entry, e)

	finally:
		xhiveframework.db.commit()  # nosemgrep
		xhiveframework.publish_realtime("completed_salary_slip_creation")


def show_payroll_submission_status(submitted, unsubmitted, payroll_entry):
	if not submitted and not unsubmitted:
		xhiveframework.msgprint(
			_(
				"No salary slip found to submit for the above selected criteria OR salary slip already submitted"
			)
		)
	elif submitted and not unsubmitted:
		xhiveframework.msgprint(
			_("Salary Slips submitted for period from {0} to {1}").format(
				payroll_entry.start_date, payroll_entry.end_date
			)
		)
	elif unsubmitted:
		xhiveframework.msgprint(
			_("Could not submit some Salary Slips: {}").format(
				", ".join(get_link_to_form("Salary Slip", entry) for entry in unsubmitted)
			)
		)


def get_existing_salary_slips(employees, args):
	return xhiveframework.db.sql_list(
		"""
		select distinct employee from `tabSalary Slip`
		where docstatus!= 2 and company = %s and payroll_entry = %s
			and start_date >= %s and end_date <= %s
			and employee in (%s)
	"""
		% ("%s", "%s", "%s", "%s", ", ".join(["%s"] * len(employees))),
		[args.company, args.payroll_entry, args.start_date, args.end_date] + employees,
	)


def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True):
	try:
		submitted = []
		unsubmitted = []
		xhiveframework.flags.via_payroll_entry = True
		count = 0

		for entry in salary_slips:
			salary_slip = xhiveframework.get_doc("Salary Slip", entry[0])
			if salary_slip.net_pay < 0:
				unsubmitted.append(entry[0])
			else:
				try:
					salary_slip.submit()
					submitted.append(salary_slip)
				except xhiveframework.ValidationError:
					unsubmitted.append(entry[0])

			count += 1
			if publish_progress:
				xhiveframework.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))

		if submitted:
			payroll_entry.make_accrual_jv_entry()
			payroll_entry.email_salary_slip(submitted)
			payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted", "error_message": ""})

		show_payroll_submission_status(submitted, unsubmitted, payroll_entry)

	except Exception as e:
		xhiveframework.db.rollback()
		log_payroll_failure("submission", payroll_entry, e)

	finally:
		xhiveframework.db.commit()  # nosemgrep
		xhiveframework.publish_realtime("completed_salary_slip_submission")

	xhiveframework.flags.via_payroll_entry = False


@xhiveframework.whitelist()
@xhiveframework.validate_and_sanitize_search_inputs
def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters):
	return xhiveframework.db.sql(
		"""
		select name from `tabPayroll Entry`
		where `{key}` LIKE %(txt)s
		and name not in
			(select reference_name from `tabJournal Entry Account`
				where reference_type="Payroll Entry")
		order by name limit %(start)s, %(page_len)s""".format(
			key=searchfield
		),
		{"txt": "%%%s%%" % txt, "start": start, "page_len": page_len},
	)


def get_employee_list(filters: xhiveframework._dict) -> list[str]:
	condition = f"and payroll_frequency = '{filters.payroll_frequency}'"

	sal_struct = get_sal_struct(
		filters.company, filters.currency, filters.salary_slip_based_on_timesheet, condition
	)

	if not sal_struct:
		return []

	cond = (
		get_filter_condition(filters)
		+ get_joining_relieving_condition(filters.start_date, filters.end_date)
		+ (
			"and t2.salary_structure IN %(sal_struct)s "
			"and t2.payroll_payable_account = %(payroll_payable_account)s "
			"and %(from_date)s >= t2.from_date"
		)
	)
	emp_list = get_emp_list(sal_struct, cond, filters.end_date, filters.payroll_payable_account)
	return remove_payrolled_employees(emp_list, filters.start_date, filters.end_date)


@xhiveframework.whitelist()
@xhiveframework.validate_and_sanitize_search_inputs
def employee_query(doctype, txt, searchfield, start, page_len, filters):
	filters = xhiveframework._dict(filters)
	conditions = []
	include_employees = []
	emp_cond = ""
	doctype = "Employee"

	if not filters.payroll_frequency:
		xhiveframework.throw(_("Select Payroll Frequency."))

	if filters.start_date and filters.end_date:
		employee_list = get_employee_list(filters)
		emp = filters.get("employees") or []
		include_employees = [
			employee.employee for employee in employee_list if employee.employee not in emp
		]
		filters.pop("start_date")
		filters.pop("end_date")
		filters.pop("salary_slip_based_on_timesheet")
		filters.pop("payroll_frequency")
		filters.pop("payroll_payable_account")
		filters.pop("currency")
		if filters.employees is not None:
			filters.pop("employees")

		if include_employees:
			emp_cond += "and employee in %(include_employees)s"

	return xhiveframework.db.sql(
		"""select name, employee_name from `tabEmployee`
		where status = 'Active'
			and docstatus < 2
			and ({key} like %(txt)s
				or employee_name like %(txt)s)
			{emp_cond}
			{fcond} {mcond}
		order by
			if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
			if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
			idx desc,
			name, employee_name
		limit %(start)s, %(page_len)s""".format(
			**{
				"key": searchfield,
				"fcond": get_filters_cond(doctype, filters, conditions),
				"mcond": get_match_cond(doctype),
				"emp_cond": emp_cond,
			}
		),
		{
			"txt": "%%%s%%" % txt,
			"_txt": txt.replace("%", ""),
			"start": start,
			"page_len": page_len,
			"include_employees": include_employees,
		},
	)
