Skip to content

Commit 6866e84

Browse files
authored
Merge branch 'frappe:develop' into develop
2 parents 81e0c86 + 9ff29f2 commit 6866e84

File tree

14 files changed

+178
-52
lines changed

14 files changed

+178
-52
lines changed

.github/workflows/initiate_release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
name: Create weekly release pull requests
55
on:
66
schedule:
7-
# 9:30 UTC => 3 PM IST Tuesday
8-
- cron: "30 9 * * 2"
7+
# 9:45 UTC => 3:15 PM IST Tuesday
8+
- cron: "45 9 * * 2"
99
workflow_dispatch:
1010

1111
jobs:

frontend/src/components/CheckInPanel.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
</div>
7070
</template>
7171

72-
<Button variant="solid" class="w-full py-5 text-sm" @click.once="submitLog(nextAction.action)">
72+
<Button :loading="checkins.insert.loading" variant="solid" class="w-full py-5 text-sm disabled:bg-gray-700" @click="submitLog(nextAction.action)">
7373
{{ __("Confirm {0}", [nextAction.label]) }}
7474
</Button>
7575
</div>
@@ -93,7 +93,6 @@ const checkinTimestamp = ref(null)
9393
const latitude = ref(0)
9494
const longitude = ref(0)
9595
const locationStatus = ref("")
96-
9796
const settings = createResource({
9897
url: "hrms.api.get_hr_settings",
9998
auto: true,

hrms/hr/doctype/attendance/attendance.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@
122122
"label": "Attendance Date",
123123
"oldfieldname": "attendance_date",
124124
"oldfieldtype": "Date",
125-
"reqd": 1
125+
"reqd": 1,
126+
"search_index": 1
126127
},
127128
{
128129
"fetch_from": "employee.company",
@@ -207,7 +208,7 @@
207208
"idx": 1,
208209
"is_submittable": 1,
209210
"links": [],
210-
"modified": "2024-04-05 20:55:02.905452",
211+
"modified": "2025-01-31 11:45:54.846562",
211212
"modified_by": "Administrator",
212213
"module": "HR",
213214
"name": "Attendance",

hrms/hr/doctype/attendance/attendance.py

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -228,42 +228,38 @@ def unlink_attendance_from_checkins(self):
228228

229229
@frappe.whitelist()
230230
def get_events(start, end, filters=None):
231-
from frappe.desk.reportview import get_filters_cond
232-
233-
events = []
234-
235231
employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user})
236-
237232
if not employee:
238-
return events
239-
240-
conditions = get_filters_cond("Attendance", filters, [])
241-
add_attendance(events, start, end, conditions=conditions)
242-
add_holidays(events, start, end, employee)
243-
return events
244-
233+
return []
234+
if isinstance(filters, str):
235+
import json
245236

246-
def add_attendance(events, start, end, conditions=None):
247-
query = """select name, attendance_date, status, employee_name
248-
from `tabAttendance` where
249-
attendance_date between %(from_date)s and %(to_date)s
250-
and docstatus < 2"""
237+
filters = json.loads(filters)
238+
if not filters:
239+
filters = []
240+
filters.append(["attendance_date", "between", [get_datetime(start).date(), get_datetime(end).date()]])
241+
attendance_records = add_attendance(filters)
242+
add_holidays(attendance_records, start, end, employee)
243+
return attendance_records
251244

252-
if conditions:
253-
query += conditions
254245

255-
for d in frappe.db.sql(query, {"from_date": start, "to_date": end}, as_dict=True):
256-
e = {
257-
"name": d.name,
258-
"doctype": "Attendance",
259-
"start": d.attendance_date,
260-
"end": d.attendance_date,
261-
"title": f"{d.employee_name}: {cstr(d.status)}",
262-
"status": d.status,
263-
"docstatus": d.docstatus,
264-
}
265-
if e not in events:
266-
events.append(e)
246+
def add_attendance(filters):
247+
attendance = frappe.get_list(
248+
"Attendance",
249+
fields=[
250+
"name",
251+
"'Attendance' as doctype",
252+
"attendance_date as start",
253+
"attendance_date as end",
254+
"employee_name",
255+
"status",
256+
"docstatus",
257+
],
258+
filters=filters,
259+
)
260+
for record in attendance:
261+
record["title"] = f"{record.employee_name} : {record.status}"
262+
return attendance
267263

