Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: release v15 #2753

Merged
merged 22 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
711b323
test: fixed failing tests due implicitly set dates while creating sal…
asmitahase Feb 1, 2025
26cf8bb
test: failing assertion value due to missing precision
asmitahase Feb 1, 2025
3d07f81
fix: show validation message before changing shift start time
asmitahase Jan 31, 2025
9f16b93
refactor: better validation message
asmitahase Jan 31, 2025
967bed8
patch: typo in the validation message
asmitahase Feb 1, 2025
2ed6e34
Merge pull request #2746 from frappe/mergify/bp/version-15-hotfix/pr-…
asmitahase Feb 3, 2025
37d9032
Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr…
asmitahase Feb 3, 2025
4d2df35
Merge pull request #2747 from frappe/mergify/bp/version-15-hotfix/pr-…
asmitahase Feb 3, 2025
9dd0910
fix: applying permissions while fetching attendance in calendar view
asmitahase Jan 31, 2025
3eae1da
fix: don't show any records in calendar if not employee
asmitahase Jan 31, 2025
85dd728
fix: set date filters correctly
asmitahase Jan 31, 2025
d9fc19e
Merge pull request #2750 from frappe/mergify/bp/version-15-hotfix/pr-…
asmitahase Feb 4, 2025
bb9a5b0
chore: fixed test for leave policy assignment based on employee joini…
asmitahase Jan 24, 2025
6a537df
fix: allocate earned leaves pro-rata if policy assignment is submitte…
asmitahase Jan 24, 2025
e4d2d7d
chore: added a test for earned allocation with leave period spanning …
asmitahase Jan 24, 2025
e191f00
chore: resolved merge conflicts
asmitahase Feb 4, 2025
b92735b
Merge pull request #2752 from frappe/mergify/bp/version-15-hotfix/pr-…
asmitahase Feb 4, 2025
419f0c9
fix: include current month full tax in total income tax and current m…
Sudharsanan11 Jan 29, 2025
2f0c238
fix: remove employee other income from CTC
Sudharsanan11 Jan 29, 2025
0316026
fix: update ctc amount
Sudharsanan11 Jan 29, 2025
e25e369
Merge pull request #2756 from frappe/mergify/bp/version-15-hotfix/pr-…
AyshaHakeem Feb 4, 2025
69a8b8c
Merge pull request #2760 from frappe/mergify/bp/version-15-hotfix/pr-…
AyshaHakeem Feb 4, 2025
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
5 changes: 3 additions & 2 deletions hrms/hr/doctype/attendance/attendance.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@
"label": "Attendance Date",
"oldfieldname": "attendance_date",
"oldfieldtype": "Date",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "employee.company",
Expand Down Expand Up @@ -207,7 +208,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-04-05 20:55:02.905452",
"modified": "2025-01-31 11:45:54.846562",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance",
Expand Down
58 changes: 27 additions & 31 deletions hrms/hr/doctype/attendance/attendance.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,42 +228,38 @@ def unlink_attendance_from_checkins(self):

@frappe.whitelist()
def get_events(start, end, filters=None):
from frappe.desk.reportview import get_filters_cond

events = []

employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user})

if not employee:
return events

conditions = get_filters_cond("Attendance", filters, [])
add_attendance(events, start, end, conditions=conditions)
add_holidays(events, start, end, employee)
return events

return []
if isinstance(filters, str):
import json

def add_attendance(events, start, end, conditions=None):
query = """select name, attendance_date, status, employee_name
from `tabAttendance` where
attendance_date between %(from_date)s and %(to_date)s
and docstatus < 2"""
filters = json.loads(filters)
if not filters:
filters = []
filters.append(["attendance_date", "between", [get_datetime(start).date(), get_datetime(end).date()]])
attendance_records = add_attendance(filters)
add_holidays(attendance_records, start, end, employee)
return attendance_records

if conditions:
query += conditions

