
    #{j2                        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mZ d dlmZ  ej                  e      ZdZh dZ G d d	e	j(                        Zy)
    N)date	timedelta)_apifieldsmodels)	UserErrorValidationError)Datetimez"hr_holidays.group_hr_holidays_user>
   nameemployee_idrequest_date_torequest_hour_toholiday_status_idrequest_date_fromrequest_hour_fromxeno_request_unitrequest_date_to_periodrequest_date_from_periodc                       e Zd ZdZ ej
                  dd      Z ej                  d      d        Z	 ej                  ddd	
      Z ej
                  ddd      Z ej                  ddddd      Z ej
                  ddd      Z ej                   ddgdd      Zej$                   fd       Z fdZd Z fdZd Zej0                  d        Zd Zd Zd  Zd! Zd" Zd# Z ej
                  d$d%      Z  ejB                  d&d%      Z" ej                   g d'd(d)d*+      Z# ejH                  d,d-.      Z% ejH                  d,d-.      Z& ejH                  d,d-.      Z' ejH                  d,d-.      Z( ejR                  d/d0      d1        Z* ejV                  d2      d3        Z, ej                   g d4d5dd67      Z-d8d9d:d:d;Z. ejR                  d<d$d2      d=        Z/ ejV                  d0      d>        Z0 ejb                  d2d?d@dAd/      dB        Z2 ejb                  d/dCdDdE      dF        Z3 fdGZ4ej0                  dH        Z5 ej
                  dIdJdKL      Z6dM Z7dN Z8dO Z9 ejR                  dPdQ      dR        Z:dS Z;dT Z< ej                  d       ejR                  dEd/dUdPdQ       fdV              Z= ej                  d       ejR                  dEd/dUdPdQ       fdW              Z> ej                  d       ejR                  dEd/dUdPdQ       fdX              Z? ej                  d       ejR                  dEd/dUdPdQ       fdY              Z@ ej                  d       ejR                  dEd/dAdZ       fd[              ZAds fd\	ZB fd]ZC fd^ZDd_ ZE fd`ZFda ZG fdbZH fdcZIg ddZJej0                  de        ZKej0                  df        ZLej0                  	 dtdg       ZM fdhZNdiZOdjZPdkdlgZQej0                  dm        ZRej0                  dn        ZSej0                  do        ZTdpZUej0                  dq        ZVej0                  dr        ZW xZXS )uHrLeavehr.leave_compute_xeno_can_edit_contentzWhether the current user may edit this leave's actual content (dates/type/reason/duration). True while the request is still being drafted (no id yet) or for Time Off Officers; false for the requester once it's saved --they can only Cancel it from then on.)computehelpuidc                     | j                   j                  j                  t              }| D ]"  }|xs |j                  j
                   |_        $ y N)envuser	has_groupHR_OFFICER_GROUP_originidxeno_can_edit_content)self
is_officerleaves      xeno_leave/models/hr_leave.pyr   z&HrLeave._compute_xeno_can_edit_content%   sH    XX]],,-=>
 	ME +5*LEMM<L<L8LE'	M    xeno.leave.approval.stepleave_idzApproval Chain)stringFzrGuards _cron_xeno_welcome_back_from_maternity from posting the new-baby message more than once for the same leave.)defaultcopyr   resource.calendar.leaveszDeducted For HolidayTa  Set only for leaves auto-created by a Company Holiday (xeno_holidays' automatic hook, not the manual wizard, which isn't tied to one specific holiday record). Lets deleting that holiday find and reverse exactly the leaves it caused, see resource.calendar.leaves.unlink().)r-   r/   indexr   a  True for any leave created by a system-driven deduction rather than an employee's own request -- currently only _xeno_deduct_for_company_holiday (manual wizard + xeno_holidays' automatic hook both set this), but the same flag is meant to be set by any future deduction source too (e.g. a planned Late Deduction feature, not yet built). Someone else chose the employee/type/days directly, so the Analysis checklist's frequency/pace checks don't apply.)company_holidayzCompany Holiday Deduction)late_deductionzLate Deductiona  Which kind of system deduction this is (blank for an employee's own request) -- lets the Leave List page's two separate 'Show ... deductions' checkboxes filter each kind independently. 'late_deduction' isn't produced by any feature yet; reserved for a planned Late Deduction feature.)r/   r   c                    |D ]  }| j                  |       | j                  |       | j                  |       | j                  |       |j	                  d      dk(  rd|d<   d|d<   f|j	                  d      dk(  s{d|d<   d|d<    t
        |   |      }|j                          |j                          |j                          |S )Nr   
first_halfamr   r   second_halfpm)
_xeno_check_leave_timing_xeno_check_weekend_dates_xeno_check_holiday_dates%_xeno_check_activity_restricted_datesgetsupercreate_xeno_check_prorated_balance_xeno_sync_allocation_used_days_xeno_build_approval_chain)r&   	vals_listvalsrecords	__class__s       r)   r?   zHrLeave.createW   s     	6D))$/**40**4066t< xx+,<37/015-.-.-?37/015-.	6 '.+ 	,,.//1**,r*   c                 F   t         |           | j                  j                  j	                  d      r| D ]  }|j
                  r|j                  s|j                  s*|j                  |j                  z
  j                  dz   }|j                  j                  j                  xs d}||_        ||z  |_         | j                  j                  j	                  d      }|=| D ]7  }|j                  j                  j                  xs d}||_        ||z  |_        9 yy)a  A company holiday is, by this company's own policy, not a paid
        non-working day -- HR deducts a real leave day for it instead (see
        wizard/leave_holiday_deduct.py). Odoo's own calendar-aware duration
        computation (core's _compute_duration/_get_durations) always
        returns 0 for a date that's a declared public holiday, since there
        are no working hours to overlap -- correct for a normal leave
        request, wrong for this specific deliberate-deduction flow. Only
        for records created via that wizard (xeno_force_full_day_duration
        in context) does this override the computed 0 with a real day
        count and the employee's own average daily hours; every other
        leave keeps core's genuine calendar-based computation untouched.
        xeno_force_full_day_duration   g       @xeno_force_number_of_daysN)r>   _compute_durationr   contextr=   number_of_daysr   r   daysr   resource_calendar_idhours_per_daynumber_of_hours)r&   r(   rN   rP   forced_daysrF   s        r)   rK   zHrLeave._compute_durationo   s    	!#88 >? A++0G0GELaLa!11E4K4KKQQTUUD$)$5$5$J$J$X$X$_\_M+/E(,0=,@E)A hh&&**+FG" D % 1 1 F F T T [X['2$(3m(C%D #r*   c                     | j                   j                  j                  d      ry| j                   d   j                         }| j                   d   j                         }| D ]  }|j                  dk7  s|j
                  s|j                  |j
                        }|j                  |D cg c]  }|j                  |d   |d   |d   d	 c}       |j                         }|s|j                  |        yc c}w )
a   Materialize this leave's approval chain from the configured
        department/employee approver steps (xeno.leave.approver.build_chain),
        snapshotting it onto xeno.leave.approval.step -- mirrors XENHR
        creating LeaveApprovalStep rows on submission. Only for leaves that
        actually need multi-step approval (state == 'confirm'); a leave
        created directly in another state (e.g. by a data import) gets no
        chain and falls back to legacy behavior everywhere it's checked.
        xeno_hr_direct_createNzxeno.leave.approverr+   confirm
step_orderlabelapprover_user_id)r,   rV   rW   rX   )r   rL   r=   sudostater   build_chainr?   r$   _xeno_current_pending_step_xeno_send_step_email)r&   ApproverStepr(   chainstep
first_steps          r)   rB   z"HrLeave._xeno_build_approval_chain   s     88 78 8812779xx2388: 	8E{{i'u/@/@(():):;EKK
  !
 	 "HH"<0g$();$<	 ! " 99;J++J7	8!s   ,!C;
c                 >   | j                   j                  sb| j                   j                  j                  t              s9t        |      t        z  }|r%| j                  d      rt        t        d            t        | -  |      }d|v r| j                          |S )Nlocked_fieldszsA submitted leave request can no longer be edited directly. Cancel it and submit a new one, or ask HR to update it.rZ   )r   sur    r!   r"   set_XENO_LOCKED_LEAVE_FIELDS_xeno_validation_enabledr	   r   r>   writerA   )r&   rD   locked_touchedresrF   s       r)   ri   zHrLeave.write   s    xx{{488==#:#:;K#L Y)BBN$"?"?"P$!% & & gmD!d?002
r*   c           	         | j                   d   j                         }| j                   d   j                         }| D ch c]F  }|j                  r8|j                  r,|j                  j                  |j                  j                  fH }}|D ]a  \  }}t        |j                  dd|fdd|fdg      j                  d            }|j                  dd|fdd|fg      j                  d|i       c y	c c}w )
ag  Keep xeno.leave.allocation's "Used" column (see the Leave Balance
        Grid pivot view) in sync with actually-validated leave. Mirrors
        XENHR's leave_balances.used_days -- a value HR can also manually
        correct on the allocation, this just keeps it accurate by default
        rather than requiring HR to update it by hand every time.
        hr.leave.allocationr   r   =r   rZ   rn   validaterM   xeno_used_daysN)	r   rY   r   r   r$   sumsearchmappedri   )r&   AllocLeavelpairsemp_idtype_idtotals           r)   rA   z'HrLeave._xeno_sync_allocation_used_days   s    ./446$))+EI ;MMa&9&9 --""A$7$7$:$:; ; ;$ 		0OFGV,$c73*&  v&'	)E
 LLV,$c73  u&./		0;s   AC3c                     | j                   d   j                         j                  dd|fdgd      }| xs |j                  S )a0  Whether an admin-toggleable leave validation (see the "Leave
        Validations" page / xeno.leave.validation.rule) is currently on.
        Fail safe: a missing row, or a rule that isn't a 'toggle' kind, is
        always enforced -- only an explicitly-disabled toggle rule is
        skipped.
        zxeno.leave.validation.rulecodern   )kindrn   togglerI   limit)r   rY   rs   enabled)r&   r}   rules      r)   rh   z HrLeave._xeno_validation_enabled   sQ     xx45::<CCc4 "9:! D E)T\\)r*   c                    | j                  d      sy| j                  j                  j                  d      ry| j                  j                  j                  t              rC|j                  d      }| j                  j                  j                  j                  }|r||k7  ry|j                  d      }|j                  d      }|r|sy| j                  d   j                         j                  |      }t        j                  j                  |      }t        j                  j                  |       }|j                  xs d}	|j                   xs d}
|	dkD  r7|t#        |		      z   }||k  r"t%        t'        d
|j(                  |	|            y|
dkD  r7|t#        |
	      z
  }||k  r"t%        t'        d|j(                  |
|            y||k  r t%        t'        d|j(                              y)a  Enforce per-leave-type advance-notice / max-backdate limits.

        Mirrors XENHR's LeaveApplicationController@store validation exactly
        (same priority: advance_days, else max_backdate_days, else no past
        dates at all) -- but only for self-service creation. XENHR's
        storeForEmployee (HR/admin creating *for someone else*) skips this
        check entirely, so here it's skipped only when a Time Off Officer
        is creating a request for a *different* employee than their own --
        NOT simply whenever the acting user happens to hold that role.
        An HR officer requesting their own leave through the normal
        self-service flow must still be checked like anyone else (bug
        found in testing: susu, an HR/admin account, could book Annual
        Leave for tomorrow because the old check bypassed on role alone).

        The one unconditional bypass is xeno_hr_direct_create: that context
        is set only by HR-initiated on-behalf creation (the Create Leave
        (HR) wizard and the holiday-deduction feature), which XENHR's
        storeForEmployee explicitly exempts from the timing limits -- these
        are deliberately allowed to backdate, regardless of whose employee
        record it is (the wizard's target can even coincide with the acting
        HR user's own employee, which is exactly the case that slipped
        through the role/target heuristic below).
        timingNrT   r   r   r   hr.leave.typer   rN   zg%(type)s requires at least %(days)s day(s) advance notice. Earliest allowed start date is %(earliest)s.)typerN   earliestz^%(type)s allows backdating up to %(days)s day(s). Earliest allowed start date is %(earliest)s.z7Start date must be today or in the future for %(type)s.)r   )rh   r   rL   r=   r    r!   r"   r   r$   rY   browser   Dateto_datecontext_todayxeno_advance_daysxeno_max_backdate_daysr   r
   r   r   )r&   rD   target_employee_idown_employee_idleave_type_idr   
leave_type
start_datetodayadvance_daysmax_backdate_daysr   s               r)   r9   z HrLeave._xeno_check_leave_timing   s   0 ,,X688 7888==""#34!%-!8"hhmm77::O!&8O&K!45 HH%89$5XXo.335<<]K
[[(():;
))$/!338q&==B!yl;;HH$%aK#|h'P Q Q %
 "y.?@@HH$%aC#/@8'U V V % E!%aM#'* + + "r*   c                    | j                  d      sy| j                  j                  j                  d      s|j                  d      ry| j                  j                  j                  t              rC|j                  d      }| j                  j                  j                  j                  }|r||k7  ry|j                  d      }|sy|j                  d      xs |}t        j                  j                  |      }t        j                  j                  |      }t        d      t        d	      d
}g }	|j                         |v r(|	j                  |d||j                            d       ||k7  r:|j                         |v r(|	j                  |d||j                            d       |	r$t        t        ddj!                  |	                  y)a  The company is closed Saturday/Sunday -- picking a weekend as the
        request's start or end date is almost always an accidental
        mis-click on the date picker, not an intended non-working day (a
        multi-day request that merely *spans* a weekend in the middle,
        e.g. Friday to Monday, is untouched -- only the two edges the
        employee actually picked are checked). Blocks with a clear alert
        naming the offending date(s) rather than silently letting core's
        calendar-aware duration computation count it as 0 days.

        Same bypasses as _xeno_check_leave_timing: HR on-behalf creation
        (xeno_hr_direct_create), an Officer creating for a different
        employee, and any system-driven deduction (xeno_is_system_deduction
        -- e.g. the company-holiday deduction, which deliberately targets
        specific calendar dates HR chose, not something the employee picked).
        weekend_datesNrT   xeno_is_system_deductionr   r   r   SaturdaySunday)       ()zzThe company is closed on weekends. The date you selected -- %s -- falls on a weekend. Please check the date and try again., )rh   r   rL   r=   r    r!   r"   r   r$   r   r   r   r   weekdayappendr
   join)
r&   rD   r   r   startendr   end_dateweekday_namesflaggeds
             r)   r:   z!HrLeave._xeno_check_weekend_dates!  s     ,,_=88 78DHHE_<`88==""#34!%-!8"hhmm77::O!&8O&K,-hh()2U[[((/
