Skip to content

Commit

Permalink
Merge pull request #2668 from frappe/version-15-hotfix
Browse files Browse the repository at this point in the history
chore: release v15
  • Loading branch information
asmitahase authored Jan 23, 2025
2 parents dd8541e + 5d50429 commit 3bdfb27
Show file tree
Hide file tree
Showing 17 changed files with 938 additions and 252 deletions.
2 changes: 1 addition & 1 deletion frappe-ui
Submodule frappe-ui updated 130 files
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"dayjs": "^1.11.11",
"feather-icons": "^4.29.1",
"firebase": "^10.8.0",
"frappe-ui": "^0.1.72",
"frappe-ui": "0.1.105",
"postcss": "^8.4.5",
"tailwindcss": "^3.4.3",
"vite": "^5.4.10",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/utils/dayjs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dayjs from "dayjs"
import updateLocale from "dayjs/plugin/updateLocale"
import localizedFormat from "dayjs/plugin/localizedFormat"
import relativeTime from "dayjs/esm/plugin/relativeTime"
import relativeTime from "dayjs/plugin/relativeTime"
import isToday from "dayjs/plugin/isToday"
import isYesterday from "dayjs/plugin/isYesterday"
import isBetween from "dayjs/plugin/isBetween"
Expand Down
1 change: 0 additions & 1 deletion frontend/src/views/Notifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ import {
arePushNotificationsEnabled,
} from "@/data/notifications"
const user = inject("$user")
const dayjs = inject("$dayjs")
const router = useRouter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,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 @@ -259,7 +259,7 @@ def set_gratuity_status(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 @@ -309,6 +309,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 @@ -183,6 +183,7 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "job_title.designation",
"fetch_if_empty": 1,
"fieldname": "designation",
"fieldtype": "Link",
Expand All @@ -194,7 +195,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-09-14 17:03:48.838409",
"modified": "2025-01-16 13:06:05.312255",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
Expand All @@ -220,4 +221,4 @@
"states": [],
"subject_field": "notes",
"title_field": "applicant_name"
}
}
106 changes: 106 additions & 0 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 Down Expand Up @@ -259,3 +261,107 @@ def test_creation_of_leave_ledger_entry_on_submit(self):
frappe.db.delete("Additional Salary", {"ref_docname": leave_encashment.name})
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, encashed 5
leave_encashment = self.get_encashment_created_after_leave_period(
employee, is_carry_forward=1, encashment_days=5
)
# 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.encashment_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, encashed 3

leave_encashment = self.get_encashment_created_after_leave_period(
employee, is_carry_forward=0, encashment_days=3
)
# 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.encashment_days * -1)
self.assertEqual(leave_ledger_entry[1].leaves, leave_encashment.encashment_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, encashment_days):
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,
"encashment_days": encashment_days,
"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
5 changes: 3 additions & 2 deletions hrms/hr/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,9 @@ def get_ec_matching_query(
ref_rank = frappe.qb.terms.Case().when(ec.employee == common_filters.party, 1).else_(0) + 1

if exact_match:
filters.append(ec.total_sanctioned_amount == common_filters.amount)
filters.append(ec.total_amount_reimbursed == common_filters.amount)
else:
filters.append(ec.total_sanctioned_amount.gt(common_filters.amount))
filters.append(ec.total_amount_reimbursed.gt(common_filters.amount))
else:
ref_rank = ConstantColumn(1)

Expand All @@ -796,6 +796,7 @@ def get_ec_matching_query(
qb.from_(ec)
.select(
ref_rank.as_("rank"),
ConstantColumn("Expense Claim").as_("doctype"),
ec.name,
ec.total_sanctioned_amount.as_("paid_amount"),
ConstantColumn("").as_("reference_no"),
Expand Down
Loading

0 comments on commit 3bdfb27

Please sign in to comment.