diff --git a/hrms/api/roster.py b/hrms/api/roster.py index effb62a7a6..6a2c249d03 100644 --- a/hrms/api/roster.py +++ b/hrms/api/roster.py @@ -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: diff --git a/hrms/hr/doctype/employee_advance/test_employee_advance.py b/hrms/hr/doctype/employee_advance/test_employee_advance.py index 02d8f5ba5d..a37ab6bbf5 100644 --- a/hrms/hr/doctype/employee_advance/test_employee_advance.py +++ b/hrms/hr/doctype/employee_advance/test_employee_advance.py @@ -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) diff --git a/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 2b58c4c388..acdc0cf670 100644 --- a/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -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( @@ -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, @@ -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: @@ -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 @@ -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( @@ -394,13 +405,26 @@ 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": ( @@ -408,16 +432,16 @@ def is_earned_leave_applicable_for_current_period(date_of_joining, allocate_on_d 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) diff --git a/hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 9cf6bc17dd..101e326246 100644 --- a/hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -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 @@ -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) diff --git a/hrms/hr/doctype/leave_type/test_leave_type.py b/hrms/hr/doctype/leave_type/test_leave_type.py index 658a354c75..d4d8af5e42 100644 --- a/hrms/hr/doctype/leave_type/test_leave_type.py +++ b/hrms/hr/doctype/leave_type/test_leave_type.py @@ -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 diff --git a/hrms/hr/report/employee_advance_summary/employee_advance_summary.js b/hrms/hr/report/employee_advance_summary/employee_advance_summary.js index a55a1dd1af..739e94d603 100644 --- a/hrms/hr/report/employee_advance_summary/employee_advance_summary.js +++ b/hrms/hr/report/employee_advance_summary/employee_advance_summary.js @@ -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 = `${value}`; + } + return value; + }, }; diff --git a/hrms/hr/report/employee_advance_summary/employee_advance_summary.py b/hrms/hr/report/employee_advance_summary/employee_advance_summary.py index 1a0997ea14..d602823de2 100644 --- a/hrms/hr/report/employee_advance_summary/employee_advance_summary.py +++ b/hrms/hr/report/employee_advance_summary/employee_advance_summary.py @@ -1,16 +1,18 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - -from pypika import Order +from collections import OrderedDict import frappe from frappe import _, msgprint +from frappe.query_builder import Order +from frappe.utils import scrub def execute(filters=None): if not filters: - filters = {} + filters = frappe._dict() + filters = frappe._dict(filters) advances_list = get_advances(filters) columns = get_columns() @@ -19,23 +21,118 @@ def execute(filters=None): msgprint(_("No record found")) return columns, advances_list - data = [] + grouped = OrderedDict() + group_totals = OrderedDict() + group_labels = OrderedDict() + for advance in advances_list: - row = [ - advance.name, - advance.employee, - advance.company, - advance.posting_date, - advance.advance_amount, - advance.paid_amount, - advance.claimed_amount, - advance.return_amount, - advance.status, - advance.currency, - ] - data.append(row) - - return columns, data + advance.outstanding_amount = advance.paid_amount - (advance.claimed_amount + advance.return_amount) + advance.department = advance.department or advance.employee_department + + if filters.get("group_by"): + group_key = advance.get(scrub(filters.get("group_by"))) + + grouped.setdefault(group_key, []).append(advance) + + if group_key not in group_totals: + group_labels[group_key] = ( + f"{advance.employee}: {advance.employee_name}" + if filters.get("group_by") == "Employee" and advance.employee_name + else group_key + ) or group_key + + group_totals[group_key] = frappe._dict( + advance_amount=0, + paid_amount=0, + claimed_amount=0, + return_amount=0, + outstanding_amount=0, + currency=advance.currency, + ) + + group_totals[group_key].advance_amount += advance.advance_amount or 0 + group_totals[group_key].paid_amount += advance.paid_amount or 0 + group_totals[group_key].claimed_amount += advance.claimed_amount or 0 + group_totals[group_key].return_amount += advance.return_amount or 0 + group_totals[group_key].outstanding_amount += advance.outstanding_amount + + if not filters.get("group_by"): + for row in advances_list: + row.title = row.name + row.outstanding_amount = row.paid_amount - (row.claimed_amount + row.return_amount) + row.department = row.department or row.employee_department + if row.employee_name: + row.employee = f"{row.employee}: {row.employee_name}" + return columns, advances_list + + result = [] + grand_total = frappe._dict( + advance_amount=0, paid_amount=0, claimed_amount=0, return_amount=0, outstanding_amount=0 + ) + first_currency = None + + for key, rows in grouped.items(): + totals = group_totals[key] + + if not first_currency: + first_currency = totals.currency + + grand_total.advance_amount += totals.advance_amount + grand_total.paid_amount += totals.paid_amount + grand_total.claimed_amount += totals.claimed_amount + grand_total.return_amount += totals.return_amount + grand_total.outstanding_amount += totals.outstanding_amount + + result.append( + frappe._dict( + title=group_labels.get(key) or _("Not Set"), + advance_amount=totals.advance_amount, + paid_amount=totals.paid_amount, + claimed_amount=totals.claimed_amount, + return_amount=totals.return_amount, + outstanding_amount=totals.outstanding_amount, + currency=totals.currency, + bold=1, + indent=0, + ) + ) + + for row in rows: + result.append( + frappe._dict( + title=row.name, + employee=f"{row.employee}: {row.employee_name}" if row.employee_name else row.employee, + advance_account=row.advance_account, + department=row.department, + branch=row.branch, + company=row.company, + posting_date=row.posting_date, + advance_amount=row.advance_amount, + paid_amount=row.paid_amount, + claimed_amount=row.claimed_amount, + return_amount=row.return_amount, + outstanding_amount=row.outstanding_amount, + status=row.status, + currency=row.currency, + indent=1, + ) + ) + + result.append( + frappe._dict( + title=_("Total"), + advance_amount=grand_total.advance_amount, + paid_amount=grand_total.paid_amount, + claimed_amount=grand_total.claimed_amount, + return_amount=grand_total.return_amount, + outstanding_amount=grand_total.outstanding_amount, + currency=first_currency, + bold=1, + indent=0, + ) + ) + + return columns, result, None, None, None, 1 def get_columns(): @@ -45,13 +142,33 @@ def get_columns(): "fieldname": "title", "fieldtype": "Link", "options": "Employee Advance", - "width": 120, + "width": 160, }, { "label": _("Employee"), "fieldname": "employee", + "fieldtype": "Data", + "width": 180, + }, + { + "label": _("Advance Account"), + "fieldname": "advance_account", + "fieldtype": "Link", + "options": "Account", + "width": 160, + }, + { + "label": _("Department"), + "fieldname": "department", "fieldtype": "Link", - "options": "Employee", + "options": "Department", + "width": 120, + }, + { + "label": _("Branch"), + "fieldname": "branch", + "fieldtype": "Link", + "options": "Branch", "width": 120, }, { @@ -67,7 +184,7 @@ def get_columns(): "fieldname": "advance_amount", "fieldtype": "Currency", "options": "currency", - "width": 120, + "width": 130, }, { "label": _("Paid Amount"), @@ -81,14 +198,21 @@ def get_columns(): "fieldname": "claimed_amount", "fieldtype": "Currency", "options": "currency", - "width": 120, + "width": 130, }, { "label": _("Returned Amount"), "fieldname": "return_amount", "fieldtype": "Currency", "options": "currency", - "width": 120, + "width": 130, + }, + { + "label": _("Outstanding Amount"), + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 140, }, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 120}, { @@ -104,12 +228,20 @@ def get_columns(): def get_advances(filters): EmployeeAdvance = frappe.qb.DocType("Employee Advance") + Employee = frappe.qb.DocType("Employee") query = ( frappe.qb.from_(EmployeeAdvance) + .left_join(Employee) + .on(EmployeeAdvance.employee == Employee.name) .select( EmployeeAdvance.name, EmployeeAdvance.employee, + Employee.employee_name, + Employee.branch, + Employee.department.as_("employee_department"), + EmployeeAdvance.advance_account, + EmployeeAdvance.department, EmployeeAdvance.paid_amount, EmployeeAdvance.status, EmployeeAdvance.advance_amount, @@ -117,27 +249,45 @@ def get_advances(filters): EmployeeAdvance.return_amount, EmployeeAdvance.company, EmployeeAdvance.posting_date, - EmployeeAdvance.purpose, EmployeeAdvance.currency, ) .where(EmployeeAdvance.docstatus < 2) ) if filters.get("employee"): - query = query.where(EmployeeAdvance.employee == filters.employee) + query = query.where(EmployeeAdvance.employee == filters.get("employee")) if filters.get("company"): - query = query.where(EmployeeAdvance.company == filters.company) + query = query.where(EmployeeAdvance.company == filters.get("company")) if filters.get("status"): - query = query.where(EmployeeAdvance.status == filters.status) + query = query.where(EmployeeAdvance.status == filters.get("status")) if filters.get("from_date"): - query = query.where(EmployeeAdvance.posting_date >= filters.from_date) + query = query.where(EmployeeAdvance.posting_date >= filters.get("from_date")) if filters.get("to_date"): - query = query.where(EmployeeAdvance.posting_date <= filters.to_date) + query = query.where(EmployeeAdvance.posting_date <= filters.get("to_date")) - return query.orderby(EmployeeAdvance.posting_date, EmployeeAdvance.name, order=Order.desc).run( - as_dict=True - ) + if filters.get("department"): + query = query.where( + (EmployeeAdvance.department.isin(filters.get("department"))) + | (EmployeeAdvance.department.isnull() & (Employee.department.isin(filters.get("department")))) + ) + + if filters.get("branch"): + query = query.where(Employee.branch.isin(filters.get("branch"))) + + if filters.get("advance_account"): + query = query.where(EmployeeAdvance.advance_account.isin(filters.get("advance_account"))) + + if filters.get("group_by") == "Department": + query = query.orderby(EmployeeAdvance.department) + elif filters.get("group_by") == "Branch": + query = query.orderby(Employee.branch) + else: + query = query.orderby(EmployeeAdvance.employee) + + query = query.orderby(EmployeeAdvance.posting_date, order=Order.desc) + + return query.run(as_dict=True) diff --git a/hrms/hr/utils.py b/hrms/hr/utils.py index f6e667ca8f..cdd6f68df4 100644 --- a/hrms/hr/utils.py +++ b/hrms/hr/utils.py @@ -370,7 +370,11 @@ def allocate_earned_leaves(): else: date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") allocation_date = get_expected_allocation_date_for_period( - e_leave_type.earned_leave_frequency, e_leave_type.allocate_on_day, today, date_of_joining + e_leave_type.earned_leave_frequency, + e_leave_type.allocate_on_day, + today, + date_of_joining, + effective_from=None, ) annual_allocation = get_annual_allocation_from_policy(allocation, e_leave_type) earned_leaves = calculate_upcoming_earned_leave(allocation, e_leave_type, date_of_joining) @@ -526,11 +530,14 @@ def get_monthly_earned_leave( return earned_leaves -def get_sub_period_start_and_end(date, frequency): +def get_sub_period_start_and_end(date, frequency, effective_from=None): + if frequency == "Half-Yearly" and effective_from: + return get_half_year_periods(date, effective_from) + return { "Monthly": (get_first_day(date), get_last_day(date)), "Quarterly": (get_quarter_start(date), get_quarter_ending(date)), - "Half-Yearly": (get_semester_start(date), get_semester_end(date)), + "Half-Yearly": (get_semester_start(date), get_semester_end(date)), # fallback only "Yearly": (get_year_start(date), get_year_ending(date)), }.get(frequency) @@ -605,11 +612,26 @@ def create_additional_leave_ledger_entry(allocation, leaves, date): allocation.create_leave_ledger_entry() -def get_expected_allocation_date_for_period(frequency, allocate_on_day, date, date_of_joining=None): +def get_expected_allocation_date_for_period( + frequency, allocate_on_day, date, date_of_joining=None, effective_from=None +): try: doj = date_of_joining.replace(month=date.month, year=date.year) - except ValueError: + except (ValueError, AttributeError): doj = datetime.date(date.year, date.month, calendar.monthrange(date.year, date.month)[1]) + + if frequency == "Half-Yearly" and effective_from: + period_start, period_end = get_half_year_periods(date, effective_from) + half_yearly_dates = { + "First Day": period_start, + "Last Day": period_end, + } + else: + half_yearly_dates = { + "First Day": get_semester_start(date), + "Last Day": get_semester_end(date), + } + return { "Monthly": { "First Day": get_first_day(date), @@ -620,7 +642,7 @@ def get_expected_allocation_date_for_period(frequency, allocate_on_day, date, da "First Day": get_quarter_start(date), "Last Day": get_quarter_ending(date), }, - "Half-Yearly": {"First Day": get_semester_start(date), "Last Day": get_semester_end(date)}, + "Half-Yearly": half_yearly_dates, "Yearly": {"First Day": get_year_start(date), "Last Day": get_year_ending(date)}, }[frequency][allocate_on_day] @@ -1050,3 +1072,29 @@ def get_semester_end(date): return get_year_ending(date) else: return add_months(get_year_ending(date), -6) + + +def get_half_year_periods(date, effective_from): + """ + Compute the half-year period relative to effective_from, + NOT relative to the calendar year. + + Example: + effective_from = 01-Apr-2026 + date = 01-Apr-2026 + → period_start = 01-Apr-2026, period_end = 30-Sep-2026 + + date = 01-Oct-2026 + → period_start = 01-Oct-2026, period_end = 31-Mar-2027 + """ + effective_from = getdate(effective_from) + date = getdate(date) + + # Find how many full 6-month periods have elapsed since effective_from + months_elapsed = (date.year - effective_from.year) * 12 + (date.month - effective_from.month) + periods_elapsed = months_elapsed // 6 + + half_year_start = add_months(effective_from, periods_elapsed * 6) + half_year_end = add_days(add_months(half_year_start, 6), -1) + + return half_year_start, half_year_end diff --git a/hrms/overrides/employee_payment_entry.py b/hrms/overrides/employee_payment_entry.py index 2c61ae2ea9..87d21d3ba0 100644 --- a/hrms/overrides/employee_payment_entry.py +++ b/hrms/overrides/employee_payment_entry.py @@ -143,10 +143,9 @@ def get_payment_entry_for_employee( pe.received_amount = received_amount if party_account and bank: - if dt == "Employee Advance": + pe.set_exchange_rate() # always set source & target exchange rate + if dt == "Employee Advance" and pe.paid_to_account_currency != pe.paid_from_account_currency: pe.target_exchange_rate = current_exchange_rate - else: - pe.set_exchange_rate() pe.set_amounts() return pe