Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c26a25c
feat(report): enhance Employee Advance Summary report
akhtarmohsin Jun 17, 2026
4f53f30
refactor(report): follow frappe/hrms conventions in Employee Advance …
akhtarmohsin Jun 18, 2026
f0c922d
refactor(report): rename balance to outstanding_amount
akhtarmohsin Jun 18, 2026
f6eb09e
fix(report): show Title and Outstanding Amount in flat view
akhtarmohsin Jun 18, 2026
439b9b5
fix(report): show employee name in group header when grouping by Empl…
akhtarmohsin Jun 18, 2026
36d17ff
feat(report): show employee ID and name together in Employee column a…
akhtarmohsin Jun 18, 2026
f296ae9
refactor(report): use filters.get() and scrub() inline for group_by
akhtarmohsin Jun 18, 2026
fbcddcf
fix: add permission check for insert_shift mutations
Jun 20, 2026
fd6161c
fix: add delete permission check for next_shift
Jun 20, 2026
19237ed
fix(employee_payment_entry): set source exchange rate on payment entr…
iamkhanraheel Jun 23, 2026
f5e01ca
test: verify no exchange gain/loss triggered for same currency advanc…
iamkhanraheel Jun 23, 2026
07b5065
Merge pull request #4715 from akhtarmohsin/fix/employee-advance-summa…
deepeshgarg007 Jun 24, 2026
229b6e3
Merge pull request #4755 from iamkhanraheel/fix/employee_advance_paym…
deepeshgarg007 Jun 24, 2026
6dd7a76
Merge pull request #4740 from pratheep-bit/fix/insert-shift-permission
deepeshgarg007 Jun 24, 2026
70ca7d9
fix: use leave period dates for half-yearly earned leave allocation
iamkhanraheel Jun 11, 2026
7f36a9a
test: add tests to assert earned leave schedule based on leave period…
iamkhanraheel Jun 11, 2026
d63ee95
test: remove current date flag
iamkhanraheel Jun 12, 2026
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
4 changes: 4 additions & 0 deletions hrms/api/roster.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,17 @@ def insert_shift(
)

if prev_shift:
frappe.has_permission("Shift Assignment", "write", prev_shift, throw=True)
if next_shift:
frappe.has_permission("Shift Assignment", "write", next_shift, throw=True)
end_date = frappe.db.get_value("Shift Assignment", next_shift, "end_date")
frappe.has_permission("Shift Assignment", "delete", next_shift, throw=True)
frappe.db.set_value("Shift Assignment", next_shift, "docstatus", 2)
frappe.delete_doc("Shift Assignment", next_shift)
frappe.db.set_value("Shift Assignment", prev_shift, "end_date", end_date or None)

elif next_shift:
frappe.has_permission("Shift Assignment", "write", next_shift, throw=True)
frappe.db.set_value("Shift Assignment", next_shift, "start_date", start_date)

else:
Expand Down
19 changes: 19 additions & 0 deletions hrms/hr/doctype/employee_advance/test_employee_advance.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,25 @@ def test_multicurrency_advance(self):
self.assertEqual(advance.base_paid_amount, expected_base_paid)
self.assertEqual(payment_entry.paid_amount, expected_base_paid)

def test_no_exchange_gain_loss_for_same_currency_advance_payment(self):
from hrms.overrides.employee_payment_entry import get_payment_entry_for_employee

gain_loss_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", None)

try:
employee_name = make_employee("_T@employee.advance", "_Test Company")
advance = make_employee_advance(employee_name)

# should not raise error even without exchange_gain_loss_account set at time of payment
pe = get_payment_entry_for_employee(advance.doctype, advance.name)

self.assertEqual(flt(pe.source_exchange_rate), 1.0)
self.assertEqual(flt(pe.target_exchange_rate), 1.0)
self.assertFalse(any(d.is_exchange_gain_loss for d in pe.deductions))
finally:
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", gain_loss_account)

