From b15b0478acb9e968c2cd49878965a0380b994ccd Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 2 Mar 2026 16:26:23 -0500 Subject: [PATCH] Mar 02 2026: fixed overnight shift calculation --- app.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/app.py b/app.py index f2192bb..0cfd9eb 100644 --- a/app.py +++ b/app.py @@ -9629,13 +9629,15 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ # ------------------------------------------------------------------- # OVERNIGHT SHIFT DETECTION # The midnight check-out record is stored in the DB with the next - # calendar day's date (e.g. checkout at 12:01 AM on Wednesday is - # stored as check_in_date = 2026-02-11). We need to move it into - # Tuesday's bucket so it pairs with the 8 PM check-in. + # calendar day's date (e.g. checkout at 12:18 AM on Thursday is + # stored as check_in_date = 2026-02-26). We need to move it into + # Wednesday's bucket so it pairs with the 8:18 PM check-in. # - # Condition to move an early-morning checkout from Day N+1 → Day N: - # Day N: more check-ins than check-outs (unmatched late IN) - # Day N+1: more check-outs than check-ins (orphaned early OUT) + # Condition to move an early-morning checkout from Day N+1 -> Day N: + # Day N: has an unmatched late check-in (>= 18:00) + # Day N+1: has an early check-out (<= 06:00) that belongs to Day N, + # detected by the absence of a non-evening IN on Day N+1 + # that could own the early OUT (or raw count imbalance). # ------------------------------------------------------------------- def _is_out(r): a = (r.action_description or '').lower() @@ -9662,21 +9664,32 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ _nxt_ins = [r for r in _next_recs if not _is_out(r)] _nxt_outs = [r for r in _next_recs if _is_out(r)] - # Day N must have an unmatched late check-in + # Day N must have an unmatched late check-in (more INs than OUTs, + # with at least one IN at or after 18:00) if len(_day_ins) <= len(_day_outs): continue _late_ins = [r for r in _day_ins if r.check_in_time.hour >= 18] if not _late_ins: continue - # Day N+1 must have an orphaned early check-out - if len(_nxt_outs) <= len(_nxt_ins): - continue + # Find early-morning OUTs (<=06:00) on Day N+1 _early_outs = [r for r in _nxt_outs if r.check_in_time.hour <= 6] if not _early_outs: continue - # Move up to as many early OUTs as there are unmatched late INs + # Determine whether the early OUT belongs to Day N or Day N+1. + # It belongs to Day N when Day N+1 has no non-evening (< 18:00) check-in + # that could own it, OR when OUTs outnumber INs on Day N+1. + # This handles both cases: + # Case A: Day N+1 has only evening INs (all >= 18:00) -> early OUT is Day N's + # Case B: Day N+1 has more OUTs than INs overall -> early OUT is unmatched + _nxt_non_evening_ins = [r for r in _nxt_ins if r.check_in_time.hour < 18] + if _nxt_non_evening_ins and len(_nxt_outs) <= len(_nxt_ins): + # Day N+1 has a non-evening IN that can own the early OUT, and counts + # are balanced -> do NOT move + continue + + # Move up to as many early OUTs as there are unmatched late INs on Day N _to_move = _early_outs[:len(_late_ins)] for _co in _to_move: _co_loc = _co.location_name or 'Unknown Location' @@ -9694,12 +9707,13 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ del daily_location_data[_ndk][_co_loc] if _ndk in daily_location_data and not daily_location_data[_ndk]: del daily_location_data[_ndk] - print(f"🌙 [TA Export] Overnight: moved checkout {_co.check_in_time} " - f"from {_ndk} → {_dk} for employee {employee_id}") + print(f"\U0001f319 [TA Export] Overnight: moved checkout {_co.check_in_time} " + f"from {_ndk} to {_dk} for employee {employee_id}") # ------------------------------------------------------------------- # END OVERNIGHT SHIFT DETECTION # ------------------------------------------------------------------- + # Track weekly hours for overtime calculation weekly_total_hours = 0 current_week_start = None