
    {jq	                        d dl Z d dlZd dlZd dlZd dlZd dlmZmZ d dl	m
Z
mZmZmZ d dlmZ  ej                   e      Zg dZdddd	d
dZd
ZdZ G d dej.                        ZdZdZdZy)    N)timezoneutc)_apifieldsmodels)	UserError)JanuaryFebruaryMarchAprilMayJuneJulyAugust	SeptemberOctoberNovemberDecember)z#ddd6fez#5b21b6)z#fed7aaz#9a3412)z#fecdd3z#9f1239)z#fbcfe8z#9d174d)z#e9e9e9z#57534eALPLSLMLUL   c                   ,   e Zd ZdZdZd Zej                  dd       Zej                  d d       Z	e
d        Z	 	 d dZd	 Zd
 Z	 	 d dZd Zej                  d        Zd Zej                  d        Zd Ze
d        Zd Zej                  d        Zej                  d        Z	 	 d dZej                  d        Ze
d        Ze
d        Ze
d        Ze
d        Ze
d        Zd Z ej                  	 	 	 	 d!d       Z!ej                  d d       Z"y)"XenoAttendanceReportzxeno.attendance.reportz2Monthly attendance report from HRSystem SQL Serverc           
         	 dd l }| j                  d   j                         }|j                  d      }|st        t        d            |j                  |t        |j                  dd            |j                  d      |j                  d	      |j                  d
d      dd      S # t        $ r t        t        d            w xY w)Nr   zThe 'pymssql' Python package is not installed in the Odoo environment. Install it in the Odoo container to enable the attendance report.zir.config_parameterz
hrsys.hostztHRSystem connection is not configured. Set hrsys.host / hrsys.db / hrsys.user / hrsys.password in system parameters.z
hrsys.port1433z
hrsys.userzhrsys.passwordzhrsys.dbHRSystemZ      )serverportuserpassworddatabasetimeoutlogin_timeout)	pymssqlImportErrorr	   r   envsudo	get_paramconnectint)selfr+   phosts       +xeno_attendance/models/attendance_report.py_connzXenoAttendanceReport._conn.   s    	 HH*+002{{<(AO   Q[[v67\*[[!12[[Z8b  
 	
  	A%  	s   B) )CNc                 n   g }|r|j                  ddt        |      f       |r|j                  ddt        |      gf       |r|j                  ddt        |      f       |sy| j                  d   j                         j	                  |      }|j                  d      D ch c]  }|s|	 c}S c c}w )	aB  xeno_employee_code set for hr.employee matching ALL of the given
        filters (Department/Group/Position on the Attendance report page) --
        None when no filter is given at all, meaning "don't restrict",
        matching restrict_codes' own None-means-unrestricted convention
        everywhere else in this file.department_id=category_idsinjob_idNhr.employeexeno_employee_code)appendr1   r-   r.   searchmapped)r2   r8   category_idr<   domain	employeescs          r5   xeno_codes_for_filtersz+XenoAttendanceReport.xeno_codes_for_filtersG   s     MM?C]1CDEMM>4#k2B1CDEMM8S#f+67HH]+00299&A	$++,@AGaQGGGs   #B2+B2c                    t        j                  ||      d   }d||fz  }d|||fz  }| j                         }	 |j                  d      }	|	j	                  d||f       |	j                         }
|	j	                  d||f       |	j                         D cg c]7  }|d   |d	   j                  d
      |d   j                  d
      |d   xs dd9 }}|	j	                  d||f       |	j                         D ch c]  }t        |d          }}|	j	                  d       |	j                         D ci c]5  }t        |d         |d   dj                  d |d   |d   fD              d7 }}t        |      }|j                          i }| j                  d   j                         j                  dddd|z  fddd|z  fg      D ]y  }|j                  j                         }|j                   j                         }||k  s=|j"                  ||j                  d
      <   |t%        j&                  d       z  }||k  r={ t        | j                  d!   j                         j                  g       j)                  d"            }t+        ||z
  |z
        }|D|D ch c]  }t        |       }}|
D cg c]!  }t        |j-                  d            |v s |# }
}|r5|
D cg c]*  }t        |j-                  d            t        |      k(  r|, }
}| j/                  |
||||||#       i }| j                  d$   j                         j                  d%d|fd%d|fg      D ]:  }|j0                  xs d||j2                  |j                  j                  d
      f<   < i }| j                  d&   j                         j                  d'dd|fdd|fg      D ]  }|j4                  d(k(  r|j6                  nGt9        | j                  d&   j:                  d)   j<                        j-                  |j4                        }t?        |j                  t%        j                  ||d            }tA        |j                   t%        j                  |||            }||k  s|xs d||j2                  |j                  d
      f<   |t%        j&                  d       z  }||k  rC | j                  d*   j                         jC                         }|D ch c]  \  }}|	 }}}|D ch c]  \  }}|	 } }}||D ch c]  }t        |       c}n|rt        |      hnd }!|
D ch c]  }t        |j-                  d             }"}||z  | z  |"z
  }#|!|#|!z  }#|#D ]2  }$|j-                  |$      }%|%s|
jE                  |%d   |$|%d+   d,d-       4 i }&| j                  d.   j                         j                  d%d|fd%d|fg      D ]  }'|'jF                  xs d |'jH                  xs d |'jJ                  xs d|'jL                  j"                  tN        jP                  jS                  |'jT                        d/|&|'j2                  |'j                  j                  d
      f<    | jW                  ||||      }(| jY                  |
D cg c]  }|j-                  d       c}      })| j[                  |
||||||&|(|)||       |
|||||||&|(|)||d0S c c}w c c}w c c}w # |j                          w xY wc c}w c c}w c c}w c c}}w c c}}w c c}w c c}w c c}w )1N   z%04d-%02d-01%04d-%02d-%02dT)as_dictz(EXEC [sp_get_attendance_month_V3] %s, %szSELECT la.employee_id, la.start_date, la.end_date, lt.code FROM leave_applications la JOIN leave_types lt ON lt.id = la.leave_type_id WHERE la.status='approved' AND la.start_date<=%s AND la.end_date>=%semployee_id
start_date%Y-%m-%dend_datecode )rK   startendrO   zYSELECT DISTINCT employee_code FROM vw_attendance_combined WHERE [date]>=%s AND [date]<=%semployee_codez>SELECT id, employee_code, first_name, last_name FROM employeesid c              3   &   K   | ]	  }|s|  y wN ).0r3   s     r5   	<genexpr>z8XenoAttendanceReport.get_month_report.<locals>.<genexpr>z   s     $W1UVQ$Ws   
first_name	last_name)rT   namezresource.calendar.leaves)resource_idr9   F	date_from<=z%s 23:59:59date_to>=z%s 00:00:00dayszxeno.attendance.code.mapsource_code)restrict_codesrS   zxeno.attendance.dayoffdatezhr.business.trip)stater9   acknowledgedothers	trip_typexeno.attendance.auto.ruler]   r   rK   rS   employee_namelatezxeno.attendance.override)time_intime_outnoteeditor	edited_at)rowsrd   holidaysleavesyearmonthunmapped_codes	overridesodoo_leavessessionsdayoffsbusiness_trips).calendar
monthranger6   cursorexecutefetchallstrftimestrjoinsetcloser-   r.   r@   r_   rg   ra   r]   datetime	timedeltarA   sortedget_xeno_merge_native_attendancereasonrS   rk   trip_type_otherdict_fields	selectionmaxminget_auto_fill_codesr?   rp   rq   rr   	write_uidr   Datetime	to_string
write_date_xeno_odoo_leaves_xeno_calendar_sessions_xeno_recompute_late)*r2   rx   ry   rS   rf   rd   r_   ra   conncurru   rrw   	raw_codesemployee_directoryknown_codesrv   hdrR   mapped_sourcesrz   rE   allowedr~   dor   btlabelauto_fill_codes_ddayoff_codesbt_codesscopepresent_codesmissingrO   empr{   ovr|   r}   s*                                             r5   get_month_reportz%XenoAttendanceReport.get_month_reportZ   s   ""4/2"dE]2	"dE4%88zz| 	++d++CKKB"G,.<<>DKK& )0';= ||~'
 	  !/<11*=}--j9&	R	 'F ' KK24=w3GI ;>,,.IQQ/0IIIKKXY "
 	 Ao&'D'HH$W<!K.0Q$WW* " " 01KJJL 45::<CC'$ 78mi78E
  		0A
   "A)).."Cs(3466J/0X''Q// s(		0 HH/0557VBZ}-
  	K 7. HI%'56!s1v6G6#N!s155+A'Bg'MANDN# J!15512c-6HH  JD J 	**$t%7) 	+ 	H ((3499;BBdI&w(?@
 	XB IK		UWGR%%rww'7'7
'CDE	X ((-.335<<+4)ItY+GI
 
	0B +-,,(*BB&&+,44[AKKIMMPSQSQ]Q]M^ BLL(--eQ"?@Abjj(--eT"BCCs(MR[VX 0 0!**Z2HIJX''Q// s(
	0( ((#>?DDFZZ\)01gq"11%34'1bA445C5O.1Q#a&1/<M*+$ 	>BCQUU?34CC"\1H<MuG 	D$((.CKK"4y4!$Va 		 	((56;;=DDdI&w(?@
 		B ::-KK/42,,++#__66r}}EKIr'')9)9*)EFG		" ,,w?//-12QUU?#24 	!!$eXvy7N	< d $"0y*"n	F 	Fw' J" JJL: 7NJj 241CD 3sl   A\ <\(\ :\%\ 4:\.\ \/7!\4\4%/\9\>%]:]
#!]]\ \,c                 |    | r'd| v r#| j                  dd      d   j                         S | xs dj                         S )z'Personal Leave . PL' -> 'PL'. Leave types in this deployment are
        named "<Full Name> . <CODE>", so the abbreviation is the trailing
        dotted segment; anything without one falls back to the full name..rH   rP   )rsplitstrip)	type_names    r5   _xeno_leave_abbrz%XenoAttendanceReport._xeno_leave_abbr  sB    
 	)##C+B/5577R&&((    c           
         | j                   d   j                         }ddd|fdd|fg}|,|j                  dd|D cg c]  }t        |       c}f       n|r|j                  dd	t        |      f       i }|j	                  |      D ]  }	|	j
                  j                  }