;;&&s+jMak:=0NN
M*BTBTBV4WXYz!h&6&6&8M&INN-@P@P@R2STU!!"ii0#2 3 3 r*   c           
         | j                  d      sy| j                  j                  j                  d      s|j                  d      ry| j                  j                  j                  t              rC|j                  d      }| j                  j                  j                  j                  }|r||k7  ry|j                  d      }|sy|j                  d      xs |}t        j                  j                  |      }t        j                  j                  |      }|h}||k7  r|j                  |       | j                  d   j                         j                  d	d
ddt        |      z  fdddt!        |      z  fg      }	g }
t#        |      D ]=  }|	j%                  |fd      dd }|s|
j'                  |d|j(                  d       ? |
r$t+        t-        ddj/                  |
                  y)a  A declared public/company holiday is already a non-working,
        unpaid-leave-exempt day by this company's policy (see
        _compute_duration's own docstring) -- HR deducts a real leave day
        for it separately via the Holiday Deduction wizard when that's
        actually intended, so a self-service request picking a holiday as
        its own start/end date is (like a weekend pick) almost always an
        accidental date-picker mis-click, not something the employee meant.
        Same edges-only semantics as _xeno_check_weekend_dates: a multi-day
        request that merely spans a holiday in the middle is unaffected,
        only the two dates the employee actually picked are checked.

        Same bypasses as the other submission-time checks: HR on-behalf
        creation (xeno_hr_direct_create -- this is exactly the path the
        Holiday Deduction wizard itself uses), an Officer creating for a
        different employee, and any system-driven deduction
        (xeno_is_system_deduction).
        holiday_datesNrT   r   r   r   r   r0   )resource_idrn   F	date_from<=z%s 23:59:59date_to>=z%s 00:00:00c                     | j                   j                         |cxk  xr | j                  j                         k  S c S r   )r   r   r   )hds     r)   <lambda>z3HrLeave._xeno_check_holiday_dates.<locals>.<lambda>z  s-    q{{//1QJ!))..:JJ J r*   rI   r   r   zThe date you selected -- %s -- is a declared company holiday. Please check the date and try again; if you genuinely need this recorded as a leave day, ask HR.r   )rh   r   rL   r=   r    r!   r"   r   r$   r   r   r   addrY   rs   maxminsortedfilteredr   r   r
   r   r   )r&   rD   r   r   r   r   r   r   check_datesholidaysr   r   holidays                r)   r;   z!HrLeave._xeno_check_holiday_datesM  s   $ ,,_=88 78DHHE_<`88==""#34!%-!8"hhmm77::O!&8O&K,-hh()2U[[((/
;;&&s+!lz!OOH%8867<<>EE'$K0@ @Amc+.>>?G
 
 $ 	>A''JLLNQPGAw||<=		>
 !!G 		'"	#$ % % r*   c                    | j                  d      sy| j                  j                  j                  d      s|j                  d      ry| j                  j                  j                  t              rC|j                  d      }| j                  j                  j                  j                  }|r||k7  ry|j                  d      }|sy|j                  d      xs |}t        j                  j                  |      }t        j                  j                  |      }| j                  d   j                         j                  d	d
d|fdd|fg      }|r-dj                  d |D              }	t        t!        d|	            y)a  Blocks a self-service leave request whose date range overlaps a
        company activity flagged Restrict Leave on These Dates (e.g. a
        mandatory Guest Visit day -- see xeno.hr.activity in
        xeno_theme_slate, whose xeno_leave already depends on).

        This is deliberately NOT an emergency override: there is no
        in-system way for the employee to bypass this themselves. A
        genuine emergency has to go through HR directly, who can still
        create the leave on the employee's behalf via the exact same
        on-behalf paths this check already exempts (xeno_hr_direct_create /
        an Officer creating for a different employee) -- the system does
        not auto-approve emergency overrides, matching the requested
        design exactly.
        activity_restricted_datesNrT   r   r   r   r   zxeno.hr.activity)is_leave_restrictedrn   Tr   r   r   r   r   c              3   n   K   | ]-  }|j                   d |j                  d|j                  d / yw)r   z to r   N)r   r   r   ).0as     r)   	<genexpr>z@HrLeave._xeno_check_activity_restricted_dates.<locals>.<genexpr>  s*      XGH1661;;		BXs   35zLeave cannot be requested for this period -- it overlaps a restricted company activity: %s. If this is a genuine emergency, contact HR directly; this cannot be self-submitted or auto-approved.)rh   r   rL   r=   r    r!   r"   r   r$   r   r   r   rY   rs   r   r
   r   )
r&   rD   r   r   r   r   r   r   
activitiesnamess
             r)   r<   z-HrLeave._xeno_check_activity_restricted_dates  sb    ,,-HI88 78DHHE_<`88==""#34!%-!8"hhmm77::O!&8O&K,-hh()2U[[((/
