Fix Time Attendance exported file format

This commit is contained in:
Nguyen Ngo
2025-09-04 12:49:50 -04:00
parent fcb0bedf8b
commit 049083588f
2 changed files with 62 additions and 29 deletions
+19 -7
View File
@@ -771,10 +771,18 @@ class PayrollExcelExporter:
date_obj = datetime.strptime(date_str, '%Y-%m-%d') date_obj = datetime.strptime(date_str, '%Y-%m-%d')
day_hours_data = emp_data['daily_hours'][date_str] day_hours_data = emp_data['daily_hours'][date_str]
# Skip days with no hours or miss punch # Skip days with miss punch, but INCLUDE days with valid hours even if single period
if day_hours_data.get('is_miss_punch', False) or day_hours_data.get('total_hours', 0) <= 0: if day_hours_data.get('is_miss_punch', False):
print(f"⚠️ Skipping {date_str} - Miss punch detected")
continue continue
# Include days with valid working hours (single periods should show Daily Total)
if day_hours_data.get('total_hours', 0) <= 0:
print(f"⚠️ Skipping {date_str} - No working hours")
continue
print(f"✅ Including {date_str} - Working hours: {day_hours_data.get('total_hours', 0):.2f}")
# Calculate week boundaries # Calculate week boundaries
week_start = date_obj - timedelta(days=date_obj.weekday()) # Monday of the week week_start = date_obj - timedelta(days=date_obj.weekday()) # Monday of the week
@@ -817,6 +825,10 @@ class PayrollExcelExporter:
# Write multiple pairs for the day (like template shows FRIDAY appearing twice) # Write multiple pairs for the day (like template shows FRIDAY appearing twice)
pairs_written = 0 pairs_written = 0
if len(day_records) >= 2: if len(day_records) >= 2:
# Calculate total number of pairs for this day
total_pairs_for_day = len(day_records) // 2
print(f"📊 Day {date_str}: {len(day_records)} records = {total_pairs_for_day} pairs")
# Create pairs from consecutive records # Create pairs from consecutive records
for i in range(0, len(day_records) - 1, 2): for i in range(0, len(day_records) - 1, 2):
if i + 1 < len(day_records): if i + 1 < len(day_records):
@@ -826,7 +838,7 @@ class PayrollExcelExporter:
current_row = self._write_record_pair_row( current_row = self._write_record_pair_row(
worksheet, current_row, date_obj, start_record, end_record, worksheet, current_row, date_obj, start_record, end_record,
location, building_address, total_hours, regular_hours, ot_hours, location, building_address, total_hours, regular_hours, ot_hours,
pairs_written, total_hours # Pass total_hours for daily total on last pair pairs_written, total_hours, total_pairs_for_day # Add total_pairs parameter
) )
pairs_written += 1 pairs_written += 1
total_pairs_written += 1 # Track total pairs total_pairs_written += 1 # Track total pairs
@@ -856,9 +868,9 @@ class PayrollExcelExporter:
return current_row return current_row
def _write_record_pair_row(self, worksheet, current_row: int, date_obj: datetime, def _write_record_pair_row(self, worksheet, current_row: int, date_obj: datetime,
start_record, end_record, location: str, building_address: str, start_record, end_record, location: str, building_address: str,
day_total_hours: float, regular_hours: float, ot_hours: float, day_total_hours: float, regular_hours: float, ot_hours: float,
pair_index: int, daily_total_hours: float) -> int: pair_index: int, daily_total_hours: float, total_pairs_for_day: int = 1) -> int:
"""Write a single record pair row (in/out times)""" """Write a single record pair row (in/out times)"""
day_name = date_obj.strftime('%A').upper() day_name = date_obj.strftime('%A').upper()
date_str = date_obj.strftime('%m/%d/%Y') date_str = date_obj.strftime('%m/%d/%Y')
@@ -918,7 +930,7 @@ class PayrollExcelExporter:
recorded_location_display = f'=HYPERLINK("{maps_url}","{recorded_location}")' recorded_location_display = f'=HYPERLINK("{maps_url}","{recorded_location}")'
# Determine if this is the last pair of the day (for daily total) # Determine if this is the last pair of the day (for daily total)
is_last_pair = pair_index > 0 # Show daily total on second+ pairs is_last_pair = (pair_index == total_pairs_for_day - 1) # Show daily total on second+ pairs
daily_total_display = daily_total_hours if is_last_pair else "" daily_total_display = daily_total_hours if is_last_pair else ""
row_data = [ row_data = [
+43 -22
View File
@@ -197,52 +197,73 @@ class SingleCheckInCalculator:
""" """
Calculate hours for a single day from check-in records Calculate hours for a single day from check-in records
Logic: CORRECTED Logic:
- Must have even number of records (complete pairs) - 0 records = No work (0 hours, not miss punch)
- 1st = check-in, 2nd = check-out, 3rd = check-in, 4th = check-out, etc. - 1 record = Miss punch (incomplete pair)
- Any odd number of records = miss punch - 2, 4, 6, 8... records = Complete pairs, calculate all
- Any invalid work period = miss punch - 3, 5, 7, 9... records = Calculate complete pairs only, ignore last odd record
Returns: (hours, is_miss_punch) Returns: (hours, is_miss_punch)
""" """
if not day_records: if not day_records:
return 0.0, False return 0.0, False # No records = no work
# Must have even number of records for complete pairs
if len(day_records) % 2 != 0:
print(f"⚠️ Odd number of records ({len(day_records)}) - miss punch (incomplete pairs)")
return 0.0, True # Odd number = miss punch
# Sort records by time # Sort records by time
sorted_records = sorted(day_records, key=lambda r: r.timestamp) sorted_records = sorted(day_records, key=lambda r: r.timestamp)
print(f"📝 Processing {len(sorted_records)} records for the day (must be complete pairs)") print(f"📝 Processing {len(sorted_records)} records for daily calculation")
# SINGLE RECORD = MISS PUNCH (CORRECTED)
if len(sorted_records) == 1:
print(f"⚠️ Single record found - Miss punch (incomplete pair)")
return 0.0, True # Single record is always miss punch
# CALCULATE COMPLETE PAIRS ONLY (CORRECTED FOR ODD NUMBERS)
# For odd numbers: process pairs and ignore the last unpaired record
num_complete_pairs = len(sorted_records) // 2
records_to_process = num_complete_pairs * 2 # Only process paired records
print(f"📊 Processing {num_complete_pairs} complete pairs from {len(sorted_records)} total records")
# Create work periods from consecutive check-ins (must be pairs)
work_periods = [] work_periods = []
for i in range(0, len(sorted_records), 2): total_hours = 0.0
# Process complete pairs only
for i in range(0, records_to_process, 2):
start_record = sorted_records[i] start_record = sorted_records[i]
end_record = sorted_records[i + 1] # We know this exists because we checked even count end_record = sorted_records[i + 1]
period = WorkPeriod(start_record, end_record) period = WorkPeriod(start_record, end_record)
# Validate work period duration # Validate work period duration
if self._is_valid_work_period(period): if self._is_valid_work_period(period):
work_periods.append(period) work_periods.append(period)
print(f"✅ Valid work period: {start_record.check_in_time} - {end_record.check_in_time} = {period.duration_minutes/60:.2f} hours") pair_hours = period.duration_minutes / 60.0
total_hours += pair_hours
print(f"✅ Valid work period: {start_record.check_in_time} - {end_record.check_in_time} = {pair_hours:.2f} hours")
else: else:
print(f"⚠️ Invalid work period: {period.duration_minutes/60:.2f} hours - miss punch (invalid duration)") print(f"⚠️ Invalid work period: {period.duration_minutes/60:.2f} hours - treating as miss punch")
return 0.0, True # Invalid period = miss punch return 0.0, True # Invalid period = miss punch
# If we got here, all periods are valid # Check if we had unpaired records (odd number)
total_minutes = sum(period.duration_minutes for period in work_periods) has_unpaired = len(sorted_records) % 2 != 0
total_hours = total_minutes / 60.0 if has_unpaired:
unpaired_record = sorted_records[-1]
print(f"⚠️ Unpaired record found: {unpaired_record.check_in_time} (ignored in calculation)")
# Round to nearest quarter hour # Round to nearest quarter hour
rounded_hours = round(total_hours * 4) / 4 rounded_hours = round(total_hours * 4) / 4
print(f"📊 Daily total: {rounded_hours:.2f} hours from {len(work_periods)} complete work periods") # Determine if this is a miss punch scenario
is_miss_punch = (num_complete_pairs == 0) # No valid pairs = miss punch
return rounded_hours, False if is_miss_punch:
print(f"⚠️ No valid work periods found - Miss punch")
return 0.0, True
else:
print(f"✅ Daily total: {rounded_hours:.2f} hours from {num_complete_pairs} complete work periods")
# Log the calculation for tracking
print(f"📊 CORRECTED: Employee daily hours calculated - {rounded_hours:.2f} hours, Miss punch: {is_miss_punch}")
return rounded_hours, False
def _is_valid_work_period(self, period: WorkPeriod) -> bool: def _is_valid_work_period(self, period: WorkPeriod) -> bool:
"""Check if a work period is valid (reasonable duration)""" """Check if a work period is valid (reasonable duration)"""