|
s| j                  |	j                  j                        |	j                  xs d
|	j                  |	j                  d}|	j                  |	j                  }}||k  s|j                  d      }||cxk  r|k  rn n||t        |
      |f<   |t!        j"                  d      z  }||k  rN |S c c}w )a  (employee_code, 'YYYY-MM-DD') -> {abbr, unit, hf, ht} for every
        approved Odoo hr.leave overlapping the month. Unlike HRSystem's
        frozen leave_applications (full-day only), these carry the
        flexible-duration unit (part_time/first_half/second_half/full_day)
        and, for part-time, the requested hour range -- which the report
        needs to colour the day's pills and pick the right on-time
        threshold.zhr.leave)rh   r9   validaterequest_date_fromr`   request_date_torb   zemployee_id.xeno_employee_coder;   r9   full_dayabbrunithfhtrM   rH   rc   )r-   r.   r?   r   r@   rK   r>   r   holiday_status_idr]   xeno_request_unitrequest_hour_fromrequest_hour_tor   r   r   r   r   )r2   r_   ra   rf   rS   LeaverC   rE   r|   lvrO   infor   rR   dss                  r5   r   z&XenoAttendanceReport._xeno_odoo_leaves  sn    $))+& $0i0

 %MM;T,:;qCF;= >MM;S#mBTUV,,v& 	0B>>44D--b.B.B.G.GH,,:
**((	D ))2+=+=sAs(ZZ
+-g-37KTB0X''Q//	 s(	0  ) <s   E!c           
         | j                   d   j                         }|j                  dd|D cg c]  }t        |       c}fg      }i }|D ]  }|j                  }i }|j
                  D ]C  }	|j                  |	j                  g       j                  |	j                  |	j                  f       E |D ]  }
||
   j                           ||t        |j                        <    |S c c}w )a  employee_code -> {weekday '0'..'6': sorted [(hour_from, hour_to)]}
        from each employee's resource.calendar, so the report can tell where
        the morning session ends / the afternoon session starts (needed for
        first-half / second-half leave on-time thresholds).r=   r>   r;   )r-   r.   r@   r   resource_calendar_idattendance_ids
setdefault	dayofweekr?   	hour_fromhour_tosortr>   )r2   codesEmployeerE   rD   r}   r   calby_dowattdows              r5   r   z,XenoAttendanceReport._xeno_calendar_sessions<  s    
 88M*//1OO&:DSXBYa3q6BY%Z$[\	 	;C**CF)) Z!!#--4;;S]]CKK<XYZ #s  "#4:HS//01	;  CZs   C'c                     |j                  ||f      }|r|S |D ]+  }|d   |k(  s|d   |cxk  r	|d   k  sn !|d   ddddc S  y)zUnified leave lookup for one (employee, day): the richer Odoo
        record if present (carries unit + hours), else HRSystem's full-day
        leave code. Returns {abbr, unit, hf, ht} or None.rK   rQ   rR   rO   r   Nr   )r   )r2   emp_idrO   datestrrw   r|   r   r   s           r5   _xeno_leave_infoz%XenoAttendanceReport._xeno_leave_infoN  si     g/K 	XB- F*r'{g/RE/R "6
JdRVWW	X r   c           	      l   |
xs i }
|xs i }fd}|D ]  }|j                  d      }t        |j                  d      xs d      }|j                  d      xs ddd }|j                  d	      xs d
dd }|	j                  |i       }d}t        d|dz         D ]  }d|fz  } ||      s||v s||f|
v s||f|v r%|j                  ||f      }|r
|d   r|d   n"| j                  |j                  d|z              }|sj| j	                  |||||      }t        t        j                  |      j                               }| j                  |||j                  |      |      \  }}|| j                  |      | j                  |      }}||||kD  s|||z
  z  } ||d<    y)aZ  Set each row's `late` to the sum of per-working-day lateness:
        max(0, first-scan - expected start). Expected start is leave-aware
        (part-time end / afternoon start for first-half / etc). A full-day
        leave day is skipped (no scan expected); weekends, holidays,
        HR-granted days off, and acknowledged business trips too.c                 T    t        j                  |       j                         dk\  S N   r   rg   weekdayr   ry   rx   s    r5   
is_weekendz=XenoAttendanceReport._xeno_recompute_late.<locals>.is_weekende  $    ==ua088:a??r   rK   rS   rP   shift_start09:00Nr   	shift_end18:00r   rH   rI   rp   %s.Inro   )
r   r   range_fmtr   r   rg   r   _xeno_expected_window_to_minutes)r2   ru   rd   rx   ry   rv   rw   r{   r|   r}   r~   r   r   r   r   rO   sstartsendemp_sessionstotalr   r   r   tinleaver   	exp_start_exp_endtin_minexp_mins      ``                         r5   r   z)XenoAttendanceReport._xeno_recompute_lateZ  s    -R'-2	@  	AUU=)Fquu_-34DeeM*5gr:FEE+&1'2A6D#<<b1LE1dQh' /*dE1-==qMW%8 '?g5$N9Z]]D'?3)+9r)} IIaeeGg,=&>? --fdGV[Y(--eQ7??AB&*&@&@D,"2"23"7'@#	8$#'#3#3C#8$:J:J9:U&7+>7WCTWw..E%/& AfI5	r   c                    t        | j                  j                  j                  d      xsP | j                  j                  j
                  xs. | j                  j                  j                  j
                  xs d      S )NtzUTC)r   r-   contextr   r&   r   companyr   )r2   s    r5   _xeno_local_tzz#XenoAttendanceReport._xeno_local_tz  sg    HH  & A$((--*:*: Axx4477A;@B 	Br   c                    sdddS | j                         }t        j                  j                  |      j                         }| j	                  |j
                  |j                  g      }|j                  d      }t        fd|d   D        d      }|sdddS | j                  |j                  d|z              }| j                  |j                  d|z              }|r	|r||k(  rd}||dS )	a  Today's merged first-in/last-out for one employee, from either
        source (the biometric scanner via HRSystem, or Odoo's own
        hr.attendance -- see _xeno_merge_native_attendance, which already
        combines both into the same "%s.In"/"%s.Out" cells). Used by the
        "My Attendance" widget to detect a fingerprint check-in that Odoo's
        own hr_attendance module has no record of yet, so it can still show
        Present/Check Out instead of Absent/Check In.
        Nr;   outrS   rf   rM   c              3   p   K   | ]-  }t        |j                  d             t              k(  s*| / ywrS   Nr   r   rY   r   rS   s     r5   rZ   z8XenoAttendanceReport.get_today_status.<locals>.<genexpr>  +     Z1AEE/,B(Cs=GY(YQZ   +66ru   r   %s.Out)r  r   nowrg   r   rx   ry   r   nextr   r   )	r2   rS   r   todaydatar   rowin_vout_vs	    `       r5   get_today_statusz%XenoAttendanceReport.get_today_status  s     t,,  "!!%%b)..0$$JJ=)? % , ..,ZVZ t,,yy7!234		#''(W"456
 EdemE5))r   c                     sy| j                  ||g      }t        fd|d   D        d      }|sydt        fd|j                         D              S )a  Distinct calendar days this month with at least one recorded
        check-in for one employee, from the SAME merged source (biometric
        scanner via HRSystem's vw_attendance_combined + Odoo's own native
        hr.attendance, see _xeno_merge_native_attendance) as the My
        Attendance report -- NOT a raw hr.attendance count. An employee who
        only ever scans the physical biometric machine has zero
        hr.attendance rows in Odoo, so counting hr.attendance alone (the
        bug this method replaces, in xeno_mobile's own "This Month / Days
        Present" card) silently showed 0 for them even with a full month of
        real attendance on the desktop report.
        r   r  c              3   p   K   | ]-  }t        |j                  d             t              k(  s*| / ywr  r	  r
  s     r5   rZ   z?XenoAttendanceReport.get_month_present_count.<locals>.<genexpr>  r  r  ru   Nz.Inc              3   R   K   | ]  \  }}|j                        s|sd    yw)rH   N)endswith)rY   kvsuffixs      r5   rZ   z?XenoAttendanceReport.get_month_present_count.<locals>.<genexpr>  s"     IA