;;&&s+XX01668??.$)j)A
 

 II XLVX XE!!3 5:	#; < < r*   c                 ^   | j                  d      sy| j                  j                  j                  d      s| j                  j                  ry| j                  j
                  j                  t              }| j                  j
                  j                  j                  }| j                  d   j                         }| j                  d   j                         }| D ]L  }|j                  }|j                  }|r|r|j                  s-|j                  dv r<|j                  rI|r|j                  |k7  r[|j                  xs d}|dk  rq|j                   xs( |j"                  xr |j"                  j%                         }	|	s|	j&                  }
|	j(                  }t%        |
d	d	      }t%        |
|t+        j,                  |
|      d	         }t/        |j1                  d
d|j                  fdd|j                  fdg      j3                  d            }|dz  }||z  }t/        |j1                  dd|j                  fd
d|j                  fdd|j                  fddd|fdd|fg      j3                  d            }||z
  }||dz   kD  s|	j5                  d      }|j7                  ||      }|rt9        d|      }nt9        d      }t;        t9        d|j<                  dt?        tA        |d      d      z  |dt?        |d      z  dt?        |d      z  dt?        |d      z  |             y)a  Block a self-service request that exceeds the pro-rated monthly
        entitlement of a pro-rated leave type (hr.leave.type.xeno_prorated).

        A pro-rated type's yearly allocation is treated as accruing evenly:
        per_month = yearly_total / 12, so by the request's month M the
        employee has accrued per_month * M days. The request is rejected if
        it would push the total already used/booked this year (up to and
        including month M) past that accrued amount. When blocked, the error
        also lists alternative leave types the employee actually has balance
        in, so it guides rather than dead-ends (see
        _xeno_prorated_alternatives).

        Bypassed for the same on-behalf/system paths as the timing check
        (xeno_hr_direct_create, an Officer creating for someone else, sudo,
        system deductions) and skipped entirely if the 'prorated_balance'
        rule is switched off on the Leave Validations page.
        prorated_balanceNrT   r   rm   cancelrefuse        r   rI   r   rn   r   ro   rM   g      (@r$   !=rZ   inrU   	validate1rp   r   r   r   {Gz?%B %Yz You could use instead: %s.zJ You have no other leave type with remaining balance -- please contact HR.u   %(type)s is pro-rated: only %(avail)s day(s) are accrued by %(month)s (%(accrued)s accrued − %(used)s already used/booked this year), but you requested %(req)s.%(alt)s%g   )r   availmonthaccruedusedreqalt)!rh   r   rL   r=   re   r    r!   r"   r   r$   rY   r   xeno_proratedrZ   r   rM   r   r   r   yearr   calendar
monthrangerr   rs   rt   strftime_xeno_prorated_alternativesr   r
   r   roundr   )r&   r'   r   rv   ru   r(   ltemp	requestedr   r   	month_num
year_start	month_endyearly_total	per_monthr   consumed	availablemonth_labelalternativesalt_msgs                         r)   r@   z$HrLeave._xeno_check_prorated_balance  s   $ ,,-?@88 78DHHKKXX]],,-=>
((--3366$))+./446 A	E((B##CS(8(8{{22-- cff7,,3IA~++[0ZEOODXDXDZE::DIdAq)JT9h.A.A$	.RST.UVIu||SVV,$c2551*-   v&'	)L
 %t+I)+G
 5<<tUXX&SVV,$c2551E$dJ7$dI6)  v&')H  (*I9t++#nnW5$@@YO =|LG !@ AG%aP %Is(;Q"??%!E'1$55x!33i 33'  mA	r*   c                 >   | j                          | j                  }| j                  xs | j                  j	                         j
                  }| j                  d   j                         }| j                  d   j                         }| j                  d   j                         j                  dd|j                  fg      }g }|D ]  }	|	j                  s)|j                  ddt        d|	j                        f       9t        |j                  d	d
