diff --git a/hrms/hr/doctype/attendance/attendance.py b/hrms/hr/doctype/attendance/attendance.py index 729cee92e5..368e83ba10 100644 --- a/hrms/hr/doctype/attendance/attendance.py +++ b/hrms/hr/doctype/attendance/attendance.py @@ -8,6 +8,7 @@ from frappe.utils import ( add_days, cint, + create_batch, cstr, format_date, get_datetime, @@ -15,6 +16,7 @@ getdate, nowdate, ) +from frappe.utils.background_jobs import get_job import hrms from hrms.hr.doctype.shift_assignment.shift_assignment import has_overlapping_timings @@ -347,6 +349,23 @@ def mark_bulk_attendance(data): if not data.unmarked_days: frappe.throw(_("Please select a date.")) return + if len(data.unmarked_days) > 10 or frappe.flags.test_bg_job: + job_id = f"process_bulk_attendance_for_employee_{data.employee}" + job = frappe.enqueue( + process_bulk_attendance_in_batches, data=data, job_id=job_id, timeout=600, deduplicate=True + ) + if job: + message = _( + "Bulk attendance marking is queued with a background job. It may take a while. You can monitor the job status {0}" + ).format(get_link_to_form("RQ Job", job.id, label="here")) + else: + message = _( + "Bulk attendance marking is already in progress for employee {0}. You can monitor the job status {1}" + ).format(frappe.bold(data.employee), get_link_to_form("RQ Job", get_job(job_id).id, label="here")) + frappe.msgprint(message, allow_dangerous_html=True) + else: + process_bulk_attendance_in_batches(data) + frappe.msgprint(_("Attendance marked successfully."), alert=True) for date in data.unmarked_days: doc_dict = { @@ -360,6 +379,31 @@ def mark_bulk_attendance(data): attendance.submit() +def process_bulk_attendance_in_batches(data, chunk_size=20): + savepoint = "mark_bulk_attendance" + for days in create_batch(data.unmarked_days, chunk_size): + for attendance_date in days: + try: + frappe.db.savepoint(savepoint) + doc_dict = { + "doctype": "Attendance", + "employee": data.employee, + "attendance_date": getdate(attendance_date), + "status": data.status, + "half_day_status": "Absent" if data.status == "Half Day" else None, + "shift": data.shift, + } + attendance = frappe.get_doc(doc_dict).insert() + attendance.submit() + except (DuplicateAttendanceError, OverlappingShiftAttendanceError, Exception): + if not frappe.flags.in_test: + frappe.db.rollback(save_point=savepoint) + continue + + if not frappe.flags.in_test: + frappe.db.commit() # nosemgrep + + @frappe.whitelist() def get_unmarked_days(employee, from_date, to_date, exclude_holidays=0): joining_date, relieving_date = frappe.get_cached_value( diff --git a/hrms/hr/doctype/attendance/attendance_list.js b/hrms/hr/doctype/attendance/attendance_list.js index 03d1db8c4c..f34ed33945 100644 --- a/hrms/hr/doctype/attendance/attendance_list.js +++ b/hrms/hr/doctype/attendance/attendance_list.js @@ -10,7 +10,6 @@ frappe.listview_settings["Attendance"] = { return [__(doc.status), "orange", "status,=," + doc.status]; } }, - onload: function (list_view) { let me = this; @@ -109,15 +108,6 @@ frappe.listview_settings["Attendance"] = { args: { data: data, }, - callback: function (r) { - if (r.message === 1) { - frappe.show_alert({ - message: __("Attendance Marked"), - indicator: "blue", - }); - cur_dialog.hide(); - } - }, }); }, ); diff --git a/hrms/hr/doctype/attendance/test_attendance.py b/hrms/hr/doctype/attendance/test_attendance.py index fee8e585b0..854019dcb6 100644 --- a/hrms/hr/doctype/attendance/test_attendance.py +++ b/hrms/hr/doctype/attendance/test_attendance.py @@ -16,6 +16,7 @@ getdate, nowdate, ) +from frappe.utils.user import add_role from erpnext.setup.doctype.employee.test_employee import make_employee @@ -24,6 +25,7 @@ OverlappingShiftAttendanceError, get_unmarked_days, mark_attendance, + mark_bulk_attendance, ) from hrms.tests.test_utils import get_first_sunday @@ -241,5 +243,35 @@ def test_duplicate_attendance_when_created_from_checkins_and_tool(self): ) self.assertEqual(len(attendances), 1) + def test_bulk_attendance_marking_through_bg(self): + user1 = "test_bg1@example.com" + user2 = "test_bg2@example.com" + employee1 = make_employee("test_bg1@example.com", company="_Test Company") + employee2 = make_employee("test_bg2@example.com", company="_Test Company") + add_role(user1, "HR Manager") + add_role(user2, "HR Manager") + frappe.flags.test_bg_job = True + frappe.set_user(user1) + data1 = frappe._dict(unmarked_days=[getdate()], employee=employee1, status="Present", shift="") + data2 = frappe._dict(unmarked_days=[getdate()], employee=employee2, status="Present", shift="") + mark_bulk_attendance(data1) + self.assertStartsWith( + frappe.message_log[-1].message, "Bulk attendance marking is queued with a background job." + ) + frappe.set_user(user2) + mark_bulk_attendance(data1) + self.assertStartsWith( + frappe.message_log[-1].message, "Bulk attendance marking is already in progress for employee" + ) + mark_bulk_attendance(data2) + self.assertStartsWith( + frappe.message_log[-1].message, "Bulk attendance marking is queued with a background job." + ) + frappe.flags.test_bg_job = False + mark_bulk_attendance(data2) + frappe.set_user("Administrator") + attendance_records = frappe.get_all("Attendance", {"employee": employee2}) + self.assertEqual(len(attendance_records), 1) + def tearDown(self): frappe.db.rollback()