for d in frappe.db.sql(query, {"from_date": start, "to_date": end}, as_dict=True):
e = {
"name": d.name,
"doctype": "Attendance",
"start": d.attendance_date,
"end": d.attendance_date,
"title": f"{d.employee_name}: {cstr(d.status)}",
"status": d.status,
"docstatus": d.docstatus,
}
if e not in events:
events.append(e)
def add_attendance(filters):
attendance = frappe.get_list(
"Attendance",
fields=[
"name",
"'Attendance' as doctype",
"attendance_date as start",
"attendance_date as end",
"employee_name",
"status",
"docstatus",
],
filters=filters,
)
for record in attendance:
record["title"] = f"{record.employee_name} : {record.status}"
return attendance


def add_holidays(events, start, end, employee=None):
Expand Down
12 changes: 10 additions & 2 deletions hrms/hr/doctype/leave_allocation/test_earned_leaves.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,14 @@ def test_alloc_based_on_joining_date(self):

# assignment created on the last day of the current month
frappe.flags.current_date = get_last_day(getdate())

leave_policy_assignments = make_policy_assignment(self.employee, assignment_based_on="Joining Date")
"""set end date while making assignment based on Joining date because while start date is fetched from
employee master, make_policy_assignment ends up taking current date as end date if not specified which
causes the date of assignment to be later than the end date of leave period"""
start_date = self.employee.date_of_joining
end_date = get_last_day(add_months(self.employee.date_of_joining, 12))
leave_policy_assignments = make_policy_assignment(
self.employee, assignment_based_on="Joining Date", start_date=start_date, end_date=end_date
)
leaves_allocated = get_allocated_leaves(leave_policy_assignments[0])
effective_from = frappe.db.get_value(
"Leave Policy Assignment", leave_policy_assignments[0], "effective_from"
Expand Down Expand Up @@ -581,6 +587,8 @@ def make_policy_assignment(
"leave_policy": leave_policy.name,
"leave_period": leave_period.name,
"carry_forward": carry_forward,
"effective_from": start_date,
"effective_to": end_date,
}

leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ def get_encashment_created_after_leave_period(self, employee, is_carry_forward,
"Salary Structure for Encashment",
"Monthly",
employee,
from_date=start_date,
other_details={"leave_encashment_amount_per_day": 50},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def set_dates(self):
)
elif self.assignment_based_on == "Joining Date":
self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining")
if not self.effective_to:
self.effective_to = get_last_day(add_months(self.effective_from, 12))

def validate_policy_assignment_overlap(self):
leave_policy_assignment = frappe.db.get_value(
Expand Down Expand Up @@ -134,12 +136,13 @@ def get_new_leaves(self, annual_allocation, leave_details, date_of_joining):
from frappe.model.meta import get_field_precision

precision = get_field_precision(frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated"))

current_date = getdate(frappe.flags.current_date) or getdate()
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_details.is_compensatory:
new_leaves_allocated = 0
# if earned leave is being allcated after the effective period, then let them be calculated pro-rata

elif leave_details.is_earned_leave:
elif leave_details.is_earned_leave and current_date < getdate(self.effective_to):
new_leaves_allocated = self.get_leaves_for_passed_months(
annual_allocation, leave_details, date_of_joining
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, get_first_day, get_year_ending, getdate
from frappe.utils import add_days, add_months, get_first_day, get_year_ending, get_year_start, getdate

from hrms.hr.doctype.leave_application.test_leave_application import get_employee, get_leave_period
from hrms.hr.doctype.leave_period.test_leave_period import create_leave_period
Expand Down Expand Up @@ -33,6 +33,9 @@ def setUp(self):
self.original_doj = employee.date_of_joining
self.employee = employee

def tearDown(self):
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)

def test_grant_leaves(self):
leave_period = get_leave_period()
leave_policy = create_leave_policy(annual_allocation=10)
Expand Down Expand Up @@ -208,5 +211,58 @@ def test_pro_rated_leave_allocation_for_custom_date_range(self):

self.assertGreater(new_leaves_allocated, 0)

def tearDown(self):
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
def test_earned_leave_allocation_if_leave_policy_assignment_submitted_after_period(self):
year_start_date = get_year_start(getdate())
year_end_date = get_year_ending(getdate())
leave_period = create_leave_period(year_start_date, year_end_date)

# assignment 10 days after the leave period
frappe.flags.current_date = add_days(year_end_date, 10)
leave_type = create_leave_type(
leave_type_name="_Test Earned Leave", is_earned_leave=True, allocate_on_day="Last Day"
)
annual_earned_leaves = 10
leave_policy = create_leave_policy(leave_type=leave_type, annual_allocation=annual_earned_leaves)
leave_policy.submit()

data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name,
}
assignment = create_assignment(self.employee.name, frappe._dict(data))
assignment.submit()

earned_leave_allocation = frappe.get_value(
"Leave Allocation", {"leave_policy_assignment": assignment.name}, "new_leaves_allocated"
)
self.assertEqual(earned_leave_allocation, annual_earned_leaves)

def test_earned_leave_allocation_for_leave_period_spanning_two_years(self):
first_year_start_date = get_year_start(getdate())
second_year_end_date = get_year_ending(add_months(first_year_start_date, 12))
leave_period = create_leave_period(first_year_start_date, second_year_end_date)

# assignment during mid second year
frappe.flags.current_date = add_months(second_year_end_date, -6)
leave_type = create_leave_type(
leave_type_name="_Test Earned Leave", is_earned_leave=True, allocate_on_day="Last Day"
)
annual_earned_leaves = 24
leave_policy = create_leave_policy(leave_type=leave_type, annual_allocation=annual_earned_leaves)
leave_policy.submit()

data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name,
}
assignment = create_assignment(self.employee.name, frappe._dict(data))
assignment.submit()

earned_leave_allocation = frappe.get_value(
"Leave Allocation", {"leave_policy_assignment": assignment.name}, "new_leaves_allocated"
)
# months passed (18) are calculated correctly but total allocation of 36 exceeds 24 hence 24
# this upper cap is intentional, without that 36 leaves would be allocated correctly
self.assertEqual(earned_leave_allocation, 24)
17 changes: 17 additions & 0 deletions hrms/hr/doctype/shift_type/shift_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from itertools import groupby

import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, create_batch, get_datetime, get_time, getdate

Expand All @@ -25,6 +26,22 @@


class ShiftType(Document):
def validate(self):
if self.is_field_modified("start_time") and self.unlinked_checkins_exist():
frappe.throw(
title=_("Unmarked Check-in Logs Found"),
msg=_("Mark attendance for existing check-in/out logs before changing shift settings"),
)

def is_field_modified(self, fieldname):
return not self.is_new() and self.has_value_changed(fieldname)

def unlinked_checkins_exist(self):
return frappe.db.exists(
"Employee Checkin",
{"shift": self.name, "attendance": ["is", "not set"], "skip_auto_attendance": 0},
)

@frappe.whitelist()
def process_auto_attendance(self):
if (
Expand Down
36 changes: 36 additions & 0 deletions hrms/hr/doctype/shift_type/test_shift_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,42 @@ def test_mark_attendance_for_default_shift_when_shift_assignment_is_not_overlapp
"Absent",
)

def test_validation_for_unlinked_logs_before_changing_important_shift_configuration(self):
# the important shift configuration is start time, it is used to sort logs chronologically
shift = setup_shift_type(shift_type="Test Shift", start_time="10:00:00", end_time="18:00:00")
employee = make_employee(
"[email protected]", company="_Test Company", default_shift=shift.name
)

from hrms.hr.doctype.employee_checkin.test_employee_checkin import make_checkin

in_time = datetime.combine(getdate(), get_time("10:00:00"))
check_in = make_checkin(employee, in_time)
check_in.fetch_shift()
# Case 1: raise valdiation error if shift time is being changed and checkin logs exists
shift.start_time = get_time("10:15:00")
self.assertRaises(frappe.ValidationError, shift.save)

# don't raise validation error if something else is being changed
# even if checkin logs exists, it's probably fine
shift.reload()
shift.begin_check_in_before_shift_start_time = 120
shift.save()
self.assertEqual(
frappe.get_value("Shift Type", shift.name, "begin_check_in_before_shift_start_time"), 120
)
out_time = datetime.combine(getdate(), get_time("18:00:00"))
check_out = make_checkin(employee, out_time)
check_out.fetch_shift()
shift.process_auto_attendance()

# Case 2: allow shift time to change if no unlinked logs exist
shift.start_time = get_time("10:15:00")
shift.save()
self.assertEqual(
get_time(frappe.get_value("Shift Type", shift.name, "start_time")), get_time("10:15:00")
)


def setup_shift_type(**args):
args = frappe._dict(args)
Expand Down
9 changes: 6 additions & 3 deletions hrms/payroll/doctype/salary_slip/salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,10 +932,14 @@ def compute_income_tax_breakup(self):

if hasattr(self, "total_structured_tax_amount") and hasattr(self, "current_structured_tax_amount"):
self.future_income_tax_deductions = (
self.total_structured_tax_amount - self.income_tax_deducted_till_date
self.total_structured_tax_amount
+ self.get("full_tax_on_additional_earnings", 0)
- self.income_tax_deducted_till_date
)

self.current_month_income_tax = self.current_structured_tax_amount
self.current_month_income_tax = self.current_structured_tax_amount + self.get(
"full_tax_on_additional_earnings", 0
)

# non included current_month_income_tax separately as its already considered
# while calculating income_tax_deducted_till_date
Expand All @@ -949,7 +953,6 @@ def compute_ctc(self):
+ self.current_structured_taxable_earnings_before_exemption
+ self.future_structured_taxable_earnings_before_exemption
+ self.current_additional_earnings
+ self.other_incomes
+ self.unclaimed_taxable_benefits
+ self.non_taxable_earnings
)
Expand Down
10 changes: 6 additions & 4 deletions hrms/payroll/doctype/salary_slip/test_salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -1331,7 +1331,9 @@ def test_statistical_component_based_on_payment_days(self):
precision = entry.precision("amount")
break