|j                  fdd
|	j                  fdg      j                  d            }
|
dk  rt        |j                  d	d
|j                  fdd
|	j                  fdg      j                  d            }|
|z
  }|dk  r||k\  rdnd}|j                  || t        d|	j                  dt!        |d      z        f        |j#                  d        |dd D cg c]  }|d   	 }}dj%                  |      S c c}w )u   A human-readable, comma-separated list of other leave types the
        employee can actually fall back on -- allocation types with real
        remaining balance (yearly allocated − used/booked this year, NOT
        pro-rated: the fallback is about what's genuinely available), plus
        no-allocation types (e.g. Unpaid) shown as unlimited. Ordered so
        types that fully cover the request come first, then unlimited ones,
        then partial balances; capped to keep the message readable.
        r   rm   r   r$   r   rI   r   z%s (no limit)r   rn   r   ro   rM   r   r   r   r   z%(name)s (%(rem)s left)r   )r   remc                     | d   | d   fS )Nr   rI    )os    r)   r   z5HrLeave._xeno_prorated_alternatives.<locals>.<lambda>3  s    AaD!A$< r*   )keyN   r   )
ensure_oner   r   r   r   r   r   rY   rs   r$   requires_allocationr   r   r   rr   rt   r   sortr   )r&   exclude_typer   r   r   rv   ru   typesoptionsr   r{   r   	remainingrankr   labelss                   r)   r   z#HrLeave._xeno_prorated_alternatives  s    	&&?$..*=*=*?EE$))+./446)..077$loo9V8WX 	]B))3/277(CDESVV,$c2551*&  v&'	)E
 z5<<SVV,$c2551E)  v&'	)H
 (ID !Y.1ADNND9*a)dU9VWEX>X/[ \ ])	], 	/0 ',1!A$,,yy   -s   ;Hz(holiday_status_id.xeno_flexible_duration)relatedreadonlyzholiday_status_id.xeno_detail))full_dayzFull-day Leave)	part_timezPart-time Leave)r5   zFirst-half Day Leave)r7   zSecond-half Day LeavezLeave Type Basisr  zOnly used (and editable) when the selected Time Off type has Flexible Duration enabled -- lets the requester pick the leave basis per request, mirroring XENHR's leave_base_types (Full-day/Part-time/First-half/Second-half).)r-   r.   r   _compute_xeno_balance)   r   )r   digitsr   r   c                 F   | j                   d   j                         }| j                   d   j                         }| D ]a  }|j                  }|j                  }|j                  j
                  xs |j
                  }|j                  j
                  xs |j
                  }|r"|r t        |t              rt        |t              sd|_        d|_	        d|_
        d|_        t        |j                  dd|fdd|fdg      j                  d            }t        |j                  dd|fdd|fdg      j                  d            }	t        |j                  dd|fdd|fd	d
ddgfg      j                  d            }
||_        |	|_	        |
|_
        ||	z
  |
z
  |_        d y )Nr   rm   r   r   rn   r   ro   rM   rZ   r   rU   r   )r   rY   r   r   r#   r$   
isinstanceintxeno_balance_totalxeno_balance_usedxeno_balance_pendingxeno_balance_remainingrr   rs   rt   )r&   rv   ru   r(   r   r   ry   lt_idr{   r   pendings              r)   r  zHrLeave._compute_xeno_balanceW  s   $))+./446 	BE##C((B[[^^-svvFJJMM*RUUEj.EZX]_bMc+.(*-'-0*/2,V,$c51*&  v&'	)E
 u||V,$c51*%  v&'	)D
 %,,V,$c51$K 89(  v&'	)G
 (-E$&*E#)0E&+04<'+AE(;	Br*   r   c                 |    | j                   dk(  rd| _        d| _        y | j                   dk(  rd| _        d| _        y y )Nr5   r6   r7   r8   )r   r   r   r&   s    r)   #_onchange_xeno_request_unit_periodsz+HrLeave._onchange_xeno_request_unit_periodsz  sC    !!\1,0D)*.D'##}4,0D)*.D' 5r*   ))dayDay)half_dayzHalf-Day)hourHours%_compute_xeno_leave_type_request_unitN)	selectionr   r   r   r  r  r  )r  r  r5   r7   zholiday_status_id.request_unitc                     | D ]k  }|j                   j                  r8|j                  r,| j                  j	                  |j                  d      |_        Q|j                   j                  |_        m y )Nr  )r   xeno_flexible_durationr   _XENO_UNIT_MAPr=   leave_type_request_unitrequest_unit)r&   r(   s     r)   r  z-HrLeave._compute_xeno_leave_type_request_unit  sf      	UE&&==%BYBY040C0C0G0G++U14- 160G0G0T0T-	Ur*   c                 Z    | j                   j                  r| j                  sd| _        y y y )Nr  )r   r  r   r  s    r)    _onchange_xeno_holiday_status_idz(HrLeave._onchange_xeno_holiday_status_id  s+    !!88AWAW%/D" BX8r*   r   r   r   c                 j   | j                  d      sy| D ]  }|j                  dk7  s|j                  s |j                  |j                  k  rt        t        d            |j                  j                  xs  |j                  j                  j                  }|r|j                  st        |j                  j                               |j                  j                  fd      }|st        |j                  d            }t!        |j                  d            }|j                  |dz
  k  s|j                  |dz   kD  r~t        t        d	| j#                  |j                        | j#                  |j                        |j                  j$                  | j#                  |      | j#                  |      
            |j&                  dk  st        t        d| j#                  |j                        | j#                  |j                        |j                  j$                               y)ac  Part-time leave lets HR/the requester pick any clock-time range
        via a plain time widget, with nothing stopping them from picking a
        range that runs outside the employee's actual scheduled hours for
        that day (e.g. 15:00-19:00 when the day ends at 18:00). Catch that
        at save time and say exactly what's wrong.

        The range only needs to sit within the day's working *span*
        (earliest attendance start .. latest attendance end); a pick that
        merely crosses an unpaid lunch break in the middle is fine. The old
        implementation compared the raw clock span to Odoo's break-excluding
        number_of_hours, which falsely rejected any range spanning lunch --
        e.g. 09:00-14:00 on a 09:00-12:30 / 13:30-18:00 calendar reads as
        4 working hours over a 5-hour span, yet is entirely within hours.
        part_time_hoursNr  z"End time must be after start time.c                 "    | j                   k(  S r   )	dayofweek)r   dows    r)   r   z5HrLeave._check_xeno_part_time_hours.<locals>.<lambda>  s    Q[[TWEW r*   	hour_fromhour_tor   z%(start)s-%(end)s is outside %(employee)s's scheduled working hours that day (%(ws)s-%(we)s). Pick a range within their working hours.)r   r   employeewswez%(start)s-%(end)s falls entirely within a non-working period (e.g. the lunch break) for %(employee)s. Pick a range that includes working time.)r   r   r'  )rh   r   r   r   r   r
   r   rO   
company_idr   strr   attendance_idsr   r   rt   r   _xeno_format_hourr   rQ   )r&   r(   r   attendances	day_startday_endr$  s         @r)   _check_xeno_part_time_hoursz#HrLeave._check_xeno_part_time_hours  s   " ,,->? '	E&&+5U=N=N$$(?(??%a(L&MNN))>> M ,,77LL 5#:#:e--5578C #11::;WXKK..{;<I+,,Y78G'')d*::,,w~=%a2 001H1HI..u/D/DE"..33--i8--g6	' 	 	 $$,%a8 001H1HI..u/D/DE"..33'  A'	r*   r   r   rZ   c                    | j                  d      sy| D ]N  }|j                  r|j                  r|j                  s)|j                  dv r8| j                  dd|j                  j                  xs |j                  fdd|j                  j                  fdd	d
|j                  fdd|j                  fgd      }|st        t        d|j                  j                  t        j                  j                  |j                        t        j                  j                  |j                        t        |j                  d   j                         |j                                  y)aj  susu (2026-07-21): core only ever shows a soft dashboard warning
        for overlapping leaves (_compute_dashboard_warning_message below) --
        it never actually blocks the second request. Turn that into a hard
        rule instead: an employee can't have two leave requests covering
        the same day(s) unless the earlier one has already been cancelled
        or rejected. Mirrors the exact overlap definition core's own
        warning already uses (date_from/date_to, the calendar-aware
        computed range, not the raw request_date_from/to), just enforced
        rather than merely flagged.
        overlapNr   r$   r   r   rn   )rZ   not inr   r   <r   >rI   r   z%(employee)s already has a leave request from %(date_from)s to %(date_to)s (%(state)s) that overlaps with this period. Cancel or wait for that request to be rejected before submitting a new one for the same day(s).rZ   )r'  r   r   rZ   )rh   r   r   r   rZ   rs   r#   r$   r
   r   r   r   r   r   dict_fieldsr  )r&   r(   conflicts      r)   _check_xeno_no_overlapzHrLeave._check_xeno_no_overlap  s<    ,,Y7 	E$$EOO5=={{22{{tU]]--9:U%6%6%9%9:9c5==1C1$  # H %a
 #..33$kk11(2D2DE"KK//0@0@Ax//8BBCHNNS
' 
 
	r*   c                     t         |           | D ]R  }|j                  s|j                  j                  dt	        d            j                  dt	        d            |_        T y )Nz?You've already booked time off which overlaps with this period:z?You've already requested leave which overlaps with this period:zDAn employee already booked time off which overlaps with this period:zDAn employee already requested leave which overlaps with this period:)r>   "_compute_dashboard_warning_messagedashboard_warning_messagereplacer   r&   r(   rF   s     r)   r<  z*HrLeave._compute_dashboard_warning_message  sl    24  	E22//UWXZ Z\]_ +	r*   c                 H    t        |      }t        ||z
  dz        }d||fz  S )N<   z	%02d:%02d)r  r   )r&   
