Skip to content

Commit

Permalink
Merge pull request #2667 from frappe/version-14-hotfix
Browse files Browse the repository at this point in the history
chore: release v14
  • Loading branch information
asmitahase authored Jan 23, 2025
2 parents e3a2e6e + 32ceaee commit 4ae938f
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ frappe.ui.form.on("Full and Final Outstanding Statement", {
args: {
ref_doctype: child.reference_document_type,
ref_document: child.reference_document,
company: frm.doc.company,
},
callback: function (r) {
if (r.message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def create_journal_entry(self):


@frappe.whitelist()
def get_account_and_amount(ref_doctype, ref_document):
def get_account_and_amount(ref_doctype, ref_document, company):
if not ref_doctype or not ref_document:
return None

Expand Down Expand Up @@ -215,6 +215,11 @@ def get_account_and_amount(ref_doctype, ref_document):
amount = details.paid_amount - (details.claimed_amount + details.return_amount)
return [payment_account, amount]

if ref_doctype == "Leave Encashment":
amount = frappe.db.get_value("Leave Encashment", ref_document, "encashment_amount")
payable_account = frappe.get_cached_value("Company", company, "default_payroll_payable_account")
return [payable_account, amount]


def update_full_and_final_statement_status(doc, method=None):
"""Updates FnF status on Journal Entry Submission/Cancellation"""
Expand Down
5 changes: 3 additions & 2 deletions hrms/hr/doctype/job_applicant/job_applicant.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "job_title.designation",
"fetch_if_empty": 1,
"fieldname": "designation",
"fieldtype": "Link",
Expand All @@ -193,7 +194,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-09-14 16:50:39.316079",
"modified": "2025-01-16 13:06:05.312255",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
Expand All @@ -219,4 +220,4 @@
"states": [],
"subject_field": "notes",
"title_field": "applicant_name"
}
}
107 changes: 105 additions & 2 deletions hrms/hr/doctype/leave_encashment/test_leave_encashment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from erpnext.setup.doctype.employee.test_employee import make_employee
from erpnext.setup.doctype.holiday_list.test_holiday_list import set_holiday_list

from hrms.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
from hrms.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from hrms.hr.doctype.leave_period.test_leave_period import create_leave_period
from hrms.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
from hrms.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
Expand All @@ -31,8 +33,10 @@ def setUp(self):
frappe.db.delete("Additional Salary")
frappe.db.delete("Leave Encashment")

if not frappe.db.exists("Leave Type", "_Test Leave Type Encashment"):
frappe.get_doc(test_records[2]).insert()
self.leave_type = "_Test Leave Type Encashment"
if frappe.db.exists("Leave Type", self.leave_type):
frappe.delete_doc("Leave Type", self.leave_type, force=True)
frappe.get_doc(test_records[2]).insert()

date = getdate()
year_start = getdate(get_year_start(date))
Expand Down Expand Up @@ -148,3 +152,102 @@ def test_creation_of_leave_ledger_entry_on_submit(self):

leave_encashment.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_encashment.name}))

@set_holiday_list("_Test Leave Encashment", "_Test Company")
def test_unused_leaves_after_leave_encashment_for_carry_forwarding_leave_type(self):
employee = make_employee("[email protected]", company="_Test Company")
# allocated 10 leaves, encashable threshold is set 5 in test records, so encashed days are 5
leave_encashment = self.get_encashment_created_after_leave_period(employee, is_carry_forward=1)
# check if unused leaves are 5 before processing expired allocation runs
unused_leaves = get_unused_leaves(
employee, self.leave_type, self.leave_period.from_date, self.leave_period.to_date
)
self.assertEqual(unused_leaves, 5)

# check if a single leave ledger entry is created
self.assertEqual(frappe.get_value("Leave Type", self.leave_type, "is_carry_forward"), 1)
leave_ledger_entry = frappe.get_all(
"Leave Ledger Entry", fields=["leaves"], filters={"transaction_name": leave_encashment.name}
)
self.assertEqual(len(leave_ledger_entry), 1)
self.assertEqual(leave_ledger_entry[0].leaves, leave_encashment.encashable_days * -1)

# check if unused leaves are 5 after processing expired allocation runs
process_expired_allocation()
unused_leaves = get_unused_leaves(
employee, self.leave_type, self.leave_period.from_date, self.leave_period.to_date
)
self.assertEqual(unused_leaves, 5)

@set_holiday_list("_Test Leave Encashment", "_Test Company")
def test_leave_expiry_after_leave_encashment_for_non_carry_forwarding_leave_type(self):
employee = make_employee("[email protected]", company="_Test Company")
# allocated 10 leaves, encashable days threshold is 5, so encashed days are 5

