diff --git a/hrms/hr/doctype/employee_onboarding/employee_onboarding.json b/hrms/hr/doctype/employee_onboarding/employee_onboarding.json index 1d2ea0c669..c70635094b 100644 --- a/hrms/hr/doctype/employee_onboarding/employee_onboarding.json +++ b/hrms/hr/doctype/employee_onboarding/employee_onboarding.json @@ -174,7 +174,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2022-01-29 12:33:57.120384", + "modified": "2024-02-12 19:33:57.120384", "modified_by": "Administrator", "module": "HR", "name": "Employee Onboarding", @@ -195,7 +195,19 @@ "share": 1, "submit": 1, "write": 1 - } + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + } ], "sort_field": "modified", "sort_order": "DESC", diff --git a/hrms/hr/doctype/expense_claim/expense_claim.py b/hrms/hr/doctype/expense_claim/expense_claim.py index a3ff9f3017..b847b67b51 100644 --- a/hrms/hr/doctype/expense_claim/expense_claim.py +++ b/hrms/hr/doctype/expense_claim/expense_claim.py @@ -54,31 +54,26 @@ def set_status(self, update=False): precision = self.precision("grand_total") - if ( - # set as paid - self.is_paid - or ( - flt(self.total_sanctioned_amount) > 0 - and ( - # grand total is reimbursed - ( - self.docstatus == 1 - and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision) + if self.docstatus == 1: + if self.approval_status == "Approved": + if ( + # set as paid + self.is_paid + or ( + flt(self.total_sanctioned_amount) > 0 + and ( + # grand total is reimbursed + (flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision)) + # grand total (to be paid) is 0 since linked advances already cover the claimed amount + or (flt(self.grand_total, precision) == 0) + ) ) - # grand total (to be paid) is 0 since linked advances already cover the claimed amount - or (flt(self.grand_total, precision) == 0) - ) - ) - ) and self.approval_status == "Approved": - status = "Paid" - elif ( - flt(self.total_sanctioned_amount) > 0 - and self.docstatus == 1 - and self.approval_status == "Approved" - ): - status = "Unpaid" - elif self.docstatus == 1 and self.approval_status == "Rejected": - status = "Rejected" + ): + status = "Paid" + elif flt(self.total_sanctioned_amount) > 0: + status = "Unpaid" + elif self.approval_status == "Rejected": + status = "Rejected" if update: self.db_set("status", status) diff --git a/hrms/hr/doctype/leave_application/leave_application_dashboard.html b/hrms/hr/doctype/leave_application/leave_application_dashboard.html index c5615a0afb..6b3961cee9 100644 --- a/hrms/hr/doctype/leave_application/leave_application_dashboard.html +++ b/hrms/hr/doctype/leave_application/leave_application_dashboard.html @@ -26,5 +26,5 @@ {% else %} -

No Leave has been allocated.

+

{{ __("No leaves have been allocated.") }}

