Fix Time Attendance exported file format
This commit is contained in:
@@ -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 = [
|
||||||
|
|||||||
@@ -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)"""
|
||||||
|
|||||||
Reference in New Issue
Block a user