268264

269265
def add_holidays(events, start, end, employee=None):

hrms/hr/doctype/employee_checkin/employee_checkin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class CheckinRadiusExceededError(frappe.ValidationError):
2020

2121

2222
class EmployeeCheckin(Document):
23+
def before_validate(self):
24+
self.time = get_datetime(self.time).replace(microsecond=0)
25+
2326
def validate(self):
2427
validate_active_employee(self.employee)
2528
self.validate_duplicate_log()
@@ -90,6 +93,7 @@ def validate_distance_from_shift_location(self):
9093
"start_date": ["<=", self.time],
9194
"shift_location": ["is", "set"],
9295
"docstatus": 1,
96+
"status": "Active",
9397
},
9498
or_filters=[["end_date", ">=", self.time], ["end_date", "is", "not set"]],
9599
pluck="shift_location",

hrms/hr/doctype/employee_checkin/test_employee_checkin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def test_add_log_based_on_employee_field(self):
7575
employee.attendance_device_id = "3344"
7676
employee.save()
7777

78-
time_now = now_datetime().__str__()[:-7]
78+
time_now = now_datetime().replace(microsecond=0)
7979
employee_checkin = add_log_based_on_employee_field("3344", time_now, "mumbai_first_floor", "IN")
8080
self.assertEqual(employee_checkin.employee, employee.name)
8181
self.assertEqual(employee_checkin.time, time_now)

hrms/hr/doctype/leave_allocation/test_earned_leaves.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,14 @@ def test_alloc_based_on_joining_date(self):
137137

138138
# assignment created on the last day of the current month
139139
frappe.flags.current_date = get_last_day(getdate())
140-
141-
leave_policy_assignments = make_policy_assignment(self.employee, assignment_based_on="Joining Date")
140+
"""set end date while making assignment based on Joining date because while start date is fetched from
141+
employee master, make_policy_assignment ends up taking current date as end date if not specified which
142+
causes the date of assignment to be later than the end date of leave period"""
143+
start_date = self.employee.date_of_joining
144+
end_date = get_last_day(add_months(self.employee.date_of_joining, 12))
145+
leave_policy_assignments = make_policy_assignment(
146+
self.employee, assignment_based_on="Joining Date", start_date=start_date, end_date=end_date
147+
)
142148
leaves_allocated = get_allocated_leaves(leave_policy_assignments[0])
143149
effective_from = frappe.db.get_value(
144150
"Leave Policy Assignment", leave_policy_assignments[0], "effective_from"
@@ -581,6 +587,8 @@ def make_policy_assignment(
581587
"leave_policy": leave_policy.name,
582588
"leave_period": leave_period.name,
583589
"carry_forward": carry_forward,
590+
"effective_from": start_date,
591+
"effective_to": end_date,
584592
}
585593

586594
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))

hrms/hr/doctype/leave_encashment/test_leave_encashment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ def get_encashment_created_after_leave_period(self, employee, is_carry_forward,
349349
"Salary Structure for Encashment",
350350
"Monthly",
351351
employee,
352+
from_date=start_date,
352353
other_details={"leave_encashment_amount_per_day": 50},
353354
)
354355

hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def set_dates(self):
3838
)
3939
elif self.assignment_based_on == "Joining Date":
4040
self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining")
41+
if not self.effective_to:
42+
self.effective_to = get_last_day(add_months(self.effective_from, 12))
4143

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

