Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ const registerServiceWorker = async () => {
let serviceWorkerURL = "/assets/hrms/frontend/sw.js"
let config = ""

try {
config = await window.frappePushNotification.fetchWebConfig()
serviceWorkerURL = `${serviceWorkerURL}?config=${encodeURIComponent(
JSON.stringify(config)
)}`
} catch (err) {
console.error("Failed to fetch FCM config", err)
if (window.frappe?.boot?.push_relay_server_url) {
try {
config = await window.frappePushNotification.fetchWebConfig()
serviceWorkerURL = `${serviceWorkerURL}?config=${encodeURIComponent(
JSON.stringify(config)
)}`
} catch (err) {
console.error("Failed to fetch FCM config", err)
}
}

navigator.serviceWorker
Expand Down
1 change: 1 addition & 0 deletions hrms/api/roster.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ def insert_shift(
"shift_type": shift_type,
"status": status,
"shift_location": shift_location,
"docstatus": ["!=", 2],
}
prev_shift = frappe.db.exists(dict({"end_date": add_days(start_date, -1)}, **filters))
next_shift = (
Expand Down
24 changes: 13 additions & 11 deletions hrms/hr/doctype/leave_allocation/leave_allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,19 @@ def validate_lwp(self):
)

def validate_allocation_overlap(self):
leave_allocation = frappe.db.sql(
"""
SELECT
name
FROM `tabLeave Allocation`
WHERE
employee=%s AND leave_type=%s
AND name <> %s AND docstatus=1
AND to_date >= %s AND from_date <= %s""",
(self.employee, self.leave_type, self.name, self.from_date, self.to_date),
)
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
leave_allocation = (
frappe.qb.from_(LeaveAllocation)
.select(LeaveAllocation.name)
.where(
(LeaveAllocation.employee == self.employee)
& (LeaveAllocation.leave_type == self.leave_type)
& (LeaveAllocation.name != self.name)
& (LeaveAllocation.docstatus == 1)
& (LeaveAllocation.to_date >= self.from_date)
& (LeaveAllocation.from_date <= self.to_date)
)
).run()

if leave_allocation:
frappe.msgprint(
Expand Down
44 changes: 27 additions & 17 deletions hrms/hr/doctype/leave_application/leave_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -1318,23 +1318,33 @@ def get_leaves_for_period(

def get_leave_entries(employee, leave_type, from_date, to_date):
"""Returns leave entries between from_date and to_date."""
return frappe.db.sql(
"""
SELECT
employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, holiday_list,
is_carry_forward, is_expired
FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1
AND (leaves<0
OR is_expired=1)
AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
""",
{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
as_dict=1,
)
Ledger = frappe.qb.DocType("Leave Ledger Entry")
return (
frappe.qb.from_(Ledger)
.select(
Ledger.employee,
Ledger.leave_type,
Ledger.from_date,
Ledger.to_date,
Ledger.leaves,
Ledger.transaction_name,
Ledger.transaction_type,
Ledger.holiday_list,
Ledger.is_carry_forward,
Ledger.is_expired,
)
.where(
(Ledger.employee == employee)
& (Ledger.leave_type == leave_type)
& (Ledger.docstatus == 1)
& ((Ledger.leaves < 0) | (Ledger.is_expired == 1))
& (
Ledger.from_date[from_date:to_date]
| Ledger.to_date[from_date:to_date]
| ((Ledger.from_date < from_date) & (Ledger.to_date > to_date))
)
)
).run(as_dict=1)


@frappe.whitelist()
Expand Down
29 changes: 14 additions & 15 deletions hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,18 @@ def on_cancel(self):

def validate_leave_allocation_against_leave_application(ledger):
"""Checks that leave allocation has no leave application against it"""
leave_application_records = frappe.db.sql_list(
"""
SELECT transaction_name
FROM `tabLeave Ledger Entry`
WHERE
employee=%s
AND leave_type=%s
AND transaction_type='Leave Application'
AND from_date>=%s
AND to_date<=%s
""",
(ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date),
)
Ledger = frappe.qb.DocType("Leave Ledger Entry")
leave_application_records = (
frappe.qb.from_(Ledger)
.select(Ledger.transaction_name)
.where(
(Ledger.employee == ledger.employee)
& (Ledger.leave_type == ledger.leave_type)
& (Ledger.transaction_type == "Leave Application")
& (Ledger.from_date >= ledger.from_date)
& (Ledger.to_date <= ledger.to_date)
)
).run(pluck=True)

if leave_application_records:
frappe.throw(
Expand Down Expand Up @@ -171,7 +170,7 @@ def process_expired_allocation():
expire_allocation = frappe.db.sql(
"""
SELECT
leaves, to_date, from_date, employee, leave_type,
leaves, `to_date`, `from_date`, employee, leave_type,
is_carry_forward, transaction_name as name, transaction_type
FROM `tabLeave Ledger Entry` l
WHERE (NOT EXISTS
Expand All @@ -187,7 +186,7 @@ def process_expired_allocation():
OR (is_carry_forward = 0 AND leave_type not in %s)
)))
AND transaction_type = 'Leave Allocation'
AND to_date < %s""",
AND `to_date` < %s""",
(leave_type, today()),
as_dict=1,
)
Expand Down
49 changes: 38 additions & 11 deletions hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,19 @@ def get_periods_passed(self, earned_leave_frequency, current_date, from_date, co
"Yearly": (1, 12),
}.get(earned_leave_frequency)

periods_passed = calculate_periods_passed(
current_date, from_date, periods_per_year, months_per_period, consider_current_period
effective_from = self.effective_from if earned_leave_frequency == "Half-Yearly" else None
return max(
calculate_periods_passed(
current_date,
from_date,
periods_per_year,
months_per_period,
consider_current_period,
effective_from=effective_from,
),
0,
)

return periods_passed

def calculate_leaves_for_passed_period(
self, annual_allocation, leave_details, date_of_joining, periods_passed, consider_current_period
):
Expand Down Expand Up @@ -301,6 +308,7 @@ def get_earned_leave_schedule(
self, annual_allocation, leave_details, date_of_joining, new_leaves_allocated
):
from hrms.hr.utils import (
get_complete_month_count,
get_expected_allocation_date_for_period,
get_monthly_earned_leave,
get_sub_period_start_and_end,
Expand All @@ -326,7 +334,16 @@ def get_earned_leave_schedule(
date_of_joining,
effective_from=self.effective_from,
)
if (
leave_details.earned_leave_frequency == "Half-Yearly"
and self.assignment_based_on == "Joining Date"
and to_date >= add_months(from_date, 12)
):
max_allocations = get_complete_month_count(to_date, from_date) // months_to_add + 1
else:
max_allocations = 0
schedule = []
allocations_added = 0
if new_leaves_allocated:
schedule.append(
{
Expand All @@ -337,6 +354,7 @@ def get_earned_leave_schedule(
"attempted": 1,
}
)
allocations_added += 1
last_allocated_date = get_sub_period_start_and_end(
today,
leave_details.earned_leave_frequency,
Expand All @@ -354,6 +372,9 @@ def get_earned_leave_schedule(
"attempted": 1 if date_already_passed else 0,
}
schedule.append(row)
allocations_added += 1
if max_allocations and allocations_added >= max_allocations:
break
date = get_expected_allocation_date_for_period(
leave_details.earned_leave_frequency,
leave_details.allocate_on_day,
Expand All @@ -363,7 +384,7 @@ def get_earned_leave_schedule(
)
if from_date < getdate(date_of_joining):
pro_rated_period_start, pro_rated_period_end = get_sub_period_start_and_end(
date_of_joining, leave_details.earned_leave_frequency
date_of_joining, leave_details.earned_leave_frequency, effective_from=self.effective_from
)
pro_rated_earned_leave = get_monthly_earned_leave(
date_of_joining,
Expand Down Expand Up @@ -391,12 +412,19 @@ def get_pro_rata_period_end_date(consider_current_month):


def calculate_periods_passed(
current_date, from_date, periods_per_year, months_per_period, consider_current_period
current_date, from_date, periods_per_year, months_per_period, consider_current_period, effective_from=None
):
periods_passed = 0
if effective_from:
from hrms.hr.utils import get_complete_month_count

from_period = (from_date.year * periods_per_year) + ((from_date.month - 1) // months_per_period)
current_period = (current_date.year * periods_per_year) + ((current_date.month - 1) // months_per_period)
effective_from = getdate(effective_from)
from_period = get_complete_month_count(from_date, effective_from) // months_per_period
current_period = get_complete_month_count(current_date, effective_from) // months_per_period
else:
from_period = (from_date.year * periods_per_year) + ((from_date.month - 1) // months_per_period)
current_period = (current_date.year * periods_per_year) + (
(current_date.month - 1) // months_per_period
)

periods_passed = current_period - from_period
if consider_current_period:
Expand All @@ -410,8 +438,7 @@ def is_earned_leave_applicable_for_current_period(
):
date = getdate(frappe.flags.current_date) or getdate()

# For Half-Yearly, compute preiod relative to effective_from
# instead of calendar year
# For Half-Yearly, compute period relative to effective_from instead of calendar year
if earned_leave_frequency == "Half-Yearly" and effective_from:
from hrms.hr.utils import get_half_year_periods

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,108 @@ def test_half_yearly_earned_leave_schedule_based_on_joining_date(self):
self.assertIn(getdate("2027-03-31"), allocation_dates)
self.assertNotIn(getdate("2026-06-30"), allocation_dates)
self.assertEqual(len(allocation_dates), 2)

def test_half_yearly_earned_leave_schedule_based_on_joining_date_mid_month_first_day(self):
"""
Employee joins: 27-May-2026 (mid-month), assignment created: 03-Jul-2026
Period 1: 27-May-2026 → 26-Nov-2026, Period 2: 27-Nov-2026 → 26-May-2027
Expected allocation dates: 03-Jul-2026 (period 1 credited on assignment date), 27-Nov-2026
"""
self.employee.date_of_joining = getdate("2026-05-27")
self.employee.save()

leave_type = create_leave_type(
leave_type_name="_Test Half Yearly Earned Leave Joining Date First Day",
is_earned_leave=True,
earned_leave_frequency="Half-Yearly",
allocate_on_day="First Day",
)
annual_allocation = 18
leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=annual_allocation)
leave_policy.submit()

# assignment created mid-period
frappe.flags.current_date = getdate("2026-07-03")

data = frappe._dict(
{
"assignment_based_on": "Joining Date",
"leave_policy": leave_policy.name,
"effective_from": self.employee.date_of_joining,
"effective_to": getdate("2027-05-26"),
}
)
assignment = create_assignment(self.employee.name, data)
assignment.submit()

allocation_name = frappe.db.get_value(
"Leave Allocation", {"leave_policy_assignment": assignment.name}, "name"
)
schedule = frappe.get_all(
"Earned Leave Schedule",
filters={"parent": allocation_name},
fields=["allocation_date"],
order_by="allocation_date asc",
)

allocation_dates = [getdate(row.allocation_date) for row in schedule]

self.assertIn(getdate("2026-07-03"), allocation_dates)
self.assertIn(getdate("2026-11-27"), allocation_dates)
self.assertNotIn(getdate("2026-06-30"), allocation_dates)
self.assertNotIn(getdate("2027-05-27"), allocation_dates)

# should be exactly 2 allocations, not 3
self.assertEqual(len(allocation_dates), 2)

def test_half_yearly_earned_leave_schedule_based_on_joining_date_mid_month_last_day(self):
"""
Employee joins: 27-May-2026 (mid-month), assignment created: 03-Jul-2026
Period 1: 27-May-2026 → 26-Nov-2026, Period 2: 27-Nov-2026 → 26-May-2027
Expected allocation dates: 26-Nov-2026, 26-May-2027
"""
self.employee.date_of_joining = getdate("2026-05-27")
self.employee.save()

leave_type = create_leave_type(
leave_type_name="_Test Half Yearly Earned Leave Joining Date Last Day",
is_earned_leave=True,
earned_leave_frequency="Half-Yearly",
allocate_on_day="Last Day",
)
annual_allocation = 18
leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=annual_allocation)
leave_policy.submit()

# assignment created mid-period
frappe.flags.current_date = getdate("2026-07-03")

data = frappe._dict(
{
"assignment_based_on": "Joining Date",
"leave_policy": leave_policy.name,
"effective_from": self.employee.date_of_joining,
"effective_to": getdate("2027-05-26"),
}
)
assignment = create_assignment(self.employee.name, data)
assignment.submit()

allocation_name = frappe.db.get_value(
"Leave Allocation", {"leave_policy_assignment": assignment.name}, "name"
)
schedule = frappe.get_all(
"Earned Leave Schedule",
filters={"parent": allocation_name},
fields=["allocation_date"],
order_by="allocation_date asc",
)

allocation_dates = [getdate(row.allocation_date) for row in schedule]

self.assertIn(getdate("2026-11-26"), allocation_dates)
self.assertIn(getdate("2027-05-26"), allocation_dates)
self.assertNotIn(getdate("2026-06-30"), allocation_dates)

# should be exactly 2 allocations, not 3
self.assertEqual(len(allocation_dates), 2)
1 change: 1 addition & 0 deletions hrms/hr/doctype/shift_assignment/shift_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def on_update_after_submit(self):
def on_cancel(self):
self.validate_employee_checkin()
self.validate_attendance()
self.db_set("status", "Inactive", update_modified=False)

def validate_employee_checkin(self):
checkins = frappe.get_all(
Expand Down
Loading
Loading