From 28194fc638dc09efc4a41207c59a812fcf9ca2a6 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 16 Mar 2026 18:11:40 -0400 Subject: [PATCH] March 16 2026: fix export overnight calculation issue --- app.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index cb3085c..e28dfac 100644 --- a/app.py +++ b/app.py @@ -9870,6 +9870,16 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ if _out_t_d.hour <= 3: if _in_t_d.hour < 18: continue + # Orphan guard: an early-morning OUT whose check_in_date + # matches the current day is an orphan from the PREVIOUS + # overnight shift — it must NOT steal an evening IN. + # Only OUTs moved in by overnight detection (check_in_date + # is later than the current day) are valid partners. + _out_orig_date = _out_r.check_in_date + if hasattr(_out_orig_date, 'date'): + _out_orig_date = _out_orig_date.date() + if _out_orig_date <= date_obj.date(): + continue elif _out_t_d <= _in_t_d: continue _in_ts = datetime.combine(_in_r.check_in_date, _in_r.check_in_time) @@ -9920,6 +9930,14 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ if _cb_out_t.hour <= 3: if _cb_in_t.hour < 18: continue + # Orphan guard: same-day early-morning OUT is from previous + # overnight shift — skip it. Only moved OUTs (check_in_date + # later than current day) are valid overnight partners. + _cb_out_orig = _cb_out.check_in_date + if hasattr(_cb_out_orig, 'date'): + _cb_out_orig = _cb_out_orig.date() + if _cb_out_orig <= date_obj.date(): + continue elif _cb_out_t <= _cb_in_t: continue _cb_in_ts = datetime.combine(_cb_in.check_in_date, _cb_in.check_in_time) @@ -10134,6 +10152,15 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ if _out_t.hour <= 3: if _in_t.hour < 18: continue # early-morning OUT cannot pair with non-evening IN + # Orphan guard: same-day early-morning OUT is from a + # previous overnight shift — not a valid partner for + # this evening IN. Only moved OUTs (check_in_date + # later than current day) should pair. + _out_orig_d = out_ri['record'].check_in_date + if hasattr(_out_orig_d, 'date'): + _out_orig_d = _out_orig_d.date() + if _out_orig_d <= date_obj.date(): + continue elif _out_t <= _in_t: continue # same-day OUT must be strictly after IN if out_ri['work_type'] == in_ri['work_type']: @@ -10170,6 +10197,15 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ if _out_t2.hour <= 3: if _in_t2.hour < 18: continue + # Orphan guard: same-day early-morning OUT is from a + # previous overnight shift — not a valid partner for + # this evening IN. Only moved OUTs (check_in_date + # later than current day) should pair. + _out_orig_d2 = out_ri['record'].check_in_date + if hasattr(_out_orig_d2, 'date'): + _out_orig_d2 = _out_orig_d2.date() + if _out_orig_d2 <= date_obj.date(): + continue elif _out_t2 <= _in_t2: continue # Cross-type pair: one side is regular, other is special @@ -11101,17 +11137,45 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename, latest_in = ins_sorted[-1] excess_ins = ins_sorted[:-1] - earliest_out = outs_sorted[0] + + # Orphan guard: when the latest IN is evening (>=18h), skip + # early-morning OUTs (<=3h) whose check_in_date matches the + # current day — they are orphans from a previous overnight shift. + _oi_in_hour = latest_in['record'].check_in_time.hour + earliest_out = None + _oi_skip = [] + for _oi_ri in outs_sorted: + if (earliest_out is None + and _oi_in_hour >= 18 + and _oi_ri['record'].check_in_time.hour <= 3): + _oi_out_d = _oi_ri['record'].check_in_date + if hasattr(_oi_out_d, 'date'): + _oi_out_d = _oi_out_d.date() + if _oi_out_d <= date_obj.date(): + _oi_skip.append(_oi_ri) + continue + if earliest_out is None: + earliest_out = _oi_ri + break for ri in excess_ins: ri['used'] = True pairs.append({'check_in': ri['record'], 'check_out': None, 'is_miss_punch': True}) - latest_in['used'] = True - earliest_out['used'] = True - pairs.append({'check_in': latest_in['record'], 'check_out': earliest_out['record'], 'is_miss_punch': False}) + if earliest_out is not None: + latest_in['used'] = True + earliest_out['used'] = True + pairs.append({'check_in': latest_in['record'], 'check_out': earliest_out['record'], 'is_miss_punch': False}) + else: + latest_in['used'] = True + pairs.append({'check_in': latest_in['record'], 'check_out': None, 'is_miss_punch': True}) - for ri in outs_sorted[1:]: + for ri in outs_sorted: + if not ri['used'] and ri not in _oi_skip: + ri['used'] = True + pairs.append({'check_in': None, 'check_out': ri['record'], 'is_miss_punch': True}) + # Orphan OUTs that were skipped + for ri in _oi_skip: ri['used'] = True pairs.append({'check_in': None, 'check_out': ri['record'], 'is_miss_punch': True}) @@ -11129,6 +11193,22 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename, if record_info[j]['used']: continue if record_info[j]['is_out']: + # Orphan guard: when this IN is an evening + # check-in (>=18h) and the candidate OUT is + # early-morning (<=3h), the OUT is only a + # valid partner if it was moved in by overnight + # detection (check_in_date > current day). + # Same-day early-morning OUTs are orphans from + # a previous overnight shift. + _in_rec = record_info[i]['record'] + _out_rec = record_info[j]['record'] + if (_in_rec.check_in_time.hour >= 18 + and _out_rec.check_in_time.hour <= 3): + _out_bb_date = _out_rec.check_in_date + if hasattr(_out_bb_date, 'date'): + _out_bb_date = _out_bb_date.date() + if _out_bb_date <= date_obj.date(): + continue # orphan — skip pairs.append({ 'check_in': record_info[i]['record'], 'check_out': record_info[j]['record'],