136138
precision = get_field_precision(frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated"))
137-
139+
current_date = getdate(frappe.flags.current_date) or getdate()
138140
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
139141
if leave_details.is_compensatory:
140142
new_leaves_allocated = 0
143+
# if earned leave is being allcated after the effective period, then let them be calculated pro-rata
141144

142-
elif leave_details.is_earned_leave:
145+
elif leave_details.is_earned_leave and current_date < getdate(self.effective_to):
143146
new_leaves_allocated = self.get_leaves_for_passed_months(
144147
annual_allocation, leave_details, date_of_joining
145148
)

hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import frappe
55
from frappe.tests import IntegrationTestCase
6-
from frappe.utils import add_months, get_first_day, get_year_ending, getdate
6+
from frappe.utils import add_days, add_months, get_first_day, get_year_ending, get_year_start, getdate
77

88
from hrms.hr.doctype.leave_application.test_leave_application import get_employee, get_leave_period
99
from hrms.hr.doctype.leave_period.test_leave_period import create_leave_period
@@ -33,6 +33,9 @@ def setUp(self):
3333
self.original_doj = employee.date_of_joining
3434
self.employee = employee
3535

36+
def tearDown(self):
37+
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
38+
3639
def test_grant_leaves(self):
3740
leave_period = get_leave_period()
3841
leave_policy = create_leave_policy(annual_allocation=10)
@@ -208,5 +211,58 @@ def test_pro_rated_leave_allocation_for_custom_date_range(self):
208211

209212
self.assertGreater(new_leaves_allocated, 0)
210213

211-
def tearDown(self):
212-
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
214+
def test_earned_leave_allocation_if_leave_policy_assignment_submitted_after_period(self):
215+
year_start_date = get_year_start(getdate())
216+
year_end_date = get_year_ending(getdate())
217+
leave_period = create_leave_period(year_start_date, year_end_date)
218+
219+
# assignment 10 days after the leave period
220+
frappe.flags.current_date = add_days(year_end_date, 10)
221+
leave_type = create_leave_type(
222+
leave_type_name="_Test Earned Leave", is_earned_leave=True, allocate_on_day="Last Day"
223+
)
224+
annual_earned_leaves = 10
225+
leave_policy = create_leave_policy(leave_type=leave_type, annual_allocation=annual_earned_leaves)
226+
leave_policy.submit()
227+
228+
data = {
229+
"assignment_based_on": "Leave Period",
230+
"leave_policy": leave_policy.name,
231+
"leave_period": leave_period.name,
232+
}
233+
assignment = create_assignment(self.employee.name, frappe._dict(data))
234+
assignment.submit()
235+
236+
earned_leave_allocation = frappe.get_value(
237+
"Leave Allocation", {"leave_policy_assignment": assignment.name}, "new_leaves_allocated"
238+
)
239+
self.assertEqual(earned_leave_allocation, annual_earned_leaves)
240+
241+
def test_earned_leave_allocation_for_leave_period_spanning_two_years(self):
242+
first_year_start_date = get_year_start(getdate())
243+
second_year_end_date = get_year_ending(add_months(first_year_start_date, 12))
244+
leave_period = create_leave_period(first_year_start_date, second_year_end_date)
245+
246+
# assignment during mid second year
247+
frappe.flags.current_date = add_months(second_year_end_date, -6)
248+
leave_type = create_leave_type(
249+
leave_type_name="_Test Earned Leave", is_earned_leave=True, allocate_on_day="Last Day"
250+
)
251+
annual_earned_leaves = 24
252+
leave_policy = create_leave_policy(leave_type=leave_type, annual_allocation=annual_earned_leaves)
253+
leave_policy.submit()
254+
255+
data = {
256+
"assignment_based_on": "Leave Period",
257+
"leave_policy": leave_policy.name,
258+
"leave_period": leave_period.name,
259+
}
260+
assignment = create_assignment(self.employee.name, frappe._dict(data))
261+
assignment.submit()
262+
263+
earned_leave_allocation = frappe.get_value(
264+
"Leave Allocation", {"leave_policy_assignment": assignment.name}, "new_leaves_allocated"
265+
)
266+
# months passed (18) are calculated correctly but total allocation of 36 exceeds 24 hence 24
267+
# this upper cap is intentional, without that 36 leaves would be allocated correctly
268+
self.assertEqual(earned_leave_allocation, 24)

hrms/hr/doctype/shift_type/shift_type.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from itertools import groupby
77

88
import frappe
9+
from frappe import _
910
from frappe.model.document import Document
1011
from frappe.utils import cint, create_batch, get_datetime, get_time, getdate
1112

@@ -25,6 +26,22 @@
2526

2627

2728
class ShiftType(Document):
29+
def validate(self):
30+
if self.is_field_modified("start_time") and self.unlinked_checkins_exist():
31+
frappe.throw(
32+
title=_("Unmarked Check-in Logs Found"),
33+
msg=_("Mark attendance for existing check-in/out logs before changing shift settings"),
34+
)
35+
36+
def is_field_modified(self, fieldname):
37+
return not self.is_new() and self.has_value_changed(fieldname)
38+
39+
def unlinked_checkins_exist(self):
40+
return frappe.db.exists(
41+
"Employee Checkin",
42+
{"shift": self.name, "attendance": ["is", "not set"], "skip_auto_attendance": 0},
43+
)
44+
2845
@frappe.whitelist()
2946
def process_auto_attendance(self):
3047
if (

hrms/hr/doctype/shift_type/test_shift_type.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,42 @@ def test_mark_attendance_for_default_shift_when_shift_assignment_is_not_overlapp
688688
"Absent",
689689
)
690690

691+
def test_validation_for_unlinked_logs_before_changing_important_shift_configuration(self):
692+
# the important shift configuration is start time, it is used to sort logs chronologically
693+
shift = setup_shift_type(shift_type="Test Shift", start_time="10:00:00", end_time="18:00:00")
694+
employee = make_employee(
695+
"[email protected]", company="_Test Company", default_shift=shift.name
696+
)
697+
698+
from hrms.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
699+
700+
in_time = datetime.combine(getdate(), get_time("10:00:00"))
701+
check_in = make_checkin(employee, in_time)
702+
check_in.fetch_shift()
703+
# Case 1: raise valdiation error if shift time is being changed and checkin logs exists
704+
shift.start_time = get_time("10:15:00")
705+
self.assertRaises(frappe.ValidationError, shift.save)
706+
707+
# don't raise validation error if something else is being changed
708+
# even if checkin logs exists, it's probably fine
709+
shift.reload()
710+
shift.begin_check_in_before_shift_start_time = 120
711+
shift.save()
712+
self.assertEqual(
713+
frappe.get_value("Shift Type", shift.name, "begin_check_in_before_shift_start_time"), 120
714+
)
715+
out_time = datetime.combine(getdate(), get_time("18:00:00"))
716+
check_out = make_checkin(employee, out_time)
717+
check_out.fetch_shift()
718+
shift.process_auto_attendance()
719+
720+
# Case 2: allow shift time to change if no unlinked logs exist
721+
shift.start_time = get_time("10:15:00")
722+
shift.save()
723+
self.assertEqual(
724+
get_time(frappe.get_value("Shift Type", shift.name, "start_time")), get_time("10:15:00")
725+
)
726+
691727

692728
def setup_shift_type(**args):
693729
args = frappe._dict(args)

hrms/payroll/doctype/salary_slip/salary_slip.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -932,10 +932,14 @@ def compute_income_tax_breakup(self):
932932

933933
if hasattr(self, "total_structured_tax_amount") and hasattr(self, "current_structured_tax_amount"):
934934
self.future_income_tax_deductions = (
935-
self.total_structured_tax_amount - self.income_tax_deducted_till_date
935+
self.total_structured_tax_amount
936+
+ self.get("full_tax_on_additional_earnings", 0)
937+
- self.income_tax_deducted_till_date
936938
)
937939

938-
self.current_month_income_tax = self.current_structured_tax_amount
940+
self.current_month_income_tax = self.current_structured_tax_amount + self.get(
941+
"full_tax_on_additional_earnings", 0
942+
)
939943

940944
# non included current_month_income_tax separately as its already considered
941945
# while calculating income_tax_deducted_till_date
@@ -949,7 +953,6 @@ def compute_ctc(self):
949953
+ self.current_structured_taxable_earnings_before_exemption
950954
+ self.future_structured_taxable_earnings_before_exemption
951955
+ self.current_additional_earnings
952-
+ self.other_incomes
953956
+ self.unclaimed_taxable_benefits
954957
+ self.non_taxable_earnings
955958
)

0 commit comments

Comments
 (0)