{% endif %} diff --git a/hrms/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/hrms/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py index 925453743c..938172a472 100644 --- a/hrms/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +++ b/hrms/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py @@ -160,12 +160,13 @@ def get_columns_for_days(filters: Filters) -> List[Dict]: days = [] for day in range(1, total_days + 1): + day = cstr(day) # forms the dates from selected year and month from filters - date = "{}-{}-{}".format(cstr(filters.year), cstr(filters.month), cstr(day)) + date = "{}-{}-{}".format(cstr(filters.year), cstr(filters.month), day) # gets abbr from weekday number weekday = day_abbr[getdate(date).weekday()] # sets days as 1 Mon, 2 Tue, 3 Wed - label = "{} {}".format(cstr(day), weekday) + label = "{} {}".format(day, weekday) days.append({"label": label, "fieldtype": "Data", "fieldname": day, "width": 65}) return days @@ -619,7 +620,7 @@ def get_chart_data(attendance_map: Dict, filters: Filters) -> Dict: for employee, attendance_dict in attendance_map.items(): for shift, attendance in attendance_dict.items(): - attendance_on_day = attendance.get(day["fieldname"]) + attendance_on_day = attendance.get(cint(day["fieldname"])) if attendance_on_day == "On Leave": # leave should be counted only once for the entire day diff --git a/hrms/payroll/doctype/salary_slip/salary_slip.py b/hrms/payroll/doctype/salary_slip/salary_slip.py index bcffe62f63..4750339573 100644 --- a/hrms/payroll/doctype/salary_slip/salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/salary_slip.py @@ -9,7 +9,7 @@ from frappe import _, msgprint from frappe.model.naming import make_autoname from frappe.query_builder import Order -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Count, Sum from frappe.utils import ( add_days, ceil, @@ -479,63 +479,83 @@ def get_working_days_details(self, lwp=None, for_preview=0): payroll_settings.payroll_based_on == "Attendance" and consider_unmarked_attendance_as == "Absent" ): - unmarked_days = self.get_unmarked_days(payroll_settings.include_holidays_in_total_working_days) + unmarked_days = self.get_unmarked_days( + payroll_settings.include_holidays_in_total_working_days, holidays + ) self.absent_days += unmarked_days # will be treated as absent self.payment_days -= unmarked_days else: self.payment_days = 0 - def get_unmarked_days(self, include_holidays_in_total_working_days): - unmarked_days = self.total_working_days + def get_unmarked_days( + self, include_holidays_in_total_working_days: bool, holidays: list | None = None + ) -> float: + """Calculates the number of unmarked days for an employee within a date range""" + unmarked_days = ( + self.total_working_days + - self._get_days_outside_period(include_holidays_in_total_working_days, holidays) + - self._get_marked_attendance_days(holidays) + ) + + if include_holidays_in_total_working_days and holidays: + unmarked_days -= self._get_number_of_holidays(holidays) + + return unmarked_days + + def _get_days_outside_period( + self, include_holidays_in_total_working_days: bool, holidays: list | None = None + ): + """Returns days before DOJ or after relieving date""" + + def _get_days(start_date, end_date): + no_of_days = date_diff(end_date, start_date) + 1 + if include_holidays_in_total_working_days: + return no_of_days + else: + days = 0 + end_date = getdate(end_date) + for day in range(no_of_days): + date = add_days(end_date, -day) + if date not in holidays: + days += 1 + return days + + days = 0 if self.actual_start_date != self.start_date: - unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving( - unmarked_days, - include_holidays_in_total_working_days, - self.start_date, - add_days(self.joining_date, -1), - ) + days += _get_days(self.start_date, add_days(self.joining_date, -1)) if self.actual_end_date != self.end_date: - unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving( - unmarked_days, - include_holidays_in_total_working_days, - add_days(self.relieving_date, 1), - self.end_date, - ) + days += _get_days(add_days(self.relieving_date, 1), self.end_date) - # exclude days for which attendance has been marked - marked_days = frappe.db.count( - "Attendance", - filters={ - "attendance_date": ["between", [self.actual_start_date, self.actual_end_date]], - "employee": self.employee, - "docstatus": 1, - }, - ) - unmarked_days -= marked_days + return days - return unmarked_days + def _get_number_of_holidays(self, holidays: list | None = None) -> float: + no_of_holidays = 0 + actual_end_date = getdate(self.actual_end_date) - def get_unmarked_days_based_on_doj_or_relieving( - self, unmarked_days, include_holidays_in_total_working_days, start_date, end_date - ): - """ - Exclude days before DOJ or after - Relieving Date from unmarked days - """ - from erpnext.setup.doctype.employee.employee import is_holiday + for days in range(date_diff(self.actual_end_date, self.actual_start_date) + 1): + date = add_days(actual_end_date, -days) + if date in holidays: + no_of_holidays += 1 - if include_holidays_in_total_working_days: - unmarked_days -= date_diff(end_date, start_date) + 1 - else: - # exclude only if not holidays - for days in range(date_diff(end_date, start_date) + 1): - date = add_days(end_date, -days) - if not is_holiday(self.employee, date): - unmarked_days -= 1 + return no_of_holidays - return unmarked_days + def _get_marked_attendance_days(self, holidays: list | None = None) -> float: + Attendance = frappe.qb.DocType("Attendance") + query = ( + frappe.qb.from_(Attendance) + .select(Count("*")) + .where( + (Attendance.attendance_date.between(self.actual_start_date, self.actual_end_date)) + & (Attendance.employee == self.employee) + & (Attendance.docstatus == 1) + ) + ) + if holidays: + query = query.where(Attendance.attendance_date.notin(holidays)) + + return query.run()[0][0] def get_payment_days(self, include_holidays_in_total_working_days): if self.joining_date and self.joining_date > getdate(self.end_date): diff --git a/hrms/payroll/doctype/salary_slip/test_salary_slip.py b/hrms/payroll/doctype/salary_slip/test_salary_slip.py index ee23c453f1..2249d606f8 100644 --- a/hrms/payroll/doctype/salary_slip/test_salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/test_salary_slip.py @@ -232,14 +232,11 @@ def test_payment_days_for_mid_joinee_including_holidays_and_unmarked_days(self): new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) - holidays = 0 for days in range(date_diff(relieving_date, joining_date) + 1): date = add_days(joining_date, days) if not is_holiday("Salary Slip Test Holiday List", date): mark_attendance(new_emp_id, date, "Present", ignore_validate=True) - else: - holidays += 1 frappe.db.set_value( "Employee", @@ -254,7 +251,7 @@ def test_payment_days_for_mid_joinee_including_holidays_and_unmarked_days(self): ) self.assertEqual(new_ss.total_working_days, no_of_days[0]) - self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8) + self.assertEqual(new_ss.payment_days, no_of_days[0] - 8) @change_settings( "Payroll Settings", @@ -519,6 +516,55 @@ def test_consider_marked_attendance_on_holidays(self): ss.save() self.assertEqual(ss.total_working_days, no_of_days[0]) + @change_settings( + "Payroll Settings", + { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": 1, + "consider_marked_attendance_on_holidays": 1, + }, + ) + def test_consider_marked_attendance_on_holidays_with_unmarked_attendance(self): + from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday + + no_of_days = get_no_of_days() + month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) + joining_date = add_days(month_start_date, 3) + + emp_id = make_employee( + "test_salary_slip_with_holidays_included@salary.com", + status="Active", + joining_date=joining_date, + relieving_date=None, + ) + + for days in range(date_diff(month_end_date, add_days(joining_date, 1)) + 1): + date = add_days(joining_date, days) + if not is_holiday("Salary Slip Test Holiday List", date): + mark_attendance(emp_id, date, "Present", ignore_validate=True) + + # mark absent on holiday + first_sunday = get_first_sunday(for_date=getdate()) + mark_attendance(emp_id, first_sunday, "Absent", ignore_validate=True) + + ss = make_employee_salary_slip( + emp_id, + "Monthly", + "Test Salary Slip With Holidays Included", + ) + + self.assertEqual(ss.total_working_days, no_of_days[0]) + # no_of_days - absent on holiday - period before DOJ - 1 unmarked attendance + self.assertEqual(ss.payment_days, no_of_days[0] - 1 - 3 - 1) + + # disable consider marked attendance on holidays + frappe.db.set_single_value("Payroll Settings", "consider_marked_attendance_on_holidays", 0) + ss.save() + self.assertEqual(ss.total_working_days, no_of_days[0]) + # no_of_days - period before DOJ + self.assertEqual(ss.payment_days, no_of_days[0] - 3 - 1) + @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1}) def test_payment_days(self): from hrms.payroll.doctype.salary_structure.test_salary_structure import (