leave_encashment = self.get_encashment_created_after_leave_period(employee, is_carry_forward=0)
# when leave encashment is created after leave allocation period is over,
# it's assumed that process expired allocation has expired the leaves,
# hence a reverse ledger entry should be created for the encashment
# check if two leave ledger entries are created
self.assertEqual(frappe.get_value("Leave Type", self.leave_type, "is_carry_forward"), 0)
leave_ledger_entry = frappe.get_all(
"Leave Ledger Entry",
fields="*",
filters={"transaction_name": leave_encashment.name},
order_by="leaves",
)
self.assertEqual(len(leave_ledger_entry), 2)
self.assertEqual(leave_ledger_entry[0].leaves, leave_encashment.encashable_days * -1)
self.assertEqual(leave_ledger_entry[1].leaves, leave_encashment.encashable_days * 1)

# check if 10 leaves are expired after processing expired allocation runs
process_expired_allocation()

expired_leaves = frappe.get_value(
"Leave Ledger Entry",
{"employee": employee, "leave_type": self.leave_type, "is_expired": 1},
"leaves",
)
self.assertEqual(expired_leaves, -10)

def get_encashment_created_after_leave_period(self, employee, is_carry_forward):
frappe.db.delete("Leave Period", {"name": self.leave_period.name})
# create new leave period that has end date of yesterday
start_date = add_days(getdate(), -30)
end_date = add_days(getdate(), -1)
self.leave_period = create_leave_period(start_date, end_date, "_Test Company")
frappe.db.set_value(
"Leave Type",
self.leave_type,
{
"is_carry_forward": is_carry_forward,
},
)

leave_policy = frappe.get_value("Leave Policy", {"title": "Test Leave Policy"}, "name")
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy,
"leave_period": self.leave_period.name,
}
create_assignment_for_multiple_employees([employee], frappe._dict(data))

make_salary_structure(
"Salary Structure for Encashment",
"Monthly",
employee,
other_details={"leave_encashment_amount_per_day": 50},
)

leave_encashment = frappe.get_doc(
{
"doctype": "Leave Encashment",
"employee": employee,
"leave_type": self.leave_type,
"leave_period": self.leave_period.name,
"encashment_date": self.leave_period.to_date,
"currency": "INR",
}
).insert()
leave_encashment.submit()
return leave_encashment
38 changes: 12 additions & 26 deletions hrms/hr/doctype/shift_type/shift_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ def process_auto_attendance(self):
frappe.db.commit() # nosemgrep

assigned_employees = self.get_assigned_employees(self.process_attendance_after, True)

# mark absent in batches & commit to avoid losing progress since this tries to process remaining attendance
# right from "Process Attendance After" to "Last Sync of Checkin"
for batch in create_batch(assigned_employees, EMPLOYEE_CHUNK_SIZE):
Expand Down Expand Up @@ -234,42 +233,29 @@ def get_marked_attendance_dates_between(self, employee: str, start_date: str, en
)
).run(pluck=True)

def get_assigned_employees(self, from_date=None, consider_default_shift=False) -> list[str]:
def get_assigned_employees(self, from_date: datetime.date, consider_default_shift=False) -> list[str]:
"""Get all such employees who either have this shift assigned that hasn't ended or have this shift as default shift.
This may fetch some redundant employees who have another shift assigned that may have started or ended before or after the
attendance processing date. But this is done to avoid missing any employee who may have this shift as active shift."""
filters = {"shift_type": self.name, "docstatus": "1", "status": "Active"}
if from_date:
filters["start_date"] = (">=", from_date)

assigned_employees = frappe.get_all("Shift Assignment", filters=filters, pluck="employee")
or_filters = [["end_date", ">=", from_date], ["end_date", "is", "not set"]]

assigned_employees = frappe.get_all(
"Shift Assignment", filters=filters, or_filters=or_filters, pluck="employee"
)

if consider_default_shift:
default_shift_employees = self.get_employees_with_default_shift(filters)
default_shift_employees = frappe.get_all(
"Employee", filters={"default_shift": self.name, "status": "Active"}, pluck="name"
)
assigned_employees = set(assigned_employees + default_shift_employees)

# exclude inactive employees
inactive_employees = frappe.db.get_all("Employee", {"status": "Inactive"}, pluck="name")

return list(set(assigned_employees) - set(inactive_employees))

def get_employees_with_default_shift(self, filters: dict) -> list:
default_shift_employees = frappe.get_all(
"Employee", filters={"default_shift": self.name, "status": "Active"}, pluck="name"
)

if not default_shift_employees:
return []

# exclude employees from default shift list if any other valid shift assignment exists
del filters["shift_type"]
filters["employee"] = ("in", default_shift_employees)

active_shift_assignments = frappe.get_all(
"Shift Assignment",
filters=filters,
pluck="employee",
)

return list(set(default_shift_employees) - set(active_shift_assignments))