hour_floatr   ms       r)   r-  zHrLeave._xeno_format_hour  s-    
O:>R'(aV##r*    _compute_xeno_is_approver_for_me_search_xeno_is_approver_for_mezI am the configured approver)r   rs   r-   c                     | j                          | j                         j                  j                  d       j	                  d      dd S )a)  The lowest-step_order pending xeno.leave.approval.step for this
        leave, or an empty recordset if it has no materialized chain (e.g.
        a legacy leave from before this feature existed) -- callers must
        treat "no step" as "fall back to pre-chain behavior", not "blocked".
        c                      | j                   dk(  S Nr  rZ   ss    r)   r   z4HrLeave._xeno_current_pending_step.<locals>.<lambda>2  s    agg* r*   rV   NrI   )r   rY   xeno_approval_step_idsr   r   r  s    r)   r\   z"HrLeave._xeno_current_pending_step*  sE     	yy{11::*,,2F<,@!E 	Er*   c                 p    | j                   j                  |k(  ry|j                  r|j                  |k(  S |S )aX  Whether acting_user may decide `step` on this leave, evaluated
        against an EXPLICIT user rather than self.env.user.

        Split out of _xeno_can_act_on_step so the searchable-field path can
        run this check over sudo()'d records (it has to enumerate every
        pending leave company-wide) without either of the two traps that
        come with that: env.is_superuser() is true for any sudo()'d call
        and would wave everything through, and reading employee_id on a
        leave the acting user can't see would raise AccessError instead of
        just excluding it.
        F)r   user_idrX   )r&   ra   acting_useracting_is_officers       r)   _xeno_step_acting_userzHrLeave._xeno_step_acting_user4  s;    & ##{2  ((K77  r*   c                     | j                   j                         ry| j                  || j                   j                  | j                   j                  j	                  t
                    S )NT)r   is_superuserrQ  r    r!   r"   r&   ra   s     r)   _xeno_can_act_on_stepzHrLeave._xeno_can_act_on_stepM  sM    88  "**$((--!8!89I!JL 	Lr*   zxeno_approval_step_ids.statez'xeno_approval_step_ids.approver_user_idc                 x    | D ]5  }|j                         }t        |      xr |j                  |      |_        7 y r   )r\   boolrU  xeno_is_approver_for_me)r&   r(   ra   s      r)   rD  z(HrLeave._compute_xeno_is_approver_for_meS  s>     	]E335D,0J,\5;V;VW[;\E)	]r*   c                    t        |t              r|}nd|v }|dv r| }| j                  j                  }|j	                  t
              }g }| j                         j                  dg      D ]S  }|j                         }t        |      xr |j                  |||      }	|	|k(  s9|j                  |j                         U dd|fgS )NT)r   r4  z<>)rZ   r   )rU   r   r$   r   )r  rW  r   r    r!   r"   rY   rs   r\   rQ  r   r$   )
r&   operatorvaluewantrO  rP  matching_idsr(   ra   is_mines
             r)   rE  z'HrLeave._search_xeno_is_approver_for_meY  s     eT"D5=D--8D$ hhmm'112BCYY['')R(ST 	.E335D4j 6U%A%Ak#4&6G$##EHH-	. t\*++r*   c                     | j                          | j                  j                         ry| j                  j                  | j                  j
                  k(  ry| j                         }|r| j                  |      S y)a  Narrows Odoo's own can_approve/can_validate/can_refuse to the
        acting user being allowed to act on this leave's *current pending
        approval-chain step* (xeno.leave.approval.step) -- a specific
        approver_user_id must match exactly, a blank one requires a Time
        Off Officer/Administrator, and earlier steps must already be
        approved (guaranteed by construction: the "current" step is always
        the lowest-order still-pending one). Never allow approving/
        refusing your own leave. A leave with no materialized chain at all
        (created before this feature existed) falls back to Odoo's own
        group-based result unchanged, exactly like before this feature.

        Fixes a real bug found in testing: Odoo's own can_approve/
        can_refuse grant any Time Off Officer approval rights over any
        leave, including their own -- since "My Team Leaves" approvers
        are Officers too (needed for Odoo's own state machine to permit
        the transition at all), this let any configured approver, or
        anyone else with that role, approve/refuse leaves that weren't
        theirs to decide, including self-approval.
        TF)r   r   rS  r   rN  r    r\   rU  rT  s     r)   _xeno_can_approve_or_refusez#HrLeave._xeno_can_approve_or_refuse~  se    ( 	88  "##txx}}4..0--d33r*   department_idc                 t    t         |           | D ]$  }|j                  s|j                         |_        & y r   )r>   _compute_can_approvecan_approver`  r?  s     r)   rc  zHrLeave._compute_can_approve  s;     	$& 	HE  $)$E$E$G!	Hr*   c                 t    t         |           | D ]$  }|j                  s|j                         |_        & y r   )r>   _compute_can_validatecan_validater`  r?  s     r)   rf  zHrLeave._compute_can_validate  s;     	%' 	IE!!%*%F%F%H"	Ir*   c                 t    t         |           | D ]$  }|j                  s|j                         |_        & y r   )r>   _compute_can_refuse
can_refuser`  r?  s     r)   ri  zHrLeave._compute_can_refuse  s;     	#% 	GE#(#D#D#F 	Gr*   c                 t    t         |           | D ]$  }|j                  s|j                         |_        & y r   )r>   _compute_can_back_to_approvecan_back_to_approver`  r?  s     r)   rl  z$HrLeave._compute_can_back_to_approve  s;     	,. 	PE((,1,M,M,O)	Pr*   r   c                    t         |           | j                  j                         ryt        j
                  j                  |       }| D ]  }|j                  j                  | j                  j                  k7  r1|j                  s|j                  dvrd|_        S|j                  xr) t        j
                  j                  |j                        }|xr ||k   |_         y)a  Replaces core's own cancel eligibility for an employee's OWN
        leave with this company's actual policy:

        - Core never allows cancelling from 'confirm' (submitted, still
          awaiting the first approval step) -- only from validate1/
          validate/refuse. This company wants a still-pending request to
          be self-cancellable too, so 'confirm'/'validate1' (pending) and
          'validate' (approved) are all allowed here.
        - Core DOES allow cancelling an already-Refused leave -- this
          company doesn't want that (a rejection is a closed decision,
          not something to undo), so 'refuse' is excluded here.
        - A system deduction (company holiday / late check-in deduction --
          xeno_is_system_deduction) was never something the employee
          asked for, so they can't cancel it themselves; only HR can
          reverse those (xeno_holidays' automatic reversal on holiday
          delete / the late-deduction cron).
        - Only a leave starting today or later can be self-cancelled --
          same past/future philosophy as _xeno_check_leave_timing, and
          with no role-based bypass for the requester's OWN leave either
          (see that method's docstring for the exact bug this mirrors:
          susu, an HR/admin account, could sidestep this company's rules
          on their own leave just by holding the Officer role). An HR
          officer acting on someone ELSE's leave is untouched -- this
          override only ever narrows/widens the leave's own requester.
        Nr   F)r>   _compute_can_cancelr   rS  r   r   r   r   rN  r    r   rZ   