def test_status_on_discard(self):
employee_name = make_employee("Test_status@employee.advance", "_Test Company")
advance = make_employee_advance(employee_name, do_not_submit=True)
Expand Down
52 changes: 38 additions & 14 deletions hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ def get_new_leaves(self, annual_allocation, leave_details, date_of_joining):

def get_leaves_for_passed_period(self, annual_allocation, leave_details, date_of_joining):
consider_current_period = is_earned_leave_applicable_for_current_period(
date_of_joining, leave_details.allocate_on_day, leave_details.earned_leave_frequency
date_of_joining,
leave_details.allocate_on_day,
leave_details.earned_leave_frequency,
effective_from=self.effective_from,
)
current_date, from_date = self.get_current_and_from_date(date_of_joining)
periods_passed = self.get_periods_passed(
Expand Down Expand Up @@ -276,7 +279,9 @@ def calculate_leaves_for_passed_period(
# calculate pro-rated leave for that month
# and normal monthly earned leave for remaining passed months
start_date, end_date = 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,
)
leaves = get_periodically_earned_leave(
date_of_joining,
Expand Down Expand Up @@ -319,6 +324,7 @@ def get_earned_leave_schedule(
leave_details.allocate_on_day,
from_date,
date_of_joining,
effective_from=self.effective_from,
)
schedule = []
if new_leaves_allocated:
Expand All @@ -331,7 +337,11 @@ def get_earned_leave_schedule(
"attempted": 1,
}
)
last_allocated_date = get_sub_period_start_and_end(today, leave_details.earned_leave_frequency)[1]
last_allocated_date = get_sub_period_start_and_end(
today,
leave_details.earned_leave_frequency,
effective_from=self.effective_from,
)[1]

while date <= to_date:
date_already_passed = today > date
Expand All @@ -349,6 +359,7 @@ def get_earned_leave_schedule(
leave_details.allocate_on_day,
add_to_date(date, months=months_to_add),
date_of_joining,
effective_from=self.effective_from,
)
if from_date < getdate(date_of_joining):
pro_rated_period_start, pro_rated_period_end = get_sub_period_start_and_end(
Expand Down Expand Up @@ -394,30 +405,43 @@ def calculate_periods_passed(
return periods_passed


def is_earned_leave_applicable_for_current_period(date_of_joining, allocate_on_day, earned_leave_frequency):
from hrms.hr.utils import get_semester_end, get_semester_start

def is_earned_leave_applicable_for_current_period(
date_of_joining, allocate_on_day, earned_leave_frequency, effective_from=None
):
date = getdate(frappe.flags.current_date) or getdate()
# If the date of assignment creation is >= the leave type's "Allocate On" date,
# then the current month should be considered
# because the employee is already entitled for the leave of that month

# For Half-Yearly, compute preiod 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

period_start, period_end = get_half_year_periods(date, effective_from)
half_yearly_condition = (allocate_on_day == "First Day" and date >= period_start) or (
allocate_on_day == "Last Day" and date == period_end
)
else:
from hrms.hr.utils import get_semester_end, get_semester_start

half_yearly_condition = (allocate_on_day == "First Day" and date >= get_semester_start(date)) or (
allocate_on_day == "Last Day" and date == get_semester_end(date)
)

condition_map = {
"Monthly": (
(allocate_on_day == "Date of Joining" and date.day >= date_of_joining.day)
or (allocate_on_day == "First Day" and date >= get_first_day(date))
or (allocate_on_day == "Last Day" and date == get_last_day(date))
),
"Quarterly": (allocate_on_day == "First Day" and date >= get_quarter_start(date))
or (allocate_on_day == "Last Day" and date == get_quarter_ending(date)),
"Half-Yearly": (allocate_on_day == "First Day" and date >= get_semester_start(date))
or (allocate_on_day == "Last Day" and date == get_semester_end(date)),
"Quarterly": (
(allocate_on_day == "First Day" and date >= get_quarter_start(date))
or (allocate_on_day == "Last Day" and date == get_quarter_ending(date))
),
"Half-Yearly": half_yearly_condition,
"Yearly": (
(allocate_on_day == "First Day" and date >= get_year_start(date))
or (allocate_on_day == "Last Day" and date == get_year_ending(date))
),
}

return condition_map.get(earned_leave_frequency)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def test_pro_rated_leave_allocation_for_custom_date_range(self):
).submit()

today_date = getdate()
frappe.flags.pop("current_date", None)

leave_policy_assignment = frappe.new_doc("Leave Policy Assignment")
leave_policy_assignment.employee = self.employee.name
Expand Down Expand Up @@ -321,3 +322,101 @@ def test_skip_zero_allocation_leaves(self):
self.assertEqual(allocations[compoff.name]["new_leaves_allocated"], 3)
self.assertEqual(allocations[annual.name]["new_leaves_allocated"], 3)
self.assertNotIn(casual.name, allocations)

def test_half_yearly_earned_leave_schedule_based_on_leave_period(self):
"""
Leave Period: 01-Apr-2026 to 31-Mar-2027
Expected allocation dates: 30-Sep-2026, 31-Mar-2027
NOT: 30-Jun-2026, 31-Dec-2026
"""
leave_period = create_leave_period(getdate("2026-04-01"), getdate("2027-03-31"), "_Test Company")
leave_type = create_leave_type(
leave_type_name="_Test Half Yearly Earned Leave",
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 at the start of the leave period
frappe.flags.current_date = getdate("2026-04-01")

data = frappe._dict(
{
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name,
}
)
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-09-30"), allocation_dates)
self.assertIn(getdate("2027-03-31"), allocation_dates)
self.assertNotIn(getdate("2026-06-30"), allocation_dates)
self.assertNotIn(getdate("2026-12-31"), 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(self):
"""
Employee joins: 01-Apr-2026
Expected allocation dates: 30-Sep-2026, 31-Mar-2027
"""
self.employee.date_of_joining = getdate("2026-04-01")
self.employee.save()

leave_type = create_leave_type(
leave_type_name="_Test Half Yearly Earned Leave Joining Date",
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()

frappe.flags.current_date = getdate("2026-04-01")

data = frappe._dict(
{
"assignment_based_on": "Joining Date",
"leave_policy": leave_policy.name,
"effective_from": self.employee.date_of_joining,
"effective_to": getdate("2027-03-31"),
}
)
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-09-30"), allocation_dates)
self.assertIn(getdate("2027-03-31"), allocation_dates)
self.assertNotIn(getdate("2026-06-30"), allocation_dates)
self.assertEqual(len(allocation_dates), 2)
3 changes: 3 additions & 0 deletions hrms/hr/doctype/leave_type/test_leave_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def create_leave_type(**args):
if leave_type.is_ppl:
leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5

if leave_type.is_earned_leave and args.earned_leave_frequency:
leave_type.earned_leave_frequency = args.earned_leave_frequency

leave_type.insert()

return leave_type
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,50 @@ frappe.query_reports["Employee Advance Summary"] = {
fieldtype: "Select",
options: "\nDraft\nPaid\nPartially Paid\nUnpaid\nClaimed\nCancelled",
},
{
fieldname: "department",
label: __("Department"),
fieldtype: "MultiSelectList",
options: "Department",
get_data: function (txt) {
return frappe.db.get_link_options("Department", txt);
},
},
{
fieldname: "branch",
label: __("Branch"),
fieldtype: "MultiSelectList",
options: "Branch",
get_data: function (txt) {
return frappe.db.get_link_options("Branch", txt);
},
},
{
fieldname: "advance_account",
label: __("Advance Account"),
fieldtype: "MultiSelectList",
options: "Account",
get_data: function (txt) {
var company = frappe.query_report.get_filter_value("company");
return frappe.db.get_link_options("Account", txt, {
company: company,
account_type: "Receivable",
});
},
},
{
fieldname: "group_by",
label: __("Group By"),
fieldtype: "Select",
options: "\nEmployee\nDepartment\nBranch",
},
],

formatter: function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (data && data.bold) {
value = `<strong>${value}</strong>`;
}
return value;
},
};
Loading
Loading