You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

677 lines
20 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: GNU General Public License v3. See license.txt
  3. import frappe
  4. from frappe import _
  5. from frappe.model.document import Document
  6. from frappe.utils import (
  7. add_days,
  8. cstr,
  9. flt,
  10. format_datetime,
  11. formatdate,
  12. get_datetime,
  13. get_link_to_form,
  14. getdate,
  15. nowdate,
  16. today,
  17. )
  18. import erpnext
  19. from erpnext import get_company_currency
  20. from erpnext.setup.doctype.employee.employee import (
  21. InactiveEmployeeStatusError,
  22. get_holiday_list_for_employee,
  23. )
  24. class DuplicateDeclarationError(frappe.ValidationError):
  25. pass
  26. def set_employee_name(doc):
  27. if doc.employee and not doc.employee_name:
  28. doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
  29. def update_employee_work_history(employee, details, date=None, cancel=False):
  30. if not employee.internal_work_history and not cancel:
  31. employee.append(
  32. "internal_work_history",
  33. {
  34. "branch": employee.branch,
  35. "designation": employee.designation,
  36. "department": employee.department,
  37. "from_date": employee.date_of_joining,
  38. },
  39. )
  40. internal_work_history = {}
  41. for item in details:
  42. field = frappe.get_meta("Employee").get_field(item.fieldname)
  43. if not field:
  44. continue
  45. fieldtype = field.fieldtype
  46. new_data = item.new if not cancel else item.current
  47. if fieldtype == "Date" and new_data:
  48. new_data = getdate(new_data)
  49. elif fieldtype == "Datetime" and new_data:
  50. new_data = get_datetime(new_data)
  51. setattr(employee, item.fieldname, new_data)
  52. if item.fieldname in ["department", "designation", "branch"]:
  53. internal_work_history[item.fieldname] = item.new
  54. if internal_work_history and not cancel:
  55. internal_work_history["from_date"] = date
  56. employee.append("internal_work_history", internal_work_history)
  57. if cancel:
  58. delete_employee_work_history(details, employee, date)
  59. return employee
  60. def delete_employee_work_history(details, employee, date):
  61. filters = {}
  62. for d in details:
  63. for history in employee.internal_work_history:
  64. if d.property == "Department" and history.department == d.new:
  65. department = d.new
  66. filters["department"] = department
  67. if d.property == "Designation" and history.designation == d.new:
  68. designation = d.new
  69. filters["designation"] = designation
  70. if d.property == "Branch" and history.branch == d.new:
  71. branch = d.new
  72. filters["branch"] = branch
  73. if date and date == history.from_date:
  74. filters["from_date"] = date
  75. if filters:
  76. frappe.db.delete("Employee Internal Work History", filters)
  77. employee.save()
  78. @frappe.whitelist()
  79. def get_employee_field_property(employee, fieldname):
  80. if employee and fieldname:
  81. field = frappe.get_meta("Employee").get_field(fieldname)
  82. value = frappe.db.get_value("Employee", employee, fieldname)
  83. options = field.options
  84. if field.fieldtype == "Date":
  85. value = formatdate(value)
  86. elif field.fieldtype == "Datetime":
  87. value = format_datetime(value)
  88. return {"value": value, "datatype": field.fieldtype, "label": field.label, "options": options}
  89. else:
  90. return False
  91. def validate_dates(doc, from_date, to_date):
  92. date_of_joining, relieving_date = frappe.db.get_value(
  93. "Employee", doc.employee, ["date_of_joining", "relieving_date"]
  94. )
  95. if getdate(from_date) > getdate(to_date):
  96. frappe.throw(_("To date can not be less than from date"))
  97. elif getdate(from_date) > getdate(nowdate()):
  98. frappe.throw(_("Future dates not allowed"))
  99. elif date_of_joining and getdate(from_date) < getdate(date_of_joining):
  100. frappe.throw(_("From date can not be less than employee's joining date"))
  101. elif relieving_date and getdate(to_date) > getdate(relieving_date):
  102. frappe.throw(_("To date can not greater than employee's relieving date"))
  103. def validate_overlap(doc, from_date, to_date, company=None):
  104. query = """
  105. select name
  106. from `tab{0}`
  107. where name != %(name)s
  108. """
  109. query += get_doc_condition(doc.doctype)
  110. if not doc.name:
  111. # hack! if name is null, it could cause problems with !=
  112. doc.name = "New " + doc.doctype
  113. overlap_doc = frappe.db.sql(
  114. query.format(doc.doctype),
  115. {
  116. "employee": doc.get("employee"),
  117. "from_date": from_date,
  118. "to_date": to_date,
  119. "name": doc.name,
  120. "company": company,
  121. },
  122. as_dict=1,
  123. )
  124. if overlap_doc:
  125. if doc.get("employee"):
  126. exists_for = doc.employee
  127. if company:
  128. exists_for = company
  129. throw_overlap_error(doc, exists_for, overlap_doc[0].name, from_date, to_date)
  130. def get_doc_condition(doctype):
  131. if doctype == "Compensatory Leave Request":
  132. return "and employee = %(employee)s and docstatus < 2 \
  133. and (work_from_date between %(from_date)s and %(to_date)s \
  134. or work_end_date between %(from_date)s and %(to_date)s \
  135. or (work_from_date < %(from_date)s and work_end_date > %(to_date)s))"
  136. elif doctype == "Leave Period":
  137. return "and company = %(company)s and (from_date between %(from_date)s and %(to_date)s \
  138. or to_date between %(from_date)s and %(to_date)s \
  139. or (from_date < %(from_date)s and to_date > %(to_date)s))"
  140. def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date):
  141. msg = (
  142. _("A {0} exists between {1} and {2} (").format(
  143. doc.doctype, formatdate(from_date), formatdate(to_date)
  144. )
  145. + """ <b><a href="/app/Form/{0}/{1}">{1}</a></b>""".format(doc.doctype, overlap_doc)
  146. + _(") for {0}").format(exists_for)
  147. )
  148. frappe.throw(msg)
  149. def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee):
  150. existing_record = frappe.db.exists(
  151. doctype,
  152. {
  153. "payroll_period": payroll_period,
  154. "employee": employee,
  155. "docstatus": ["<", 2],
  156. "name": ["!=", docname],
  157. },
  158. )
  159. if existing_record:
  160. frappe.throw(
  161. _("{0} already exists for employee {1} and period {2}").format(
  162. doctype, employee, payroll_period
  163. ),
  164. DuplicateDeclarationError,
  165. )
  166. def validate_tax_declaration(declarations):
  167. subcategories = []
  168. for d in declarations:
  169. if d.exemption_sub_category in subcategories:
  170. frappe.throw(_("More than one selection for {0} not allowed").format(d.exemption_sub_category))
  171. subcategories.append(d.exemption_sub_category)
  172. def get_total_exemption_amount(declarations):
  173. exemptions = frappe._dict()
  174. for d in declarations:
  175. exemptions.setdefault(d.exemption_category, frappe._dict())
  176. category_max_amount = exemptions.get(d.exemption_category).max_amount
  177. if not category_max_amount:
  178. category_max_amount = frappe.db.get_value(
  179. "Employee Tax Exemption Category", d.exemption_category, "max_amount"
  180. )
  181. exemptions.get(d.exemption_category).max_amount = category_max_amount
  182. sub_category_exemption_amount = (
  183. d.max_amount if (d.max_amount and flt(d.amount) > flt(d.max_amount)) else d.amount
  184. )
  185. exemptions.get(d.exemption_category).setdefault("total_exemption_amount", 0.0)
  186. exemptions.get(d.exemption_category).total_exemption_amount += flt(sub_category_exemption_amount)
  187. if (
  188. category_max_amount
  189. and exemptions.get(d.exemption_category).total_exemption_amount > category_max_amount
  190. ):
  191. exemptions.get(d.exemption_category).total_exemption_amount = category_max_amount
  192. total_exemption_amount = sum([flt(d.total_exemption_amount) for d in exemptions.values()])
  193. return total_exemption_amount
  194. @frappe.whitelist()
  195. def get_leave_period(from_date, to_date, company):
  196. leave_period = frappe.db.sql(
  197. """
  198. select name, from_date, to_date
  199. from `tabLeave Period`
  200. where company=%(company)s and is_active=1
  201. and (from_date between %(from_date)s and %(to_date)s
  202. or to_date between %(from_date)s and %(to_date)s
  203. or (from_date < %(from_date)s and to_date > %(to_date)s))
  204. """,
  205. {"from_date": from_date, "to_date": to_date, "company": company},
  206. as_dict=1,
  207. )
  208. if leave_period:
  209. return leave_period
  210. def generate_leave_encashment():
  211. """Generates a draft leave encashment on allocation expiry"""
  212. from hrms.hr.doctype.leave_encashment.leave_encashment import create_leave_encashment
  213. if frappe.db.get_single_value("HR Settings", "auto_leave_encashment"):
  214. leave_type = frappe.get_all("Leave Type", filters={"allow_encashment": 1}, fields=["name"])
  215. leave_type = [l["name"] for l in leave_type]
  216. leave_allocation = frappe.get_all(
  217. "Leave Allocation",
  218. filters={"to_date": add_days(today(), -1), "leave_type": ("in", leave_type)},
  219. fields=[
  220. "employee",
  221. "leave_period",
  222. "leave_type",
  223. "to_date",
  224. "total_leaves_allocated",
  225. "new_leaves_allocated",
  226. ],
  227. )
  228. create_leave_encashment(leave_allocation=leave_allocation)
  229. def allocate_earned_leaves():
  230. """Allocate earned leaves to Employees"""
  231. e_leave_types = get_earned_leaves()
  232. today = getdate()
  233. for e_leave_type in e_leave_types:
  234. leave_allocations = get_leave_allocations(today, e_leave_type.name)
  235. for allocation in leave_allocations:
  236. if not allocation.leave_policy_assignment and not allocation.leave_policy:
  237. continue
  238. leave_policy = (
  239. allocation.leave_policy
  240. if allocation.leave_policy
  241. else frappe.db.get_value(
  242. "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"]
  243. )
  244. )
  245. annual_allocation = frappe.db.get_value(
  246. "Leave Policy Detail",
  247. filters={"parent": leave_policy, "leave_type": e_leave_type.name},
  248. fieldname=["annual_allocation"],
  249. )
  250. from_date = allocation.from_date
  251. if e_leave_type.based_on_date_of_joining:
  252. from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
  253. if check_effective_date(
  254. from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
  255. ):
  256. update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
  257. def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
  258. earned_leaves = get_monthly_earned_leave(
  259. annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
  260. )
  261. allocation = frappe.get_doc("Leave Allocation", allocation.name)
  262. new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
  263. if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
  264. new_allocation = e_leave_type.max_leaves_allowed
  265. if new_allocation != allocation.total_leaves_allocated:
  266. today_date = today()
  267. allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
  268. create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
  269. if e_leave_type.based_on_date_of_joining:
  270. text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
  271. frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
  272. )
  273. else:
  274. text = _("allocated {0} leave(s) via scheduler on {1}").format(
  275. frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
  276. )
  277. allocation.add_comment(comment_type="Info", text=text)
  278. def get_monthly_earned_leave(annual_leaves, frequency, rounding):
  279. earned_leaves = 0.0
  280. divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
  281. if annual_leaves:
  282. earned_leaves = flt(annual_leaves) / divide_by_frequency[frequency]
  283. if rounding:
  284. if rounding == "0.25":
  285. earned_leaves = round(earned_leaves * 4) / 4
  286. elif rounding == "0.5":
  287. earned_leaves = round(earned_leaves * 2) / 2
  288. else:
  289. earned_leaves = round(earned_leaves)
  290. return earned_leaves
  291. def is_earned_leave_already_allocated(allocation, annual_allocation):
  292. from hrms.hr.doctype.leave_policy_assignment.leave_policy_assignment import get_leave_type_details
  293. leave_type_details = get_leave_type_details()
  294. date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
  295. assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment)
  296. leaves_for_passed_months = assignment.get_leaves_for_passed_months(
  297. allocation.leave_type, annual_allocation, leave_type_details, date_of_joining
  298. )
  299. # exclude carry-forwarded leaves while checking for leave allocation for passed months
  300. num_allocations = allocation.total_leaves_allocated
  301. if allocation.unused_leaves:
  302. num_allocations -= allocation.unused_leaves
  303. if num_allocations >= leaves_for_passed_months:
  304. return True
  305. return False
  306. def get_leave_allocations(date, leave_type):
  307. return frappe.db.sql(
  308. """select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
  309. from `tabLeave Allocation`
  310. where
  311. %s between from_date and to_date and docstatus=1
  312. and leave_type=%s""",
  313. (date, leave_type),
  314. as_dict=1,
  315. )
  316. def get_earned_leaves():
  317. return frappe.get_all(
  318. "Leave Type",
  319. fields=[
  320. "name",
  321. "max_leaves_allowed",
  322. "earned_leave_frequency",
  323. "rounding",
  324. "based_on_date_of_joining",
  325. ],
  326. filters={"is_earned_leave": 1},
  327. )
  328. def create_additional_leave_ledger_entry(allocation, leaves, date):
  329. """Create leave ledger entry for leave types"""
  330. allocation.new_leaves_allocated = leaves
  331. allocation.from_date = date
  332. allocation.unused_leaves = 0
  333. allocation.create_leave_ledger_entry()
  334. def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining):
  335. import calendar
  336. from dateutil import relativedelta
  337. from_date = get_datetime(from_date)
  338. to_date = get_datetime(to_date)
  339. rd = relativedelta.relativedelta(to_date, from_date)
  340. # last day of month
  341. last_day = calendar.monthrange(to_date.year, to_date.month)[1]
  342. if (from_date.day == to_date.day and based_on_date_of_joining) or (
  343. not based_on_date_of_joining and to_date.day == last_day
  344. ):
  345. if frequency == "Monthly":
  346. return True
  347. elif frequency == "Quarterly" and rd.months % 3:
  348. return True
  349. elif frequency == "Half-Yearly" and rd.months % 6:
  350. return True
  351. elif frequency == "Yearly" and rd.months % 12:
  352. return True
  353. if frappe.flags.in_test:
  354. return True
  355. return False
  356. def get_salary_assignments(employee, payroll_period):
  357. start_date, end_date = frappe.db.get_value(
  358. "Payroll Period", payroll_period, ["start_date", "end_date"]
  359. )
  360. assignments = frappe.db.get_all(
  361. "Salary Structure Assignment",
  362. filters={"employee": employee, "docstatus": 1, "from_date": ["between", (start_date, end_date)]},
  363. fields=["*"],
  364. order_by="from_date",
  365. )
  366. return assignments
  367. def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
  368. total_given_benefit_amount = 0
  369. query = """
  370. select sum(sd.amount) as total_amount
  371. from `tabSalary Slip` ss, `tabSalary Detail` sd
  372. where ss.employee=%(employee)s
  373. and ss.docstatus = 1 and ss.name = sd.parent
  374. and sd.is_flexible_benefit = 1 and sd.parentfield = "earnings"
  375. and sd.parenttype = "Salary Slip"
  376. and (ss.start_date between %(start_date)s and %(end_date)s
  377. or ss.end_date between %(start_date)s and %(end_date)s
  378. or (ss.start_date < %(start_date)s and ss.end_date > %(end_date)s))
  379. """
  380. if component:
  381. query += "and sd.salary_component = %(component)s"
  382. sum_of_given_benefit = frappe.db.sql(
  383. query,
  384. {
  385. "employee": employee,
  386. "start_date": payroll_period.start_date,
  387. "end_date": payroll_period.end_date,
  388. "component": component,
  389. },
  390. as_dict=True,
  391. )
  392. if sum_of_given_benefit and flt(sum_of_given_benefit[0].total_amount) > 0:
  393. total_given_benefit_amount = sum_of_given_benefit[0].total_amount
  394. return total_given_benefit_amount
  395. def get_holiday_dates_for_employee(employee, start_date, end_date):
  396. """return a list of holiday dates for the given employee between start_date and end_date"""
  397. # return only date
  398. holidays = get_holidays_for_employee(employee, start_date, end_date)
  399. return [cstr(h.holiday_date) for h in holidays]
  400. def get_holidays_for_employee(
  401. employee, start_date, end_date, raise_exception=True, only_non_weekly=False
  402. ):
  403. """Get Holidays for a given employee
  404. `employee` (str)
  405. `start_date` (str or datetime)
  406. `end_date` (str or datetime)
  407. `raise_exception` (bool)
  408. `only_non_weekly` (bool)
  409. return: list of dicts with `holiday_date` and `description`
  410. """
  411. holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception)
  412. if not holiday_list:
  413. return []
  414. filters = {"parent": holiday_list, "holiday_date": ("between", [start_date, end_date])}
  415. if only_non_weekly:
  416. filters["weekly_off"] = False
  417. holidays = frappe.get_all(
  418. "Holiday", fields=["description", "holiday_date"], filters=filters, order_by="holiday_date"
  419. )
  420. return holidays
  421. @erpnext.allow_regional
  422. def calculate_annual_eligible_hra_exemption(doc):
  423. # Don't delete this method, used for localization
  424. # Indian HRA Exemption Calculation
  425. return {}
  426. @erpnext.allow_regional
  427. def calculate_hra_exemption_for_period(doc):
  428. # Don't delete this method, used for localization
  429. # Indian HRA Exemption Calculation
  430. return {}
  431. def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, component=False):
  432. total_claimed_amount = 0
  433. query = """
  434. select sum(claimed_amount) as 'total_amount'
  435. from `tabEmployee Benefit Claim`
  436. where employee=%(employee)s
  437. and docstatus = 1
  438. and (claim_date between %(start_date)s and %(end_date)s)
  439. """
  440. if non_pro_rata:
  441. query += "and pay_against_benefit_claim = 1"
  442. if component:
  443. query += "and earning_component = %(component)s"
  444. sum_of_claimed_amount = frappe.db.sql(
  445. query,
  446. {
  447. "employee": employee,
  448. "start_date": payroll_period.start_date,
  449. "end_date": payroll_period.end_date,
  450. "component": component,
  451. },
  452. as_dict=True,
  453. )
  454. if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0:
  455. total_claimed_amount = sum_of_claimed_amount[0].total_amount
  456. return total_claimed_amount
  457. def share_doc_with_approver(doc, user):
  458. # if approver does not have permissions, share
  459. if not frappe.has_permission(doc=doc, ptype="submit", user=user):
  460. frappe.share.add(doc.doctype, doc.name, user, submit=1, flags={"ignore_share_permission": True})
  461. frappe.msgprint(
  462. _("Shared with the user {0} with {1} access").format(user, frappe.bold("submit"), alert=True)
  463. )
  464. # remove shared doc if approver changes
  465. doc_before_save = doc.get_doc_before_save()
  466. if doc_before_save:
  467. approvers = {
  468. "Leave Application": "leave_approver",
  469. "Expense Claim": "expense_approver",
  470. "Shift Request": "approver",
  471. }
  472. approver = approvers.get(doc.doctype)
  473. if doc_before_save.get(approver) != doc.get(approver):
  474. frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver))
  475. def validate_active_employee(employee, method=None):
  476. if isinstance(employee, (dict, Document)):
  477. employee = employee.get("employee")
  478. if employee and frappe.db.get_value("Employee", employee, "status") == "Inactive":
  479. frappe.throw(
  480. _("Transactions cannot be created for an Inactive Employee {0}.").format(
  481. get_link_to_form("Employee", employee)
  482. ),
  483. InactiveEmployeeStatusError,
  484. )
  485. def validate_loan_repay_from_salary(doc, method=None):
  486. if doc.applicant_type == "Employee" and doc.repay_from_salary:
  487. from hrms.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
  488. get_employee_currency,
  489. )
  490. if not doc.applicant:
  491. frappe.throw(_("Please select an Applicant"))
  492. if not doc.company:
  493. frappe.throw(_("Please select a Company"))
  494. employee_currency = get_employee_currency(doc.applicant)
  495. company_currency = erpnext.get_company_currency(doc.company)
  496. if employee_currency != company_currency:
  497. frappe.throw(
  498. _(
  499. "Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}"
  500. ).format(doc.applicant, employee_currency)
  501. )
  502. if not doc.is_term_loan and doc.repay_from_salary:
  503. frappe.throw(_("Repay From Salary can be selected only for term loans"))
  504. def get_matching_queries(
  505. bank_account, company, transaction, document_types, amount_condition, account_from_to
  506. ):
  507. """Returns matching queries for Bank Reconciliation"""
  508. queries = []
  509. if transaction.withdrawal > 0:
  510. if "expense_claim" in document_types:
  511. ec_amount_matching = get_ec_matching_query(bank_account, company, amount_condition)
  512. queries.extend([ec_amount_matching])
  513. return queries
  514. def get_ec_matching_query(bank_account, company, amount_condition):
  515. # get matching Expense Claim query
  516. mode_of_payments = [
  517. x["parent"]
  518. for x in frappe.db.get_all(
  519. "Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"]
  520. )
  521. ]
  522. mode_of_payments = "('" + "', '".join(mode_of_payments) + "' )"
  523. company_currency = get_company_currency(company)
  524. return f"""
  525. SELECT
  526. ( CASE WHEN employee = %(party)s THEN 1 ELSE 0 END
  527. + 1 ) AS rank,
  528. 'Expense Claim' as doctype,
  529. name,
  530. total_sanctioned_amount as paid_amount,
  531. '' as reference_no,
  532. '' as reference_date,
  533. employee as party,
  534. 'Employee' as party_type,
  535. posting_date,
  536. '{company_currency}' as currency
  537. FROM
  538. `tabExpense Claim`
  539. WHERE
  540. total_sanctioned_amount {amount_condition} %(amount)s
  541. AND docstatus = 1
  542. AND is_paid = 1
  543. AND ifnull(clearance_date, '') = ""
  544. AND mode_of_payment in {mode_of_payments}
  545. """