Updated the exported file

This commit is contained in:
2025-10-17 16:44:25 -04:00
parent 4964b9936d
commit 076caecacc
2 changed files with 218 additions and 134 deletions
+124 -94
View File
@@ -7388,6 +7388,13 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
# Get distance value from the record # Get distance value from the record
distance_value = getattr(record, 'distance', None) distance_value = getattr(record, 'distance', None)
# CRITICAL: Determine record_type from action_description
record_type = 'check_in' # Default
if hasattr(record, 'action_description') and record.action_description:
action_lower = record.action_description.lower()
if 'out' in action_lower or 'checkout' in action_lower:
record_type = 'check_out'
converted_record = type('Record', (), { converted_record = type('Record', (), {
'id': record.id, 'id': record.id,
'employee_id': str(record.employee_id), 'employee_id': str(record.employee_id),
@@ -7397,8 +7404,10 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
'latitude': None, 'latitude': None,
'longitude': None, 'longitude': None,
'distance': distance_value, 'distance': distance_value,
'event_description': record.event_description or '', # ADD: Building Address 'record_type': record_type, # ADD THIS LINE
'recorded_address': record.recorded_address or '', # ADD: Recorded Location 'action_description': record.action_description, # ADD THIS LINE
'event_description': record.event_description or '',
'recorded_address': record.recorded_address or '',
'qr_code': type('QRCode', (), { 'qr_code': type('QRCode', (), {
'location': record.location_name, 'location': record.location_name,
'location_address': record.recorded_address or '', 'location_address': record.recorded_address or '',
@@ -7757,30 +7766,90 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
current_row += 1 current_row += 1
else: else:
# Multiple records for this location - show pairs # Multiple records for this location - use action_description for pairing
num_complete_pairs = len(sorted_records) // 2 # Determine record types based on action_description
record_info = []
for record in sorted_records:
action_desc = record.action_description.lower() if record.action_description else ''
is_out = 'out' in action_desc or 'checkout' in action_desc
record_info.append({
'record': record,
'is_out': is_out,
'used': False
})
print(f" Record at {record.check_in_time}: action='{record.action_description}', is_out={is_out}")
# Write complete pairs (2 records per row) # Create pairs based on IN -> OUT logic
for i in range(0, num_complete_pairs * 2, 2): pairs_to_write = []
check_in_record = sorted_records[i] i = 0
check_out_record = sorted_records[i + 1] while i < len(record_info):
if record_info[i]['used']:
i += 1
continue
# Calculate hours for this complete pair current_is_out = record_info[i]['is_out']
if not current_is_out: # This is an IN
# Look for next OUT
out_found = False
for j in range(i + 1, len(record_info)):
if record_info[j]['used']:
continue
if record_info[j]['is_out']: # Found an OUT
pairs_to_write.append({
'check_in': record_info[i]['record'],
'check_out': record_info[j]['record'],
'is_miss_punch': (j > i + 1) # Missed punch if not consecutive
})
record_info[i]['used'] = True
record_info[j]['used'] = True
out_found = True
break
if not out_found: # No OUT found for this IN
pairs_to_write.append({
'check_in': record_info[i]['record'],
'check_out': None,
'is_miss_punch': True
})
record_info[i]['used'] = True
else: # This is an orphaned OUT
pairs_to_write.append({
'check_in': None,
'check_out': record_info[i]['record'],
'is_miss_punch': True
})
record_info[i]['used'] = True
i += 1
print(f" Created {len(pairs_to_write)} pairs")
# Write all pairs
for pair_idx, pair_data in enumerate(pairs_to_write):
check_in_record = pair_data['check_in']
check_out_record = pair_data['check_out']
is_miss_punch = pair_data['is_miss_punch']
# Show day name and date only for first pair of first location
day_display = date_obj.strftime('%A').upper() if (location_count == 1 and pair_idx == 0) else ''
date_display = date_obj.strftime('%m/%d/%Y') if (location_count == 1 and pair_idx == 0) else ''
# Calculate hours if complete pair
if check_in_record and check_out_record and not is_miss_punch:
pair_datetime_in = datetime.combine(date_obj, check_in_record.check_in_time) pair_datetime_in = datetime.combine(date_obj, check_in_record.check_in_time)
pair_datetime_out = datetime.combine(date_obj, check_out_record.check_in_time) pair_datetime_out = datetime.combine(date_obj, check_out_record.check_in_time)
pair_hours = (pair_datetime_out - pair_datetime_in).total_seconds() / 3600.0 pair_hours = (pair_datetime_out - pair_datetime_in).total_seconds() / 3600.0
pair_hours = round(pair_hours, 2) pair_hours = round(pair_hours, 2)
else:
pair_hours = 'Missed Punch'
# Show day name and date only for first group's first row # Show daily total on last pair of last location
day_display = date_obj.strftime('%A').upper() if (location_count == 1 and i == 0) else '' is_last_pair = (pair_idx == len(pairs_to_write) - 1) and is_last_location
date_display = date_obj.strftime('%m/%d/%Y') if (location_count == 1 and i == 0) else '' current_daily_total = daily_total_display if is_last_pair else ''
# Show daily total only on last group's last complete pair (if no unpaired record)
is_last_pair_of_location = (i == (num_complete_pairs - 1) * 2)
has_unpaired = (len(sorted_records) % 2 == 1)
show_daily_total = is_last_location and is_last_pair_of_location and not has_unpaired
current_daily_total = daily_total_display if show_daily_total else ''
# Build row data
if check_in_record and check_out_record:
row_data = [ row_data = [
day_display, day_display,
date_display, date_display,
@@ -7797,87 +7866,48 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
getattr(check_in_record, 'distance', None) or '', getattr(check_in_record, 'distance', None) or '',
calculate_possible_violation(getattr(check_in_record, 'distance', None)) calculate_possible_violation(getattr(check_in_record, 'distance', None))
] ]
elif check_in_record: # IN without OUT
row_data = [
day_display,
date_display,
check_in_record.check_in_time.strftime('%I:%M:%S %p'), # In
'', # No OUT
check_in_record.location_name,
'',
'Missed Punch',
current_daily_total,
'',
'',
check_in_record.event_description or '',
check_in_record.recorded_address or '',
getattr(check_in_record, 'distance', None) or '',
calculate_possible_violation(getattr(check_in_record, 'distance', None))
]
else: # OUT without IN
row_data = [
day_display,
date_display,
'', # No IN
check_out_record.check_in_time.strftime('%I:%M:%S %p'), # Out
check_out_record.location_name,
'',
'Missed Punch',
current_daily_total,
'',
'',
check_out_record.event_description or '',
check_out_record.recorded_address or '',
getattr(check_out_record, 'distance', None) or '',
calculate_possible_violation(getattr(check_out_record, 'distance', None))
]
# Determine border style day_border = border_day_last if is_last_pair else border_day_middle
is_last_pair_overall = is_last_location and is_last_pair_of_location and not has_unpaired
day_border = border_day_last if is_last_pair_overall else border_day_middle
for col, value in enumerate(row_data, 1): for col, value in enumerate(row_data, 1):
cell = ws.cell(row=current_row, column=col, value=value) cell = ws.cell(row=current_row, column=col, value=value)
cell.font = data_font cell.font = data_font
cell.border = day_border cell.border = day_border
current_row += 1 # Apply orange background to Missed Punch cell
if col == 7 and value == 'Missed Punch':
# Write the last unpaired record if odd number of records
if len(sorted_records) % 2 == 1:
last_record = sorted_records[-1]
# Get the original TimeAttendance record to check action_description
original_record = None
for rec in records:
if (rec.employee_id == last_record.employee_id and
rec.attendance_date == last_record.check_in_date and
rec.attendance_time == last_record.check_in_time):
original_record = rec
break
# Determine if this is a check-in or check-out
is_check_out = False
if original_record and original_record.action_description:
action_lower = original_record.action_description.lower()
is_check_out = 'out' in action_lower or 'checkout' in action_lower
# Show day name and date only for first group's first row
day_display = date_obj.strftime('%A').upper() if (location_count == 1 and num_complete_pairs == 0) else ''
date_display = date_obj.strftime('%m/%d/%Y') if (location_count == 1 and num_complete_pairs == 0) else ''
# Show daily total only if this is the last group
current_daily_total = daily_total_display if is_last_location else ''
if is_check_out:
# Unpaired check-out
row_data = [
day_display,
date_display,
'', # No check-in
last_record.check_in_time.strftime('%I:%M:%S %p'), # Out
last_record.location_name,
'',
'Missed Punch',
current_daily_total,
'',
'',
last_record.event_description or '',
last_record.recorded_address or '',
getattr(last_record, 'distance', None) or '',
calculate_possible_violation(getattr(last_record, 'distance', None))
]
else:
# Unpaired check-in
row_data = [
day_display,
date_display,
last_record.check_in_time.strftime('%I:%M:%S %p'), # In
'', # No check-out
last_record.location_name,
'',
'Missed Punch',
current_daily_total,
'',
'',
last_record.event_description or '',
last_record.recorded_address or '',
getattr(last_record, 'distance', None) or '',
calculate_possible_violation(getattr(last_record, 'distance', None))
]
day_border_last = border_day_last
for col, value in enumerate(row_data, 1):
cell = ws.cell(row=current_row, column=col, value=value)
cell.font = data_font
cell.border = day_border_last
# Apply orange background to Missed Punch cell (column G)
if col == 7:
cell.fill = missed_punch_fill cell.fill = missed_punch_fill
current_row += 1 current_row += 1
+69 -15
View File
@@ -151,7 +151,16 @@ class RecordPairBuilder:
@staticmethod @staticmethod
def build_pairs_from_records(records: List[AttendanceRecord]) -> List[RecordPair]: def build_pairs_from_records(records: List[AttendanceRecord]) -> List[RecordPair]:
"""Build check-in/check-out pairs from attendance records""" """
Build check-in/check-out pairs from attendance records with missed punch handling
Pairing Rules:
- Normal: IN - OUT - IN - OUT (2 pairs)
- Missed punch IN-IN-OUT: First IN to OUT (1 pair, marked as missed punch)
- Missed punch IN-OUT-OUT: IN to last OUT (1 pair, marked as missed punch)
- General: If next to first IN isn't OUT, or before OUT isn't IN,
pair from first IN to last OUT
"""
if not records: if not records:
return [] return []
@@ -163,35 +172,65 @@ class RecordPairBuilder:
while i < len(sorted_records): while i < len(sorted_records):
current_record = sorted_records[i] current_record = sorted_records[i]
# Look for matching check-out record # Determine if current record is check-in or check-out
is_check_in = current_record.record_type == 'check_in'
if is_check_in:
# Found a check-in, now look for matching check-out
check_out_record = None check_out_record = None
if i + 1 < len(sorted_records): is_miss_punch = False
next_record = sorted_records[i + 1] j = i + 1
# Simple pairing: assume alternating check-in/check-out
if current_record.record_type == 'check_in' and next_record.record_type == 'check_out': # Look ahead for the matching OUT
while j < len(sorted_records):
next_record = sorted_records[j]
if next_record.record_type == 'check_out':
# Found a check-out
check_out_record = next_record check_out_record = next_record
i += 2 # Skip both records
else: # Check if there's a missed punch between IN and OUT
i += 1 # (if j > i+1, there are records in between)
if j > i + 1:
is_miss_punch = True
# Log missed punch detection
print(f"⚠️ Missed punch detected: IN at {current_record.timestamp} paired with OUT at {check_out_record.timestamp} (skipped {j-i-1} record(s))")
i = j + 1 # Move past this OUT
break
else: else:
# It's another IN, keep looking
j += 1
# If no OUT found, it's an incomplete pair (missed punch)
if check_out_record is None:
is_miss_punch = True
print(f"⚠️ Incomplete pair: IN at {current_record.timestamp} has no matching OUT")
i += 1 i += 1
# Create pair # Create the pair
if current_record.record_type == 'check_in':
pair = RecordPair( pair = RecordPair(
check_in=current_record, check_in=current_record,
check_out=check_out_record, check_out=check_out_record,
is_miss_punch=(check_out_record is None) is_miss_punch=is_miss_punch
) )
pairs.append(pair)
else: else:
# Orphaned check-out # Orphaned check-out (OUT without preceding IN)
print(f"⚠️ Orphaned check-out at {current_record.timestamp} (no preceding IN)")
pair = RecordPair( pair = RecordPair(
check_in=None, check_in=None,
check_out=current_record, check_out=current_record,
is_miss_punch=True is_miss_punch=True
) )
pairs.append(pair) pairs.append(pair)
i += 1
# Log pairing summary
total_pairs = len(pairs)
missed_pairs = sum(1 for p in pairs if p.is_miss_punch)
print(f"📊 Pairing complete: {total_pairs} total pairs, {missed_pairs} with missed punches")
return pairs return pairs
@@ -237,13 +276,28 @@ class WorkingHoursCalculator:
record_id = record['id'] record_id = record['id']
if emp_id == employee_id: if emp_id == employee_id:
# Determine record_type from action_description if available
record_type = 'check_in' # Default
if hasattr(record, 'action_description'):
action_desc = getattr(record, 'action_description', '')
if action_desc:
action_lower = str(action_desc).lower()
if 'out' in action_lower or 'checkout' in action_lower:
record_type = 'check_out'
elif isinstance(record, dict) and 'action_description' in record:
action_desc = record.get('action_description', '')
if action_desc:
action_lower = str(action_desc).lower()
if 'out' in action_lower or 'checkout' in action_lower:
record_type = 'check_out'
att_record = AttendanceRecord( att_record = AttendanceRecord(
id=record_id, id=record_id,
employee_id=emp_id, employee_id=emp_id,
check_in_date=date_val, check_in_date=date_val,
check_in_time=time_val, check_in_time=time_val,
location_name=location, location_name=location,
record_type='check_in' # Default, could be enhanced record_type=record_type # Now properly determined
) )
records.append(att_record) records.append(att_record)