self.assertEqual(amount, flt((1000 * ss.payment_days / ss.total_working_days) * 0.5, precision))
self.assertEqual(
amount, flt(flt((1000 * ss.payment_days / ss.total_working_days), precision) * 0.5, precision)
)

def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
Expand Down Expand Up @@ -1474,14 +1476,14 @@ def test_income_tax_breakup_fields(self):

monthly_tax_amount = 11403.6

self.assertEqual(salary_slip.ctc, 1226000.0)
self.assertEqual(salary_slip.ctc, 1216000.0)
self.assertEqual(salary_slip.income_from_other_sources, 10000.0)
self.assertEqual(salary_slip.non_taxable_earnings, 10000.0)
self.assertEqual(salary_slip.total_earnings, 1236000.0)
self.assertEqual(salary_slip.total_earnings, 1226000.0)
self.assertEqual(salary_slip.standard_tax_exemption_amount, 50000.0)
self.assertEqual(salary_slip.tax_exemption_declaration, 100000.0)
self.assertEqual(salary_slip.deductions_before_tax_calculation, 2400.0)
self.assertEqual(salary_slip.annual_taxable_amount, 1073600.0)
self.assertEqual(salary_slip.annual_taxable_amount, 1063600.0)
self.assertEqual(flt(salary_slip.income_tax_deducted_till_date, 2), monthly_tax_amount)
self.assertEqual(flt(salary_slip.current_month_income_tax, 2), monthly_tax_amount)
self.assertEqual(flt(salary_slip.future_income_tax_deductions, 2), 125439.65)
Expand Down
Loading