can_cancelr   r   )r&   r   r(   r   rF   s       r)   ro  zHrLeave._compute_can_cancel  s    8 	#%88  "))$/ 	=E  ((DHHMM9..{{*NN#( ++\0C0CED[D[0\E$)$;eem<E	=r*   c           	         | j                         }| D ](  }|j                         }|s||z  }| j                  j                         s%|j	                  |      st        t        d            |j                         j                  d| j                  j                  j                  t        j                         d       |j                         }|rd|j                  t        d|j                  | j                  j                  j                  |j                               |j!                  |       $||z  }+ |r$t"        t$        |O  |       |j)                          y)	a  Approve the current pending step of this leave's chain, not the
        whole leave outright -- matches XENHR's step-wise approveStep():
        only when the LAST pending step is approved does the leave itself
        actually transition (delegating to Odoo core's own action_approve
        so allocation consumption / notifications happen exactly as they
        always have). A leave with no materialized chain (legacy, from
        before this feature existed) is approved in one shot as before.
        zJOnly the configured approver for this step can approve this leave request.approvedrZ   
decided_by
decided_atz@%(prev_label)s approved by %(user)s. Waiting on: %(next_label)s.)
prev_labelr    
next_labelbodycheck_stateT)r   r\   r   rS  rU  r	   r   rY   ri   r    r$   r   nowmessage_postrW   r   r]   r>   r   action_approve_xeno_send_final_notice)r&   r{  to_finalizer(   ra   r   rF   s         r)   r~  zHrLeave.action_approve  s;    kkm 	%E335Du$88((*53N3Nt3T #J !K L LIIK#"hhmm..&lln 
 88:I""&#zz0B0B(	)0" 1
 ++I6u$-	%. ';6;6O//1r*   c                    | j                   j                         s'| D ]"  }|j                  rt        t	        d             | D ]  }|j                         }|s<|j                         j                  j                  d       j                  d      dd  }|sT|j                         j                  d| j                   j                  j                  t        j                         d        t        | A         }| j#                          |S )Nz;Only the configured approver can refuse this leave request.c                      | j                   dk(  S )Nrr  rI  rJ  s    r)   r   z'HrLeave.action_refuse.<locals>.<lambda>'  s    agg3 r*   rV   rejectedrs  )r   rS  rj  r	   r   r\   rY   rL  r   r   ri   r    r$   r   r|  r>   action_refuser  )r&   r(   ra   rk   rF   s       r)   r  zHrLeave.action_refuse  s     xx$$& +''#A)%* + ++
  	E335D zz|::CC355;VL5I"#O 		!!'"&((--"2"2"*,,.# %	. g#%$$&
r*   c                 4   |j                  d      }|rv|j                  d      set        |       dk(  rW| j                  j                  j                  }|r5t        |      |j                  hk(  rt        |      }|j                  d       t        | (  di |S )a  Suppress Odoo core's own "Your leave has been accepted/refused"
        notification to the requestor (hardcoded, unconditional, inside
        _action_validate()/action_refuse()) so they get exactly one email:
        our own polished _xeno_send_final_notice(). Core's call is
        recognizable as partner_ids pointing at nobody but the leave's own
        employee with no subtype override; any other message_post (our own
        chatter notes, notifications to other people) is untouched.
        partner_idssubtype_xmlidrI   r   )r=   lenr   rN  
partner_idrf   r$   r7  popr>   r}  )r&   kwargsr  employee_partnerrF   s       r)   r}  zHrLeave.message_post4  s     jj/vzz/:s4yA~#//77BBC$49I9L9L8M$Mf

=)w#-f--r*   c                 ~    | j                   j                  j                  t              st	        t        d            y )NzROnly Time Off Officers can force-approve, force-reject, or edit a submitted leave.)r   r    r!   r"   r	   r   r  s    r)   _xeno_check_hr_officerzHrLeave._xeno_check_hr_officerE  s;    xx}}&&'78A J K L L 9r*   c                 F   | j                          | D ]  }t        |j                  d   j                        j	                  |j
                        }|j                  t        d| j                  j                  j                  |             |j                         j                  j                  d       j                  d| j                  j                  j                  t!        j"                         d        t$        t&        | S  d	      }| j+                          |S )
NrZ   ziForce-approved by %(user)s (HR override, bypassing the normal approval chain). Previous status: %(prev)s.)r    prevrx  c                      | j                   dk(  S rH  rI  rJ  s    r)   r   z3HrLeave.action_xeno_force_approve.<locals>.<lambda>V  s    !''Y. r*   rr  rs  Frz  )r  r7  r8  r  r=   rZ   r}  r   r   r    r   rY   rL  r   ri   r$   r   r|  r>   r   r~  r  )r&   r(   r  rk   rF   s       r)   action_xeno_force_approvez!HrLeave.action_xeno_force_approveJ  s    ##% 	Eg.889==ekkJDAEXX]]''d%4 5 JJL//88.e#"hhmm..&lln 	& GT1e1D$$&
r*   c                 N   | j                          | j                  d       }|r0t        t        d|d   j                  |d   j
                              | D ]=  }|j                  t        d| j                  j                  j                               ? | j                         S )Nc                     | j                   dv S )N)r   r   rI  rw   s    r)   r   z2HrLeave.action_xeno_force_reject.<locals>.<lambda>e  s    !''5I*I r*   z,%s is already %s -- nothing to force-reject.r   z)Force-rejected by %(user)s (HR override).)r    rx  )r  r   r	   r   display_namerZ   r}  r   r    r   r  )r&   blockedr(   s      r)   action_xeno_force_rejectz HrLeave.action_xeno_force_rejectc  s    ##%-- IJA>
'')9)9; < <  	WEA;$((--BTBT%V W	W !!##r*   c                 p    | D ]  }|j                  t        d              t        t        |   d      S )a  Immediately validate a batch of leaves created by
        wizard/leave_hr_create.py (HR creating directly on an employee's
        behalf) -- no approval chain exists on these (see the
        xeno_hr_direct_create context guard in _xeno_build_approval_chain),
        so there's nothing to mark as bypassed, just the real core
        validate transition, same as action_xeno_force_approve's own
        bypass call.
        zAdded by HR/Admin.rx  Frz  )r}  r   r>   r   r~  r?  s     r)   _xeno_hr_created_approvez HrLeave._xeno_hr_created_approveo  s@      	=EA&:$;<	=Wd2u2EEr*   c                 l    | D ]  }|j                  t        d              t        t        |          S )a  Same bypass as _xeno_hr_created_approve() above, for rejecting
        instead -- used by the Leave Request Import feature to backfill
        already-decided historical records without spamming the requestor
        with a real rejection email for something that was decided in the
        past (this module's own action_refuse() always sends one via
        _xeno_send_final_notice(), core's plain action_refuse() does not).z*Recorded as rejected by HR/Admin (import).rx  )r}  r   r>   r   r  r?  s     r)   _xeno_hr_created_rejectzHrLeave._xeno_hr_created_reject|  s=      	UEA&R$ST	UWd133r*   )Annual Leave . ALPersonal Leave . PLzUnpaid Leave . ULc                     | j                   d   }|j                         }| j                  D ]6  }|j                  dd|fgd      }|st	        t        d|            ||z  }8 |S )aO  Returns a real hr.leave.type recordset, ordered to match
        _XENO_HOLIDAY_DEDUCT_CASCADE_NAMES exactly (a 3-record recordset
        built via ordered |= concatenation, safe to unpack positionally
        as `annual, personal, unpaid = ...` or use recordset methods like
        .ids/.mapped() on directly -- unlike a plain list).r   r   rn   rI   r   zLeave type '%(name)s' is required for the company holiday deduction cascade but no longer exists. Check Time Off > Configuration > Leave Types.r   )r   r   "_XENO_HOLIDAY_DEDUCT_CASCADE_NAMESrs   r	   r   r&   Typer   r   r   s        r)   "_xeno_holiday_deduct_cascade_typesz*HrLeave._xeno_holiday_deduct_cascade_types  s     xx(;; 	 DvsD&9%:!DJD 	!    
 ZE	  r*   c           
         | j                   d   j                         }| j                         }t        |j                  dd|j                  fdd|j                  fdg      j                  d            }t        |j                  dd|j                  fdd|j                  fdg      j                  d            }t        |j                  dd|j                  fdd|j                  fddd	d
