From cf1580491056c86e922182bc928d59e89cb63d23 Mon Sep 17 00:00:00 2001
From: mymi14s <mymi14s@hotmail.com>
Date: Thu, 28 Dec 2023 18:44:03 +0000
Subject: [PATCH] added: hourly marking

---
 one_fm/hooks.py                |   3 +-
 one_fm/overrides/attendance.py | 195 ++++++++++++++++++++++++++++++++-
 2 files changed, 195 insertions(+), 3 deletions(-)

diff --git a/one_fm/hooks.py b/one_fm/hooks.py
index 5a80fa80c4..65db180095 100644
--- a/one_fm/hooks.py
+++ b/one_fm/hooks.py
@@ -522,7 +522,8 @@
 	"hourly": [
 		# "one_fm.api.tasks.send_checkin_hourly_reminder",
 		'one_fm.utils.send_gp_letter_attachment_reminder3',
-		'one_fm.utils.send_gp_letter_reminder'
+		'one_fm.utils.send_gp_letter_reminder',
+        "one_fm.overrides.attendance.run_attendance_marking_hourly",
 	],
 
 	"weekly": [
diff --git a/one_fm/overrides/attendance.py b/one_fm/overrides/attendance.py
index 2e1e27b74a..32639381c0 100644
--- a/one_fm/overrides/attendance.py
+++ b/one_fm/overrides/attendance.py
@@ -2,9 +2,10 @@
 import frappe, erpnext
 from frappe import _
 from frappe.utils import (
-	now_datetime,nowtime, cstr, getdate, get_datetime, cint, add_to_date,
-	datetime, today, add_days, now
+    now_datetime,nowtime, cstr, getdate, get_datetime, cint, add_to_date,
+    datetime, today, add_days, now
 )
+from datetime import timedelta
 from hrms.hr.doctype.attendance.attendance import *
 from hrms.hr.utils import  validate_active_employee, get_holidays_for_employee
 from one_fm.utils import get_holiday_today
@@ -997,3 +998,193 @@ def mark_timesheet_daily_attendance(timesheet_employees,start_date):
             frappe.db.commit()
     except:
         frappe.log_error(message=frappe.get_traceback(), title ='Timesheet Attendance')
+
+
+class AttendanceMarking():
+    """
+        This class will be used to mark attendance
+    """
+
+    def __ini__(self):
+        self.start = None
+        self.end = None
+
+    def get_datetime(self):
+        dt = now_datetime()
+        dt = dt.replace(minute=0, second=0, microsecond=0)
+        self.start = dt + timedelta(hours=-2)
+        self.end = dt + timedelta(hours=-1)
+
+    
+    def get_shifts(self):
+        self.get_datetime()
+        shifts =  frappe.db.sql(f"""
+            SELECT * FROM `tabShift Assignment`
+            WHERE
+            end_datetime BETWEEN '{self.start}' AND  '{self.end}'
+        """, as_dict=1)
+        if shifts:
+            checkins = self.get_checkins(tuple([i.name for i in shifts]) if len(shifts)>1 else (shifts[0]))
+            if checkins:
+                # employees = [i.employee for i in shifts]
+                checked_in_employees = [i.employee for i in checkins]
+                no_checkins = [i for i in shifts if not i.employee in checked_in_employees]
+                if no_checkins: #create absent
+                    for i in no_checkins:
+                        try:
+                            record = frappe._dict({**dict(i), **{
+                                "status":"Absent", "comment":"No checkin record found", "working_hours":0,
+                                "dt":"Shift Assignment"}})
+                            self.create_attendance(record)
+                        except:
+                            pass
+                if checkins: # check for work hours
+                    # start checkin
+                    for i in checkins:
+                        if not frappe.db.exists("Attendance", {
+                            'employee':i.employee,
+                            'attendance_date':i.shift_actual_start.date(),
+                            'roster_type':i.roster_type,
+                            'status': ["IN", ["Present", "On Leave", "Holiday", "Day Off"]]
+                            }):
+                            total_hours = (i.shift_actual_end - i.shift_actual_start).total_seconds() / (60*60)
+                            half_hour = total_hours/2
+                            working_hours = 0
+                            status = "Absent"
+                            comment = ""
+                            if ((i.earliest_time - i.shift_actual_end).total_seconds() / (60*60)) > half_hour:
+                                status = 'Absent'
+                                comment = f" Checked in late, checkin in at {i.earliest_time}"
+                            elif i.earliest_time and i.latest_time:
+                                working_hours = ((i.latest_time - i.earliest_time).total_seconds() / (60*60))
+                                if working_hours < half_hour:
+                                    status = "Absent"
+                                    comment = f"Worked less than 50% of {total_hours}hrs."
+                                else:
+                                    status = "Present"
+                                    comment = ""
+                            elif i.earliest_time and not i.latest_time:
+                                working_hours = (i.shift_actual_end - i.earliest_time).total_seconds() / (60*60)
+                                if working_hours < half_hour:
+                                    status = "Absent"
+                                    comment = f"Worked less than 50% of {total_hours}hrs."
+                                else:
+                                    status = "Present"
+                                    comment = ""
+
+                            #  continue to mark attendace
+                            try:
+                                self.create_attendance(frappe._dict({**dict(i), **{
+                                    "status":status, "comment":comment, "working_hours":working_hours,
+                                    "dt":"Employee Checkin"}}))
+                            except Exception as e:
+                                print(e)
+
+    def get_checkins(self, shift_assignments):
+        query = f"""
+            SELECT 
+            ec.name, 
+            ec.owner, 
+            ec.creation, 
+            ec.modified, 
+            ec.modified_by, 
+            ec.docstatus, 
+            ec.idx, 
+            ec.employee, 
+            ec.employee_name, 
+            ec.log_type, 
+            ec.late_entry,
+            ec.early_exit,
+            ec.time, 
+            ec.date, 
+            ec.skip_auto_attendance, 
+            ec.shift_actual_start, 
+            ec.shift_actual_end, 
+            ec.shift_assignment, 
+            ec.operations_shift, 
+            ec.shift_type,
+            ec.roster_type, 
+            ec.operations_site, 
+            ec.project, 
+            ec.company, 
+            ec.operations_role, 
+            ec.post_abbrv,
+            ec.shift_permission, 
+            ec.actual_time, 
+            MIN(CASE WHEN ec.log_type = 'IN' THEN ec.time END) AS earliest_time,
+            MAX(CASE WHEN ec.log_type = 'OUT' THEN ec.time END) AS latest_time
+        FROM 
+            `tabEmployee Checkin` ec
+        WHERE 
+            ec.shift_assignment in {shift_assignments}
+        GROUP BY 
+            ec.shift_assignment;
+        """
+        return frappe.db.sql(query, as_dict=1)
+
+
+    
+    def create_attendance(self, record, day_off=False):
+        # clear absent
+        _date = None
+        if record.shift_actual_start:
+            _date = record.shift_actual_start.date()
+        elif record.date:
+            _date = record.date
+        else:
+            _date = record.start_date
+        try:
+            frappe.db.sql(f"""
+                DELETE FROM `tabAttendance` WHERE employee="{record.employee}" AND
+                attendance_date="{_date}" 
+                AND roster_type="{record.roster_type}"
+                AND status="Absent"
+            """)
+        except:
+            pass
+        frappe.db.commit()
+        doc = frappe._dict({})
+        doc.doctype = "Attendance"
+        doc.employee = record.employee
+        doc.status = record.status
+        doc.attendance_date = _date
+        if record.shift_assignment:
+            doc.shift_assignment = record.shift_assignment
+            doc.shift = record.shift_type
+            doc.operations_shift = record.operations_shift
+            doc.site = record.site
+        if record.dt=="Shift Assignment":
+            doc.shift_assignment = record.name
+            doc.shift = record.shift_type
+            doc.operations_shift = record.shift
+            doc.site = record.site
+        if record.late_entry:
+            doc.late_entry= record.late_entry
+        if record.early_exit:
+            doc.early_exit = record.early_exit
+        # check if worked less
+        if record.working_hours:
+            doc.working_hours = record.working_hours
+        if record.roster_type:
+            doc.roster_type = record.roster_type
+        if record.comment:
+            doc.comment = record.comment
+        doc = frappe.get_doc(doc)
+        doc.status = record.status
+        doc.flags.ignore_validate = True
+        doc.save()
+        doc.db_set('docstatus', 1)
+        # updated checkins if exists
+        if record.dt=="Employee Checkin":
+            if record.earliest_time:
+                frappe.db.set_value("Employee Checkin", record.name, 'attendance', doc.name)
+            # if record.latest_checkout:
+            #     frappe.db.set_value("Employee Checkin", record.checkout.name, 'attendance', doc.name)
+        frappe.db.commit()
+
+
+
+def run_attendance_marking_hourly():
+    """Marks Attendances for Hourly Employees based on Employee Checkin."""
+    attendance_marking = AttendanceMarking()
+    frappe.enqueue(attendance_marking.get_shifts(), queue="long", timeout=4000)
\ No newline at end of file