60Bq1Is   ''')r   r  sumitems)r2   rS   rx   ry   r  r  r  s    `    @r5   get_month_present_countz,XenoAttendanceReport.get_month_present_count  sk     $$%})? % , ZVZ IIIIr   c           	         | j                   d   j                         j                  |      }|j                         r|j                  st        t        d            |j                  }| j                  |j                        }|r>| j                  |      t        kD  r| j                  |       nt        t        d            | j                  |      }|d   r|d   rt        t        d            | j                         }t        j                  j                  |      j!                         }d |d   j#                  d      D        \  }}	|j%                  t        j                  j'                  |t        j(                  ||	                  }
|
j+                  t,              j/                  d	
      }t        j                  j1                         }||k  rt        t        d            | j                   d   j                         j3                  |j                  ||d      }dt4        j6                  j9                  |j:                        t4        j6                  j9                  |j<                        dS )a  Create the Odoo-native hr.attendance record that closes out a day
        that started with a fingerprint-only check-in (no hr.attendance row
        yet): check_in = the fingerprint's first scan time today, check_out
        = now. Everything is re-verified server-side (no open native
        attendance already exists; a fingerprint "in" with no "out" really
        is there today) rather than trusting the client's cached state.
        r=   z'No linked employee/HRSystem code found.zYAn attendance check-in already exists in Odoo -- use the normal Check Out button instead.r;   r  z-No open fingerprint check-in found for today.c              3   2   K   | ]  }t        |        y wrW   )r1   )rY   xs     r5   rZ   z=XenoAttendanceReport.close_fingerprint_day.<locals>.<genexpr>  s     @1A@s   :Ntzinfoz;Check-out time must be after the fingerprint check-in time.hr.attendance)rK   check_in	check_outchecked_out)attendance_statelast_check_inlast_check_out)r-   r.   browseexistsr>   r	   r   _xeno_open_attendancerT   _xeno_attendance_age_hoursSTALE_CHECKIN_HOURS_xeno_close_staler  r  r   r  rg   splitlocalizecombinetime
astimezoner   replaceutcnowcreater   r   r   r'  r(  )r2   rK   employeerO   open_nativestatusr   r  hourminutelocal_incheck_in_utccheck_out_utc
attendances                 r5   close_fingerprint_dayz*XenoAttendanceReport.close_fingerprint_day  s    88M*//188E (C(CAGHII**00=..{;>QQ &&{37!8 9 9 &&t,d|ve}AMNOO  "!!%%b)..0@t(:(:3(?@f;;x0088dTZ@[\]**3/77t7D ))002L(AMO P P XXo.335<<#;;$&>
 
 !.#__66z7J7JK$oo77
8L8LM
 	
r   c                 j    | j                   d   j                         j                  dd|fdgdd      S )Nr&  rK   r9   )r(  r9   FrH   zcheck_in desc)limitorder)r-   r.   r@   )r2   rK   s     r5   r/  z*XenoAttendanceReport._xeno_open_attendance  sA    xx(--/66S+.0IJ? 7 , 	,r   c                 z    t         j                   j                         | j                  z
  j                         dz  S )Ng      @)r   r9  r'  total_seconds)rC  s    r5   r0  z/XenoAttendanceReport._xeno_attendance_age_hours  s1    !!((*Z-@-@@OOQTZZZr   c           
         | j                         }t        j                  |j                        j	                  |      }|j                  t
        j
                  j                  |j                         t        j                  ddd                  }|j                  d|j	                  t              j                  d      i       y)zClose a forgotten open check-in at 23:59:59 on its own calendar
        day (local tz) -- a conservative marker that doesn't bleed hours
        into the following day.   ;   r(  Nr$  )r  r   r4  r'  r7  r   r5  rg   r6  writer8  )r2   rC  r   r@  local_end_of_days        r5   r2  z&XenoAttendanceReport._xeno_close_stale  s       "<<
 3 34??C;;%%hmmox}}RR7PQS+'7'B'B3'G'O'OW['O'\]^r   c                 h    | j                  |      }|sdddS d| j                  |      t        kD  dS )aZ  Whether the employee currently has an open (no check_out) native
        hr.attendance record, and whether it's stale (see STALE_CHECKIN_HOURS)
        -- Odoo's own attendance_state has no per-day boundary at all, so
        this lets the My Attendance widget tell a genuinely still-open shift
        from one that was simply never checked out.F)openstaleT)r/  r0  r1  r2   rK   r   s      r5   get_native_checkin_statusz.XenoAttendanceReport.get_native_checkin_status	  s>     ((5!E22t'F'Fs'KNa'abbr   c                     | j                  |      }|sddiS | j                  |      t        k  rt        t	        d            | j                  |       ddiS )zvAuto-close a forgotten open check-in (re-verified stale
        server-side) so the employee can check in fresh today.closedFzAThis check-in isn't stale yet -- use the normal Check Out button.T)r/  r0  r1  r	   r   r2  rR  s      r5   close_stale_checkinz(XenoAttendanceReport.close_stale_checkin  si     ((5e$$**3/3FFASU V Vs#$r   c           
         | j                   d   j                         }dg}	|+|	j                  dd|D 
cg c]  }
t        |
       c}
f       |r|	j                  ddt        |      f       |j	                  |	      }|sy|D ci c]  }|j
                  | }}| j                         }t        j                  ||d      t        j                  d      z
  }t        j                  |||      t        j                  d	      z   }| j                   d
   j                         j	                  dd|j                  fdd|fdd|fg      }i }|D ]  }|j                  }|j
                  }t        j                  |j                        j                  |      }|j                  d      }||f}|j!                  |g g d       ||   d   j                  |j#                                |j$                  st        j                  |j$                        j                  |      }||   d   j                  |j#                                 |syi }|D ],  }|j!                  t        |j'                  d            |       . |j)                         D ]  \  \  }}}|d   rt+        |d         nd}|d   rt-        |d         nd}|j'                  |      }|E|j'                  |      }|sZ|j.                  ||j0                  dd}|j                  |       |||<   d|z  d|z  }!} |j'                  |       s|r||| <   |j'                  |!      r|s|||!<    yc c}
w c c}w )a  Now that XENHR is frozen (employees can no longer reach it),
        check-ins happen two ways: the biometric scanners (still writing
        straight to HRSystem's vw_attendance_combined, untouched) and
        Odoo's own GPS/systray widget (hr.attendance, which HRSystem has
        no knowledge of at all). Without this, any native Odoo check-in
        silently never appears on Monthly/My/My Team Attendance -- mutates
        `rows` in place: fills a blank HRSystem cell from the native scan,
        or appends a whole new row for an employee HRSystem has nothing
        for this month (mirrors the auto-fill append pattern below).
        r=   )r>   z!=FNr>   r;   r9   rH   rc      r&  rK   r'  rb   <rM   r  r  rS   r   rm   r   r  )r-   r.   r?   r   r@   r>   r  r   r   idsrK   r   r4  r'  r7  r   r   r6  r(  r   r  r   r   rT   r]   )"r2   ru   rx   ry   rd   r   rf   rS   r   rC   rE   rD   eemp_by_coder   
range_fromrange_toattsby_dayr   r   rO   r@  r   key	local_outrows_by_coder   scansfirst_inlast_outr  in_keyout_keys"                                     r5   r   z2XenoAttendanceReport._xeno_merge_native_attendance"  s>    88M*//156%MM/7W1A7WXYMM/c-6HIJOOF+	8AB1q++Q.BB  " &&tUA69K9KQR9SS
$$T5$7(:L:LRS:TTxx(--/66D)--0z*h'8
   
	<C//C))D||CLL1<<R@H''
3G/Cc"R#893K$$X]]_5}}LL7BB2F	sE")))..*:;
	<  	DA##Co(>$?C	D '-lln 	("OT7U+0;s5;'DH,1%Ls5<(dH""4(C{!ood+&)fft(+!=C %(T"%/G1CGF776?x&F777#'G!	(Q 8X Cs   MMc                 N    t        j                  |d      S t        |            S )NrP   )_htmlescaper   )r2   ss     r5   _esczXenoAttendanceReport._escm  s!    ||!)B88Q88r   c                 L    t         j                  | t              \  }}d|d|S )Nzbackground:z;color:)LEAVE_TYPE_COLORSr   LEAVE_TYPE_COLOR_DEFAULT)rO   bginks      r5   _leave_chip_stylez&XenoAttendanceReport._leave_chip_styleq  s$    #''.FGC+-s33r   c                     g }| r|j                  d| z         |r|j                  d|z         |rddj                  |      z  S dS )a  Inline style attr for an attendance pill: an optional leave-colour
        background and/or an explicit font colour (red for late/early, the
        leave ink otherwise). Empty when neither applies, so plain scans
        keep the stylesheet's default look.zbackground:%szcolor:%sz style="%s";rP   )r?   r   )rq  colorpartss      r5   _pill_stylez XenoAttendanceReport._pill_stylev  sK     LL2-.LLe+,27}sxx.?R?r   c                     | yt        | t              r| dd S t        | t        j                  t        j                  f      r| j	                  d      S t        |       dd S )z.datetime.time / str / None -> 'HH:MM' or None.Nr   z%H:%M)
isinstancer   r   r6  r   )ts    r5   r   zXenoAttendanceReport._fmt  sY     9aRa5La(--):):;<::g&&1vbqzr   c                     | rt        |       dk  ry	 t        | dd       dz  t        | dd       z   S # t        $ r Y yw xY w)z+'HH:MM' -> minutes since midnight, or None.r   NrX  <      )lenr1   
ValueError)hhmms    r5   r   z XenoAttendanceReport._to_minutes  sO     s4y1}	tBQx=2%D1I66 		s   3 	??c                 r    t        |       }t        t        | |z
  dz              }|dk(  r|dz   d}}d||fz  S )zC9.0 -> '09:00', 14.5 -> '14:30' (part-time leave hours are floats).r}  rH   r   z	%02d:%02d)r1   round)
hour_floatr   ms      r5   _hour_float_to_hhmmz(XenoAttendanceReport._hour_float_to_hhmm  sH     
OzA~+,-7q5!qAaV##r   c                    |s||fS |j                  d      xs d}|dk(  ry|s0|dk(  r'|j                  d      | j                  |d         |fS ||fS |d   d   }t        |      dkD  r|d   d   n|d   d   }|dk(  r+|j                  d      | j                  |d         n|}||fS |dk(  r| j                  |      |fS |d	k(  r|| j                  |      fS ||fS )
a{  The (expected_start, expected_end) 'HH:MM' pair used to decide
        late-in / early-out for one day, adjusted for any leave:

        - no leave                -> (shift start, shift end)
        - full_day                -> (None, None)   (no scan expected)
        - part_time (off hf..ht)  -> due in at ht, out at shift end
        - first_half (morning off)-> due in at the afternoon session start
        - second_half (arvo off)  -> in at shift start, out at morning end

        day_sessions is the sorted [(hour_from, hour_to), ...] for that
        weekday; when it's missing we fall back to plain shift start/end.
        r   r   NN	part_timer   r   rH   
first_halfsecond_half)r   r  r  )	r2   r   r   day_sessionsr   r   morning_endafternoon_startess	            r5   r   z*XenoAttendanceReport._xeno_expected_window  s!    4<yy .J:{"uyy'B//d<dBB4<"1oa(03L0AA0E,q/!,<XY?[\K];YYt_0 **5;76< t8O<++O<dBB= 433K@@@t|r   c                      j                  ||      }|d   |d   }}|d   |d   }}|j                  d      xs g }|j                  d      xs i }|j                  d      xs i }|j                  d      xs i }|j                  d	      xs i }|j                  d
      xs i } j                  }d}fd}g g }}t        d|dz         D ]  }d|fz  } ||      ||v }}t	        j
                  |      j                  d      } |rdn|rdnd}!|rd |||         z  nd}"|j                  d|!|"|| fz         |j                  d|!d|!d         j                  d   j                         j                         }#g }$t        |      D ]  \  }%}&|%dz  dk(  rdnd}'|&j                  d      }(t        |&j                  d      xs d      })|&j                  d      xs d d d! }*|&j                  d"      xs d#d d! }+|j                  |)i       },g }-t        d|dz         D ]  }d|fz  } ||      ||v }}|)|f|v }.|)|f|v }/|/rd$n|.rd%n	|rdn|rdnd}! j                  |&j                  d&|z              }0 j                  |&j                  d'|z              }1|j                  |)|f      }2|2r
|2d(   r|2d(   n|0}3|2r
|2d)   r|2d)   n|1}4t        |2xr |2d(         }5t        |2xr |2d)         }6|s|s|.s|/s j                  |(|)|||      nd }7t        |7      xr |7j                  d*      d+k(  }8d x}9}:|7r t         j                  |7d,   t"              \  }9}:d-x};}<|s|s|.s|/s|8st        t	        j
                  |      j%                               }  j'                  |*|+|,j                  |       |7      \  }=}>|3r'|=r% j)                  |3       j)                  |=      kD  rd.};|4r'|>r% j)                  |4       j)                  |>      k  rd.}<|3 xr | xr | xr |. xr |/ xr	 |7 xr |)|#v }?dx}@}A|/r(|j                  |)|f      }Bd/|Brd0 |B      z   ndz  x}@}An'|.r%|j                  |)|f      rd1 |||)|f         z  x}@}A|5r=d2 ||2d3         d4 ||2d5         d6 ||0xs d7      |2d8   rd9 ||2d8         z   ndd:}@|6r=d2 ||2d3         d4 ||2d5         d6 ||1xs d7      |2d8   rd9 ||2d8         z   ndd:}A|rd; ||)      d<|d:nd}C|/rd=x}D}En.|.rd>x}D}En'|8r ||7d,         x}D}En|?r	d?|4xs d@}E}Dn|3xs d@}D|4xs d@}E|;r|n|8r|:nd }F|<r|n|8r|:nd }G|5rdAn|?rdBnd}H|6rdAnd}I|-j                  dC|!dDHd: j+                  |9F      @CdEDdF       |-j                  dG|!dD|Id: j+                  |9G      A|CdHEdF        |&j                  dI      xs d}J|JrdJnd}K|$j                  dK|'dL ||&j                  dM            dN ||&j                  d            dO|KdPJdFdj-                  |-      dQ        |s|$j                  dR|dz  z         dSg}Lt/               }Mt1        |dT U      D ]  }&t        |&j                  d      xs d      }N|NMv r&Mj3                  N       |rt        |      Nk(  rdVnd}OLj                  dW |N      d:|OdX ||&j                  dM            dY ||N      dZ	        dj-                  fd[t        dd\      D              }Pt        j
                  j5                         j6                  }Qdj-                  fd]t        |Q|Qd!z
  d^      D              }Rdj-                   fd_d`D              }Sdag}T j                  db   j                         j9                  g dcd      D ]^  }U|	r#t        |	      t        Uj:                        k(  rdVnd}OTj                  dWUj:                  d:|OdX ||Uj<                        de       ` dfg}V j                  dg   j                         j9                  g dcd      D ]^  }W|
r#t        |
      t        Wj:                        k(  rdVnd}OVj                  dWWj:                  d:|OdX ||Wj<                        de       ` dhg}X j                  di   j                         j9                  g dcd      D ]^  }Y|r#t        |      t        Yj:                        k(  rdVnd}OXj                  dWYj:                  d:|OdX ||Yj<                        de       ` |r&djt?        |       |dkj-                  |            fz  }Znd}Z|rt@        dl|iz  nd}[|rd}\d}]nMdmdj-                  L      z  }\dndj-                  T      dodj-                  V      dpdj-                  X      dq}]|rdr\  }^}_n|rds\  }^}_ndt\  }^}_|rdun|rdvndw}`dx^tB        dz
     fz  }ady_tB        dz
     fz  }"tD        i dztF        d{|`d||"d}|adl|d~\dPdRd]dSddj-                  |      ddj-                  |      ddj-                  |$      dt?        |      d|ddfz  |rd|z  ndz   |	rd|	z  ndz   |
rd|
z  ndz   |rd|z  ndz   dZd[iz  S )Nru   rd   rv   rw   rz   r{   r|   r}   r~   r   #ff2323c                 T    t        j                  |       j                         dk\  S r   r   r   s    r5   r   z4XenoAttendanceReport.render_html.<locals>.is_weekend  r   r   rH   rI   %aholwkndrP   z title="%s"z]<th colspan="2" class="day %s"%s><span class="dnum">%d</span><span class="dow">%s</span></th>z<th class="io z">In</th><th class="io io-out z
">Out</th>rl   rX  r   evenoddrK   rS   r   r   r   r   r   r   offr   r  rp   rq   r   r   r   FTz title="Business Trip%s"z: z title="Day off: %s"z title="Edited by rs   z on rt   u    — source: u   —rr   u    — "z
 data-ec="z" data-date="BTOffPresentz&ndash;editedautofillz<td class="io z"><span class="pill z data-field="in">z</span></td>z<td class="io io-out z data-field="out">ro   hasz<tr class="z%"><td class="c-emp"><span class="en">rn   z</span><span class="ec">z5</span></td><td class="c-late"><span class="late-val ">z</tr>z<tr><td class="c-emp">&mdash;</td><td class="c-late"></td><td colspan="%d" style="padding:2rem;color:var(--ink-400)">No attendance records for this month.</td></tr>z7<option value="">&mdash; All employees &mdash;</option>c                 >    t        | j                  d      xs d      S )Nrn   rP   r	  )r"  s    r5   <lambda>z2XenoAttendanceReport.render_html.<locals>.<lambda>_  s    Co0F0L",M r   )ra  	 selectedz<option value=">z (z
)</option>c              3   P   K   | ]  }d ||k(  rdndt         |dz
     fz    yw)z <option value="%d"%s>%s</option>r  rP   rH   N)MONTHS)rY   r  ry   s     r5   rZ   z3XenoAttendanceReport.render_html.<locals>.<genexpr>g  s;      #  /e+VAE]CD#s   #&   c              3   <   K   | ]  }d ||k(  rdnd|fz    yw)z <option value="%d"%s>%d</option>r  rP   NrX   )rY   yrx   s     r5   rZ   z3XenoAttendanceReport.render_html.<locals>.<genexpr>l  s2      :  /d+A67:s   r   c              3   N   K   | ]  }d j                  |      d|d  yw)z<span class="lv leg" style="r  z</span>N)rs  )rY   rE   r2   s     r5   rZ   z3XenoAttendanceReport.render_html.<locals>.<genexpr>q  s-      ? FJE[E[\]E^`ab ?s   "%r   z9<option value="">&mdash; All departments &mdash;</option>zhr.departmentr]   )rG  z	</option>z4<option value="">&mdash; All groups &mdash;</option>zhr.employee.categoryz7<option value="">&mdash; All positions &mdash;</option>zhr.jobaz  <div class="warn-banner"><strong>%d unrecognized employee code(s)</strong> seen in this period's raw scans that don't match any employee or known remap: <code>%s</code>. Their attendance is not shown above. Add a remap under Attendances &rarr; Configuration &rarr; Attendance Code Remaps if these are a known old/duplicate code, or check the device/employee record if not.</div>z, base_urlz<div class="field"><label for="employee">Employee</label><select class="control" id="employee" name="employee" onchange="this.form.submit()">%s</select></div>z<div class="field"><label for="department">Department</label><select class="control" id="department" name="department" onchange="this.form.submit()">z</select></div><div class="field"><label for="group">Group</label><select class="control" id="group" name="group" onchange="this.form.submit()">z</select></div><div class="field"><label for="position">Position</label><select class="control" id="position" name="position" onchange="this.form.submit()">z</select></div>)My Attendancer  )My Teamr  )Monthly Attendancer  u"   Xenoptics HR · My Team Attendanceu   Xenoptics HR · My Attendanceu   Xenoptics HR · Attendancez
%s - %s %dz%s &mdash; %s %dcsseyebrowtitletitle_plainextra_filters_pre
month_opts	year_optsextra_filters_postlegendhead1head2bodynrowsndays	export_qszyear=%d&month=%dz&employee=%sz&department=%sz	&group=%sz&position=%sbanneredit_script)$r   r   rm  r   r   rg   r   r?   r-   r.   r   	enumerater   r   boolr   ro  rp  r   r   r   rx  r   r   r   addr  rx   r@   rT   r]   r  _EDIT_SCRIPTr  _PAGE_CSS)br2   rx   ry   rS   rf   r  	team_modemy_modecan_editr8   rB   r<   r  ru   rd   rv   rw   rz   r{   r|   r}   r~   r   EREDr   h1h2r   r   r  r  r   klassr  r   r  ir   rcr   emp_coder   r   r   cellsr  r   raw_tinraw_toutr   r   tout	in_edited
out_editedr   is_full_leavelv_bglv_inkin_late	out_earlyr   exp_endauto_filledin_title	out_titlebt_label
edit_attrsin_textout_textin_color	out_colorin_clsout_clsro   late_clsemp_optsseenrO   selr  	this_yearr  leg	dept_optsdept
group_optscatjob_optsjobr  r  r  r  kind	kind_htmlr  r  sb   ```                                                                                               r5   render_htmlz XenoAttendanceReport.render_html  s{   
 $$T5-P&\4<d
+T(^&"239rHH[)/R	hh}-388J'-2((9%+"239rII	@ RBq$(# 		(A&$q)99G"1w(':#D--eQ/88>C Ev2E<?MAhw&7$88REII 9<A5!S;QR SII' (		( ((#>?DDFZZ\dO m	4DAqq5A:5BUU=)F15517R8HeeM*5gr:FEE+&1'2A6D#<<"5LE1dQh' ]9*dE1-==&qM7h+>c)W4(N: "#SEY]vce))AEE'G*;$<=99QUU8g+=%>?]]Hg#67(*r)}r)}'*,J:X !596	!""7J8
 !%SR ..vx&R]^MQ  $U O		&0AZ0O!%%$5$9$9f'?%AME6
 ',+)CB}hmmD%;CCEFC)-)C)Cl&6&6s&;U*D&IwyT-=-=c-BTEUEUV_E`-`"&D,<,<T,BTEUEUV]E^,^$(	 $'w  Mt8  MC  MG  MTVPV  M',9 M19_1L  (*)9-118W2EFH+E08(+b,B BHyW[[(G)<=+AAgxY`NaFbDc+ccHy"X,2k?);Qw?O%=P57Z1RZ=0RG IH "X,2k?);Qx?P5=Q57Z1RZ=0RG!II   @A{GT%'  )--Gh)..Gh")*5=)99Gh (1D4EIXG!.YG#0yH #*3-T#,C]6PT	%.;ZTV&0(bfd&6&6uh&GW67 gt'7'7y'I *h89u]9| 55=%AD $u"HKK q/0!AEE/4J2KT2775>	34Qm	4^ KK JMQTUXW X
 NNu"MN 	RAquu_-34Dt|HHTN!.3}3E3M+SUCOO wQquu_/E-F$Q R	R WW # 1b\# #
 MM'')..	GG : 9i!mR8: :	
 gg ?=? ? QQ	HH_-224;;Bf;M 	`D!.3}3ETWW3U+[]C477CQRSWS\S\Q]^_	` MM
882388:AA"FAS 	_C!,[1AS[1P+VXCCFFCQRSVS[S[Q\]^	_ NN88H%**,33Bf3E 	]C!'CK3svv;,F+BCOO#&&#qQTQYQY{[\	] ; ~&$))N*C(DEF  F?Glj(%;;R  "!#B !	!" #RWWZ%8"''(:K
"M >OD)2OD)HOD):C65<13 	 #dF519,=t%DD"i	1BD%II 
4
w
 U
 ;	

 
  !2
 *
 
 !"4
 c
 RWWR[
 RWWR[
 BGGDM
 SY
 T
  +tUm;>KN]:QSU@M,}<SUW :EK+5"N 8>NV32	G!
* f+
, ;-
 
 	
r   c                    LMNO dd l } j                  ||      }|d   |d   }}|d   |d   c}	M|j                  d      xs i }
|j                  d      xs i N|j                  d      xs i }|j                  d	      xs i }|j                  d
      xs i }fd}MN fd}t        j                         }|j                  |ddi      OOj                  d      }d}d}Oj                  dd|d      }Oj                  dddddd      }Oj                  dddddd      }Oj                  dddddd      }Oj                  ddd      }Oj                  dddd      }Oj                  ddd      }Oj                  dd|dd      }i LdTLOfd!	}|j                  dddd"d#t        dz
     fz  |       |j                  ddd$       |j                  d%dd&       d"}|j                  |dd'Oj                  d(di             d}t        j                         D ])  \  }\  } }!|j                  ||| || |!d             |dz  }+ |j                  ||d) |d |d             |dz   }"|j                  |"dd*|       |j                  |"dd+|       |j                  |"d%d,|       |j                  |"d        d"}#i }$t        d|dz         D ]  }%d-|%fz  }&|&|	v }' ||%      }(|'r|n|(r|n|})|j                  |"|#|"|#dz   d.|%t        j                   |%      j#                  d/      fz  |)       |j                  |"dz   |#d0|)       |j                  |"dz   |#dz   d1|)       |#|$|%<   |#d%z  }# |j%                  ddd       |j%                  ddd2       |j%                  d%d%d3       |j%                  d"|#dz
  d4       |j'                  |"d%z   d"       |"d%z   }*|D ]  }+|+j                  d5      },t)        |+j                  d6      xs d7      }-|+j                  d8      xs d9d d: }.|+j                  d;      xs d<d d: }/|j                  |-i       }0|j                  |*d|+j                  d6      |       |j                  |*d|+j                  d=      |       |+j                  d>      xs d}1|j                  |*d%|1|1r|n|       t        d|dz         D ]  }%d-|%fz  }& ||%      |&|	v }'}(|-|&f|v }2|-|&f|v }3|
j                  |-|&f      }4t+        |4xr |4d?         }5t+        |4xr |4d@         }6|5r|4d?   n" j-                  |+j                  dA|&z              }7|6r|4d@   n" j-                  |+j                  dB|&z              }8|(s|'s|2s|3s j/                  |,|-|&MN      nd }9t+        |9      xr |9j                  dC      dDk(  }:d x};}<|9r t        j                  |9dE   t0              \  };}<|3rdF}=n|2rdG}=n|9r|;}=n|'rd}=n|(rd}=nd }=d }>|3rdH}>n|2rdI}>n|9s	|'rdJ}>n|(rdK}>d x}?}@|(s|'s|2s|3s|:st)        t        j                   |%      j3                               }A j5                  |.|/|0j                  |A      |9      \  }B}C|7r'Br% j7                  |7       j7                  B      kD  rd}?|8r'Cr% j7                  |8       j7                  C      k  rd}@|?r|n|:r|<n|>}D@r|n|:r|<n|>}E ||=Dt+        |?xs
 |:xs |2xs |3            }F ||=Et+        @xs
 |:xs |2xs |3            }G|9r|9dE   nd }H|3rdLx}I}Jn:|2rdMx}I}Jn3|:rHx}I}Jn,|7xs d7}I|8xs d7}JHrIrIdNHdOndPHz  }IJrJdNHdOndPHz  }J|5r	IrdQIz  ndR}I|6r	JrdQJz  ndR}J|$|%   }K|j                  |*|KIF       |j                  |*|Kdz   JG        |*dz  }* |s|j                  |*ddS       Oj9                          |j;                  d       |j=                         S )UNr   ru   rd   rv   rw   r{   r|   r}   r~   r   c                 T    t        j                  |       j                         dk\  S r   r   r   s    r5   r   z4XenoAttendanceReport.export_xlsx.<locals>.is_weekend  r   r   c                 R    j                  | t        |      |      }|r|d   S d S )Nr   )r   r   )r   r  r   r   rw   r|   r2   s       r5   	leave_forz3XenoAttendanceReport.export_xlsx.<locals>.leave_for  s1    ((XQ\]D#'4<1T1r   	in_memoryT
Attendancez#142933r     )bold	font_size
font_colorz#e7edefrH   centervcenter)r  bg_colorborderalignvalignz#e3e9ebz#dff2e6)r  r  z#6d8791)r  r  r   r  r  )r  r  r   r  Fc                 |    | ||f}|vr.ddd}| r| |d<   |r||d<   |rd|d<   j                  |      |<   |   S )NrH   r  r  r  r   Tr  )
add_format)rq  r   r  ra  props
_fmt_cachewbs        r5   get_fmtz1XenoAttendanceReport.export_xlsx.<locals>.get_fmt  sc    z4(C*$#$x8(*E*%*4E,'$(E&M"$--"6
3c?"r   r~  zMonthly Attendance -- %s %dziMerged source: app/GPS punches + biometric machine (vw_attendance_combined -> sp_get_attendance_month_V3)rX  uV   * = HR-corrected value — source scan overridden, see Attendance Overrides for detailzLegend:r  zLate / EarlyzEmployee CodezEmployee Namez
Late (min)rI   z%d (%s)r  InOut   
      rK   rS   rP   r   r   r   r   r   rn   ro   rp   rq   r   r  r   r   r   z#cffafez#dbeafez#0e7490z#1d4ed8z#1f7a4dz#7d939br  r  z []z[%s]z%s **z%No attendance records for this month.)NNF)
xlsxwriterr   r   ioBytesIOWorkbookadd_worksheetr	  merge_ranger  rM  ro  r  set_rowr   r   rg   r   
set_columnfreeze_panesr   r  r   r   rp  r   r   r   r   seekread)Pr2   rx   ry   rS   rf   r  r  ru   rd   rv   r{   r}   r~   r   r   r  bufwsnavyr  f_titlef_headf_head_wknd
f_head_holf_empf_codef_late
f_late_hasr  
LEGEND_ROW
legend_colrO   rq  rr  
HEADER_ROWcolday_first_colr   r   r  r  fmtr   r  r   r  r   r   r   ro   r  r   r   r  r  r   r  r   r  r  r  base_bg	base_fontr  r  r   r   r  in_fontout_fontin_fmtout_fmtr   r  r  rE   r  rw   r|   r  sP   ```                                                                         @@@@r5   export_xlsxz XenoAttendanceReport.export_xlsx  s   $$T5-P&\4<d
+T(^&HH[)/R	hh}-388J'-2((9%+"239r	@	2 jjl  {D&9:l+--Bd ST)q*2i I JmmTyTU/79%N O]]DiST.6)$M N
i@A!yPYZ[!h?@]]a(36$F G
 
	# 	q!Q#@F5ST9DUW[C\#\^ef
A P 	Q
Aop
 

Q	2==&$+HI
0668 	OD)2sHHZT72sD3IJ!OJ	 	ZsD9QR!^

Q8
Q8
Qf5


:t$q$(# 
	A&$q)99GX%Ca=D #*+6CNN:sJa%HMM$q,I,R,RSW,X(YY[^`HHZ!^S$4HHZ!^S1WeS9"M!1HC
	 	aB
aB
aB
aq!$

Q*N _	CWW]+F377?39r:Hggm,7!<FGGK(3GRa8D#<<"5LHHQ377?3V<HHQ377?3U;776?'aDHHQ4t@1dQh' T6*dE1-==&qM7h+>c)W4(N:]]Hg#67 !596	!""7J8
(1r)}tyySZIZA[7\*4:$))CGGHW^L^D_:` $SR ..vx&R]^MQ  $U O		&0AZ0O "&%$5$9$9%-Ia$bME6'G'G#G'G'G"G 	 )I )I$-	$-	&++)CB}hmmD%;CCEFC)-)C)Cl&6&6s&;U*D&IwyT-=-=c-BTEUEUV_E`-`"&D,<,<T,BTEUEUV]E^,^$(	!(#vI"+3M&y
 !'48]=8]TW8][]3^_!'8T):a}:aX[:a_a5bc&+U6])--Gh)..Gh" *,+Gh!iRG#zrHAH#<fWYkCK2$>QWZ\Q\29fw.sG4<v0#H!!$Aw/AE8W5iT6j FA_	B HHQBC

xxzr   )NNNr  )	NNz/odoo/attendances/bymonthFFFNNN)#__name__
__module____qualname___name_descriptionr6   r   modelrF   r   staticmethodr   r   r   r   r   r  r  r  rD  r/  r0  r2  rS  rV  r   rm  rs  rx  r   r   r  r   r  r7  rX   r   r5   r   r   )   s   $EGL
2 	YYH H$ 	YYoF oFb ) ) DH(,%N$
 HL,0(TB
 	YY* *@J2 	YY0
 0
d,
 [ [_ 	YY	c 	c 	YY
  
  KO59H(V 	YY9 9 4 4 
@ 
@     $ $ F 	YYJNEJ-2BFN
 N
b 	YYI Ir   r   a  
@font-face{font-family:'Oswald Var';src:url(/xeno_attendance/static/fonts/oswald.woff2) format('woff2');font-weight:200 700;font-display:swap}
@font-face{font-family:'Plex Var';src:url(/xeno_attendance/static/fonts/plexsans.woff2) format('woff2');font-weight:100 700;font-display:swap}
:root{--navy-900:#142933;--ink-900:#142933;--ink-600:#3f5964;--ink-400:#6d8791;
--surface-0:#eef2f4;--surface-1:#fff;--surface-2:#f4f7f8;--surface-head:#e7edef;
--border:#d8e1e4;--border-strong:#b7c8cd;--accent:#ec1d23;--accent-ink:#c81119;--focus:#1d6fb3;
--wknd-bg:#e3e9eb;--wknd-ink:#7d939b;--hol-bg:#dff2e6;--hol-ink:#1f7a4d;
--off-bg:#dbeafe;--off-ink:#1d4ed8;--bt-bg:#cffafe;--bt-ink:#0e7490;
--late-bg:#fbe6c8;--late-ink:#9a5a10;--early-bg:#fbdad9;--early-ink:#b21f22}
*{box-sizing:border-box}html,body{margin:0;padding:0}
body{background:#f8f9fa;color:var(--ink-900);font-family:'Plex Var','IBM Plex Sans',system-ui,sans-serif;-webkit-font-smoothing:antialiased;font-size:14px}
.wrap{max-width:1360px;margin:0 auto;padding:1.5rem 1.25rem 3rem}
.head{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:1rem}
.head .eyebrow{font-weight:600;font-size:.7rem;letter-spacing:.13em;text-transform:uppercase;color:var(--accent-ink)}
.head h1{font-family:'Oswald Var','Oswald',sans-serif;font-weight:600;font-size:clamp(1.35rem,2.5vw,1.9rem);margin:.2rem 0 0;color:var(--ink-900)}
.head .src{font-size:.78rem;color:var(--ink-400);text-align:right;line-height:1.5}
.head .src code{font-size:.72rem;color:var(--ink-600)}
.toolbar{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap;background:var(--surface-1);border:1px solid var(--border);border-radius:9px 9px 0 0;border-bottom:none;padding:.7rem .85rem}
.field{display:flex;align-items:center;gap:.4rem}.field label{font-size:.78rem;color:var(--ink-400);font-weight:500}
.control{font:inherit;font-size:.83rem;color:var(--ink-900);background:var(--surface-2);border:1px solid var(--border-strong);border-radius:6px;padding:.32rem .5rem;cursor:pointer}
.control:focus-visible{outline:2px solid var(--focus);outline-offset:1px}
.btn{display:inline-flex;align-items:center;gap:.35rem;font-size:.82rem;font-weight:600;background:var(--navy-900);color:#fff;border:none;border-radius:6px;padding:.4rem .75rem;text-decoration:none;white-space:nowrap}
.btn:hover{opacity:.9}
.warn-banner{background:var(--late-bg);border:1px solid var(--late-ink);border-left-width:4px;border-radius:6px;padding:.65rem .9rem;margin-bottom:.85rem;font-size:.83rem;color:var(--late-ink)}
.warn-banner code{background:rgba(0,0,0,.06);padding:.05rem .3rem;border-radius:4px}
.spacer{flex:1 1 auto}.legend{display:flex;gap:.4rem;flex-wrap:wrap}
.lv{display:inline-flex;align-items:center;font-size:.68rem;font-weight:600;padding:.12rem .45rem;border-radius:100px;white-space:nowrap}
.lv.leg{font-size:.72rem}
.grid-scroll{overflow:auto;max-height:76vh;border:1px solid var(--border);border-radius:0 0 9px 9px;background:var(--surface-1)}
table{border-collapse:separate;border-spacing:0;font-variant-numeric:tabular-nums}
thead th{position:sticky;top:0;z-index:20;background:var(--surface-head);color:var(--ink-600);font-weight:600;font-size:.68rem;border-bottom:1px solid var(--border);border-right:1px solid var(--border);padding:.3rem .4rem;text-align:center;white-space:nowrap}
thead tr:nth-child(2) th{top:40px;font-size:.63rem;padding:.2rem .4rem}
th.day .dnum{font-family:'Oswald Var','Oswald',sans-serif;font-size:.92rem;font-weight:600;color:var(--ink-900);display:block;line-height:1.1}
th.day .dow{font-size:.58rem;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-400);display:block}
th.io{min-width:52px}th.io-out{border-right:1px solid var(--border)}
th.c-emp-h{left:0;z-index:40;text-align:left;min-width:190px;padding-left:.85rem}
th.c-late-h{left:190px;z-index:40;min-width:70px}
thead tr:first-child th.c-emp-h,thead tr:first-child th.c-late-h{z-index:41}
td{border-bottom:1px solid var(--border);border-right:1px solid var(--border);padding:.28rem .35rem;text-align:center;white-space:nowrap}
tbody tr.even td{background:var(--surface-1)}tbody tr.odd td{background:var(--surface-2)}
tbody tr:hover td{background:color-mix(in srgb,var(--focus) 9%,var(--surface-1))}
td.c-emp{position:sticky;left:0;z-index:10;text-align:left;min-width:190px;max-width:190px;padding:.35rem .85rem;border-right:1px solid var(--border-strong)}
td.c-emp .en{display:block;font-weight:600;font-size:.82rem;color:var(--ink-900);overflow:hidden;text-overflow:ellipsis}
td.c-emp .ec{display:block;font-size:.7rem;color:var(--ink-400)}
td.c-late{position:sticky;left:190px;z-index:10;min-width:70px;border-right:1px solid var(--border-strong)}
.late-val{font-weight:600;color:var(--ink-400);font-size:.82rem}.late-val.has{color:var(--late-ink)}
.pill{display:inline-block;font-size:.74rem;padding:.08rem .34rem;border-radius:5px;color:var(--ink-600);min-width:40px}
.pill.late,.pill.early{font-weight:600}
.pill.autofill{background:var(--hol-bg);color:var(--hol-ink);font-weight:600;font-size:.68rem}
.pill.edited::after{content:'';display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--focus);margin-left:.3rem;vertical-align:middle}
.pill.editable{cursor:pointer}.pill.editable:hover{outline:1px dashed var(--focus);outline-offset:1px}
.pill-input{width:62px;font:inherit;font-size:.78rem;border:1px solid var(--focus);border-radius:4px;padding:0 .2rem;background:var(--surface-1);color:var(--ink-900)}
th.wknd,td.wknd{background:var(--wknd-bg)!important;color:var(--wknd-ink)}
th.hol,td.hol{background:var(--hol-bg)!important}th.hol .dnum{color:var(--hol-ink)}
td.hol .pill{color:var(--hol-ink);opacity:.6}td.wknd .pill{color:var(--wknd-ink);opacity:.55}
td.off{background:var(--off-bg)!important}td.off .pill{color:var(--off-ink);font-weight:600}
td.bt{background:var(--bt-bg)!important}td.bt .pill{color:var(--bt-ink);font-weight:600}
td.io .lv{margin-right:.2rem;vertical-align:middle}
.foot{margin-top:.9rem;font-size:.78rem;color:var(--ink-400);display:flex;justify-content:space-between;flex-wrap:wrap;gap:.5rem}
ul  <!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>%(title_plain)s · Xenoptics HR</title><style>%(css)s</style></head><body>
<div class="wrap">
<div class="head">
<div><span class="eyebrow">%(eyebrow)s</span>
<h1>%(title)s</h1></div>
<div class="src">Merged source: app/GPS punches + biometric machine<br>
<code>vw_attendance_combined</code> &rarr; <code>sp_get_attendance_month_V3</code></div>
</div>
%(banner)s
<form method="get" action="%(base_url)s">
<div class="toolbar">
%(extra_filters_pre)s<div class="field"><label for="month">Month</label>
<select class="control" id="month" name="month" onchange="this.form.submit()">%(month_opts)s</select></div>
<div class="field"><label for="year">Year</label>
<select class="control" id="year" name="year" onchange="this.form.submit()">%(year_opts)s</select></div>
%(extra_filters_post)s<a class="btn" href="%(base_url)s/export?%(export_qs)s">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export Excel</a>
<div class="spacer"></div>
<div class="legend">%(legend)s</div>
</div>
</form>
<div class="grid-scroll"><table>
<thead>
<tr><th class="c-emp-h" rowspan="2">Employee</th><th class="c-late-h" rowspan="2">Late<br>(min)</th>%(head1)s</tr>
<tr>%(head2)s</tr>
</thead>
<tbody>%(body)s</tbody>
</table></div>
<div class="foot">
<span>%(nrows)s employees · %(ndays)s days · In = first scan, Out = last scan (MIN/MAX per day)</span>
<span>Orange = late in · Red = early out · Green = holiday · Grey = weekend · Blue dot = HR-edited · "Present" = auto-filled (no scan required)</span>
</div>
</div>%(edit_script)s</body></html>a  <script>
(function(){
  function rpc(url, params){
    return fetch(url, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      credentials: 'same-origin',
      body: JSON.stringify({jsonrpc: '2.0', method: 'call', params: params})
    }).then(function(r){ return r.json(); }).then(function(j){
      if (j.error) {
        throw new Error((j.error.data && j.error.data.message) || 'Save failed');
      }
      return j.result;
    });
  }
  document.querySelectorAll('.pill[data-ec]').forEach(function(pill){
    pill.classList.add('editable');
    pill.addEventListener('click', function(){
      if (pill.querySelector('input')) return;
      var current = pill.firstChild ? pill.firstChild.textContent.trim() : '';
      if (current === '\u2013') current = '';
      var input = document.createElement('input');
      input.type = 'time';
      input.value = current;
      input.className = 'pill-input';
      pill.textContent = '';
      pill.appendChild(input);
      input.focus();
      var settled = false;
      function save(){
        if (settled) return;
        settled = true;
        rpc('%(base_url)s/edit', {
          employee_code: pill.dataset.ec,
          date: pill.dataset.date,
          field: pill.dataset.field,
          value: input.value || ''
        }).then(function(){
          window.location.reload();
        }).catch(function(e){
          alert(e.message);
          window.location.reload();
        });
      }
      input.addEventListener('blur', save);
      input.addEventListener('keydown', function(ev){
        if (ev.key === 'Enter') { input.blur(); }
        if (ev.key === 'Escape') { settled = true; window.location.reload(); }
      });
    });
  });
})();
</script>)r   r   htmlrj  r  loggingpytzr   r   odoor   r   r   r   odoo.exceptionsr	   	getLoggerr8  _loggerr  ro  rp  r1  AbstractModelr   r  r  r  rX   r   r5   <module>rG     s       	   ' ' %
'

H
%
D !
 
 
 
   2   w6// wt#;z#	'J4r   