gfg      j                  d            }||z
  |z
  S )Nrm   r   rn   r   ro   rM   rZ   r   rU   r   )r   rY   rr   rs   r$   rt   )r&   r'  r   ru   rv   r{   r   r  s           r)   &_xeno_holiday_deduct_remaining_balancez.HrLeave._xeno_holiday_deduct_remaining_balance  s   ./446		ELLC- #z}}5&"
  6"#	%
 5<<C- #z}}5&!
  6"#	%
 ellC- #z}}5dY45$
  6"#	%
 t|g%%r*   c                 X   |s| j                         S ||z
  j                  dz   }| j                         \  }}}	| j                         }
| j                         }|D ]  }| j	                  ||      |k\  r|}n| j	                  ||      |k\  r|}n|	}|
j                  dd      j                  |j                  |j                  d|||xs t        d      |r|j                  ndddd	      }|j                  t        d	||j                  
             ||z  } |j                          |S )a  Create + immediately approve one full-day (or date_from-date_to
        span) leave per employee, cascading Annual -> Personal -> Unpaid
        per _XENO_HOLIDAY_DEDUCT_CASCADE_NAMES. Runs sudo()'d -- callers
        (the wizard, or xeno_holidays' automatic hook) are responsible for
        their own permission checks; this method just does the work.

        source_holiday, when given (only by xeno_holidays' automatic hook,
        never the manual wizard -- that one isn't tied to one specific
        holiday record), links the created leave back to it so deleting
        that holiday can find and reverse exactly the leaves it caused.
        rI   T)rT   rH   r  zCompany holiday deductionFr2   )	r   r   r   r   r   r   xeno_source_holiday_idr   xeno_system_deduction_typez?Company holiday deduction: %(days)s day(s) charged to %(type)s.)rN   r   rx  )r   rN   r  rY   r  with_contextr?   r$   r   r}  r   r  )r&   	employeesr   r   reasonsource_holidaydays_neededannualpersonalunpaidrv   createdr'  r   r(   s                  r)    _xeno_deduct_for_company_holidayz(HrLeave._xeno_deduct_for_company_holiday  sK    ;;= *0014#'#J#J#L &		++-! 	H::8VLP[[#
<<XxPT__%
#
&&&* ' f'{{%/]]%/%.#*@!$?"@?M.*;*;SX,0.?
 
  A"-JOO%E F uG/	0 	((*r*   c                     | j                         j                  d       }|sy|D ]  }|j                  t        d              t        t
        |          y)a  Give the day back: called when the Company Holiday that caused
        this deduction is being deleted. Only reverses leaves still in
        'validate' state (an already-cancelled/refused one has nothing to
        give back); runs the same core refuse transition Force Reject
        uses, bypassing the HR-officer check since this is a system
        action triggered by deleting the holiday, not a human decision.c                      | j                   dk(  S )Nrp   rI  r  s    r)   r   z9HrLeave._xeno_reverse_holiday_deduction.<locals>.<lambda>  s    :0E r*   NzvAutomatically reversed: the Company Holiday this leave was deducted for has been deleted. The day has been given back.rx  )rY   r   r}  r   r>   r   r  )r&   leavesr(   rF   s      r)   _xeno_reverse_holiday_deductionz'HrLeave._xeno_reverse_holiday_deduction  s]     %%&EF 	 EA%  	 
 	gv,.r*   
   i  r  r  c                     | j                   d   }|j                         }| j                  D ]6  }|j                  dd|fgd      }|st	        t        d|            ||z  }8 |S )Nr   r   rn   rI   r   zLeave type '%(name)s' is required for the late check-in deduction cascade but no longer exists. Check Time Off > Configuration > Leave Types.r  )r   r   _XENO_LATE_DEDUCT_CASCADE_NAMESrs   r	   r   r  s        r)   _xeno_late_deduct_cascade_typesz'HrLeave._xeno_late_deduct_cascade_types  s    xx(88 	 DvsD&9%:!DJ5;?!A B B ZE	  r*   c                    | j                         \  }}| j                  d   j                         }| j                  d   j                         }| j                         }| j                  d   j                         j                  ||      }t	        ||t        j                  ||      d         }	|	j                  d      }
t        |j                  dd|fdd|fg      j                  j                        }d	}d
}|j                  dg       D ]  }|j                  d      xs d	}|| j                  k  r)|j                  d      }|s=|j                  ddt        |      fgd      }|r|j                  |v rn|| j                  z
  }|| j                   z  }| j#                  ||      |k\  r|n|}||k(  r|n|}d}d}||fD ]  }	 | j                  j$                  j'                         5  |j)                  d|      j+                  |j                  |j                  d|	|	t-        d|
|| j                        ddd      }|j/                          ddd       |}} n |s#t4        j7                  d|j8                  |
       y|j;                  t-        d||
| j                  ||j8                               |j+                  |j                  |||||j                  |j                  d       |dz  }||z  } ||fS # 1 sw Y   xY w# t0        t2        f$ r8}t4        j7                  d|j8                  ||j8                  |       Y d}~d}~ww xY w)a|  Sums each employee's total late-checkin minutes for the given
        month (xeno_attendance's own monthly report computation --
        xeno.attendance.report.get_month_report, the same figure shown on
        the Monthly Attendance page's Late column), deducts the excess
        beyond the monthly grace period as a fractional leave day.
        Idempotent per (employee, year, month) via the unique constraint
        on xeno.late.deduction.log -- already-processed employees are
        silently skipped on a re-run (e.g. cron + manual trigger both
        firing for the same month). Returns (employee_count, total_days).
        zhr.employeezxeno.late.deduction.logzxeno.attendance.reportrI   r   r   rn   r   r   r   rowslateemployee_codexeno_employee_coder   NT)rT   rJ   r  zcLate check-in deduction for %(month)s: %(minutes)s min over the %(grace)s-min monthly grace period.)r   minutesgracer3   )r   r   r   r   r   r   r   r  zZLate check-in deduction: could not charge %s's %.2f day(s) to %s (%s) -- trying next tier.zLate check-in deduction: skipped %s for %s -- neither Annual nor Personal could be charged (likely no valid allocation for this date). Will retry on the next run.zzLate check-in deduction: %(minutes)s min late in %(month)s (%(grace)s min grace) -> %(days).2f day(s) charged to %(type)s.)r  r   r  rN   r   rx  )r   r   r   late_minutesdeducted_daysr   r,   )r  r   rY   get_month_reportr   r   r   r   rf   rs   r   idsr=   _XENO_LATE_DEDUCT_GRACE_MINUTESr+  r$   !_XENO_LATE_DEDUCT_WORKDAY_MINUTESr  cr	savepointr  r?   r   r  r
   r	   _loggerwarningr   r}  )r&   r   r   r  r  EmployeeLogrv   reportlast_dayr   alreadyemployee_count