def get_holiday_list(self, employee: str) -> str:
holiday_list_name = self.holiday_list or get_holiday_list_for_employee(employee, False)
return holiday_list_name
Expand Down
88 changes: 82 additions & 6 deletions hrms/hr/doctype/shift_type/test_shift_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,28 +347,83 @@ def test_mark_absent_for_dates_with_no_attendance_for_midnight_shift(self):
shift_type="Test Absent with no Attendance",
start_time="15:00:00",
end_time="23:30:00",
process_attendance_after=add_days(today, -6),
process_attendance_after=add_days(today, -8),
allow_check_out_after_shift_end_time=120,
last_sync_of_checkin=f"{today} 15:00:00",
)
# single day assignment
date1 = add_days(today, -5)
date1 = add_days(today, -7)
make_shift_assignment(shift_type.name, employee, date1, date1)

# assignment without end date
date2 = add_days(today, -4)
# assignment after a gap
date2 = add_days(today, -5)
make_shift_assignment(shift_type.name, employee, date2, date2)

# assignment without end date
date3 = add_days(today, -3)
make_shift_assignment(shift_type.name, employee, date3)

shift_type.process_auto_attendance()
absent_records = frappe.get_all(
"Attendance",
{
fields=["name", "employee", "attendance_date", "status", "shift"],
filters={
"attendance_date": ["between", [date1, today]],
"employee": employee,
"status": "Absent",
},
)
self.assertEqual(len(absent_records), 2)

self.assertEqual(len(absent_records), 5)
# absent for first assignment
self.assertEqual(
frappe.db.get_value(
"Attendance",
{"attendance_date": date1, "shift": shift_type.name, "employee": employee},
"status",
),
"Absent",
)
# no attendance for day after first assignment
self.assertIsNone(
frappe.db.get_value(
"Attendance",
{"attendance_date": add_days(date1, 1), "shift": shift_type.name, "employee": employee},
)
)
# absent for second assignment
self.assertEqual(
frappe.db.get_value(
"Attendance",
{"attendance_date": date2, "shift": shift_type.name, "employee": employee},
"status",
),
"Absent",
)
# no attendance for day after second assignment
self.assertIsNone(
frappe.db.get_value(
"Attendance",
{"attendance_date": add_days(date2, 1), "shift": shift_type.name, "employee": employee},
)
)
# absent for third assignment
self.assertEqual(
frappe.db.get_value(
"Attendance",
{"attendance_date": date3, "shift": shift_type.name, "employee": employee},
"status",
),
"Absent",
)
self.assertEqual(
frappe.db.get_value(
"Attendance",
{"attendance_date": add_days(date3, 1), "shift": shift_type.name, "employee": employee},
"status",
),
"Absent",
)

def test_do_not_mark_absent_before_shift_actual_end_time(self):
from hrms.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
Expand Down Expand Up @@ -612,6 +667,27 @@ def test_skip_auto_attendance_for_overlapping_shift(self):
self.assertEqual(log_in.skip_auto_attendance, 1)
self.assertEqual(log_out.skip_auto_attendance, 1)

def test_mark_attendance_for_default_shift_when_shift_assignment_is_not_overlapping(self):
shift_1 = setup_shift_type(shift_type="Deafult Shift", start_time="08:00:00", end_time="12:00:00")
shift_2 = setup_shift_type(shift_type="Not Default Shift", start_time="10:00:00", end_time="18:00:00")
employee = make_employee(
"[email protected]", company="_Test Company", default_shift=shift_1.name
)
shift_assigned_date = add_days(getdate(), +1)
make_shift_assignment(shift_2.name, employee, shift_assigned_date)
from hrms.hr.doctype.attendance.attendance import mark_attendance

mark_attendance(employee, add_days(getdate(), -1), "Present", shift=shift_1.name)
shift_1.process_auto_attendance()
self.assertEqual(
frappe.db.get_value(
"Attendance",
{"employee": employee, "attendance_date": getdate(), "shift": shift_1.name},
"status",
),
"Absent",
)


def setup_shift_type(**args):
args = frappe._dict(args)
Expand Down
4 changes: 2 additions & 2 deletions hrms/payroll/doctype/salary_slip/salary_slip.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ frappe.ui.form.on("Salary Slip", {
const message = `
<div class="small text-muted pb-3">
${__("Note").bold()}: ${__("Payment Days calculations are based on these Payroll Settings")}:
<br><br>${__("Payroll Based On")}: ${payroll_based_on.bold()}
<br>${__("Consider Unmarked Attendance As")}: ${consider_unmarked_attendance_as.bold()}
<br><br>${__("Payroll Based On")}: ${__(payroll_based_on).bold()}
<br>${__("Consider Unmarked Attendance As")}: ${__(consider_unmarked_attendance_as).bold()}
<br>${__("Consider Marked Attendance on Holidays")}:
${
cint(include_holidays_in_total_working_days) &&
Expand Down

0 comments on commit 4ae938f

Please sign in to comment.