total_daysrowr  r}   r'  excess_minutesdeduct_days	preferredfallbackr(   r   	candidatecandidate_leaveexcs                              r)   _xeno_process_late_deductionsz%HrLeave._xeno_process_late_deductions  s     ??A88M*//1hh01668		2388:KKDRWXeX%8%8u%Ea%HI''0cjjS$'3!6"
 ;ss 
::fb) R	&C776?/aLtCCC77?+D)=sCI(N'OWXYHx{{g5)D,P,PPN(4+Q+QQK ;;HfM %-  $-#6xFHEJ'2 I	I..0 C*/*<*<266A += + !&+3;;1:1;19/7$%!8 '2<&*&J&J%L 9=:J"  ($ (@@B'C( )8:E/I< - /7mm[	J
 A' %K:: z%8 9 JJ'{{D5 ,{!+EHH 
 aN+%JeR	&h z))gC C, (3 IOOF {INNCI IIs1   $L*A'LLLLM-MMc                     t         j                  j                  |       }|j                  dkD  ry|j	                  d      t        d      z
  }| j                  |j                  |j                         y)aJ  Runs daily (like _cron_xeno_welcome_back_from_maternity) but
        only acts during the first week of the month, processing the
        just-ended previous month -- deliberately a daily check with a
        day-of-month guard rather than a strict monthly interval, so a
        paused/delayed cron still catches up within that window instead
        of drifting or skipping a month. Safe to run more than once (or
        every day of the window): the unique constraint on
        xeno.late.deduction.log means only employees not yet processed
        for that month are ever touched.   NrI   )r  r   )	r   r   r   r  r>  r   r  r   r   )r&   r   prev_month_ends      r)   "_cron_xeno_process_late_deductionsz*HrLeave._cron_xeno_process_late_deductions  s\     ))$/99q=1-	q0AA**>+>+>@T@TUr*   zMaternity Leave . MLc                    | j                   d   j                  dd| j                  fgd      }|syt        j                  j                  |       t        d      z
  }| j                         j                  dd|j                  fd	d
d|fdg      }|D ]  }|j                  }t        d|j                        }|j                  |       |j                  xs$ |j                  r|j                  j                  nd}|r}|j!                  t        d|j                        |d      }| j                   d   j                         j#                  t        d|j                        ||dd      j%                          d|_         y)a  Daily: an employee whose approved Maternity Leave ended
        yesterday is back at work today -- post a congratulations note on
        their employee record and email them, once per leave (guarded by
        xeno_welcome_back_sent). Date-driven rather than tied to their
        first actual attendance scan, which would depend on GPS/kiosk
        reliability for a one-off nice-to-have message.r   r   rn   rI   r   Nr   r   ro   r   )xeno_welcome_back_sentrn   FzWelcome back, %(name)s! Congratulations on your new baby, and best wishes to your family. We're glad to have you back with the team.r  rx  FzWelcome back, %(name)s!u   👶🎉)titlemessageemojiz	mail.mailT)subject	body_htmlemail_toauto_delete)r   rs   "_XENO_WELCOME_BACK_LEAVE_TYPE_NAMEr   r   r   r   rY   r$   r   r   r   r}  
work_emailrN  email_xeno_render_wish_email_htmlr?   sendr  )	r&   r   	yesterdayr  r(   r'  r  r  htmls	            r)   &_cron_xeno_welcome_back_from_maternityz.HrLeave._cron_xeno_welcome_back_from_maternity  s    XXo.55c4BBCDA 6 O
KK--d3iQ6GG	## #z}}5&Y/2	%
   	0E((H/5=]]DG !!w!/'' G*2*:*:  &&   <<5HMMJ#: = 7 %**,33 !:O!% %#'	5 
 46+/E(-	0r*   c                    | j                         j                  ddg dfdd|fdd|fg      }|D cg c]  }|j                  |j                  j                  |j                  j
                  xs d|j                  j                  j                  xs d|j                  j                  |j                  |j                  |j                  |j                  |j                  r|j                  j                         nd	|j                  r|j                  j                         nd	|j                  d
 }}| j                   d   j                         j                  ddddgfdd|fdd|fg      }||D cg c]  }d|j                  z  |j                  j                  |j                  j
                  xs d|j                  j                  j                  xs dddd	d	|j                  |j"                  r|j"                  j                         nd	|j$                  r|j$                  j                         nd	|j                  dk(  rdndd
 c}z  }|S c c}w c c}w )a  Company-wide "who's on approved/pending leave" for one date --
        the same data the HR Dashboard's own Employee Attendance Report
        widget shows (xeno_hr_dashboard), but safe to expose to every
        internal user (no groups= restriction), for the same widget on the
        self-service My Profile landing page. Deliberately NOT a raw
        client-side hr.leave search_read: core's own hr_holidays record
        rules would silently scope a regular employee down to just their
        own (+ direct reports') leaves there, the same "looks company-wide,
        is actually partial" trap already avoided elsewhere in this
        codebase (My Team Leaves reads xeno.leave.approver, never hr.leave,
        for exactly this reason). sudo()'d, and only ever returns the same
        handful of already-public-facing fields the HR widget shows --
        no reason, no attachments, nothing sensitive.
        rZ   r   r   r   r   r   r   -F)r$   employeeNameposition
departmentleaveTypeNamerequestUnitrequestHourFromrequestHourTonumberOfDaysrequestDateFromrequestDateTorZ   zhr.business.triprU   acknowledgedr   r   zbt-%dzBusiness Trip . BTr  rp   )rY   rs   r$   r   r   	job_titlera  r   r   r   r   rM   r   	isoformatr   rZ   r   r   r   )r&   date_strr  r(   r  tripstrips          r)   xeno_get_attendance_reportz"HrLeave.xeno_get_attendance_report  s?     ##d@A $1h/%
 $   ((!--22))33:s++99>>E#"4499 22$66"22!00FKF]F]u66@@BchBGBWBWU22<<>]b[[
  0 +,113::dY78$)h'<
 
 	   DGG# ,,11((229c**88==D1%$" //=A^^t~~779QV9=T\\3355#'::#?ZY
  	 W:s   D I3CI)T)NN)Y__name__
__module____qualname___inheritr   Booleanr%   r   depends_contextr   One2manyrL  r  Many2oner  r   	Selectionr  model_create_multir?   rK   rB   ri   rA   modelrh   r9   r:   r;   r<   r@   r   r  Htmlxeno_leave_type_detailr   Floatr	  r
  r  r  dependsr  onchanger  r  r  r  r  
constrainsr1  r:  r<  r-  rX  r\   rQ  rU  rD  rE  r`  rc  rf  ri  rl  ro  r~  r  r}  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  __classcell__)rF   s   @r)   r   r      s   H*FNN05 S	M  	M -V__"J7GI ,V^^EGH
 -V__"+A$67  .v~~EG	 H "2!1!1	9	-	/K	"L 	 ."DH8@0, 	YY	* 	*=+~*3X5%n*<X[z(!T ,V^^:T
 )V[[/$
 )((	
 ":" &.EgV$-DWU'6<<0GPWX)V\\2IRYZS[[ 34 B 5 BD S\\%&/ '/ /f..O7$  !	N S[[(2
U
U S\\%&0 '0 S^^')<>O(-9898t S^^M;	7C$ D$L( 	YY$ $
 -fnn2;\-
E!2L S[[/1Z[] \]
#,J< SS[[-/1Z\H\  H SS[[-/1Z\I\  I SS[[-/1Z\G\  G SS[[-/1Z\P\  P SS[[-)<>XY&= Z  &=P$L"H."L
2
$F	4&*I& 	YY & 	YY& &( 	YYFJ, ,\/. ')#(+%':<Q&R#YY  	YYn* n*` 	YYV V& *@&YY(0 (0T 	YY? ?r*   r   )r   loggingdatetimer   r   odoor   r   r   r   odoo.exceptionsr	   r
   odoo.fieldsr   	getLoggerr  r  r"   rg   Modelr   r   r*   r)   <module>r     sL      $ ' ' 6  
'

H
%7  lfll lr*   