Updated the exported file
This commit is contained in:
@@ -7388,6 +7388,13 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
||||
# Get distance value from the record
|
||||
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', (), {
|
||||
'id': record.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,
|
||||
'longitude': None,
|
||||
'distance': distance_value,
|
||||
'event_description': record.event_description or '', # ADD: Building Address
|
||||
'recorded_address': record.recorded_address or '', # ADD: Recorded Location
|
||||
'record_type': record_type, # ADD THIS LINE
|
||||
'action_description': record.action_description, # ADD THIS LINE
|
||||
'event_description': record.event_description or '',
|
||||
'recorded_address': record.recorded_address or '',
|
||||
'qr_code': type('QRCode', (), {
|
||||
'location': record.location_name,
|
||||
'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
|
||||
|
||||
else:
|
||||
# Multiple records for this location - show pairs
|
||||
num_complete_pairs = len(sorted_records) // 2
|
||||
# Multiple records for this location - use action_description for pairing
|
||||
# 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)
|
||||
for i in range(0, num_complete_pairs * 2, 2):
|
||||
check_in_record = sorted_records[i]
|
||||
check_out_record = sorted_records[i + 1]
|
||||
# Create pairs based on IN -> OUT logic
|
||||
pairs_to_write = []
|
||||
i = 0
|
||||
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_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 = round(pair_hours, 2)
|
||||
else:
|
||||
pair_hours = 'Missed Punch'
|
||||
|
||||
# Show day name and date only for first group's first row
|
||||
day_display = date_obj.strftime('%A').upper() if (location_count == 1 and i == 0) else ''
|
||||
date_display = date_obj.strftime('%m/%d/%Y') if (location_count == 1 and i == 0) 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 ''
|
||||
# Show daily total on last pair of last location
|
||||
is_last_pair = (pair_idx == len(pairs_to_write) - 1) and is_last_location
|
||||
current_daily_total = daily_total_display if is_last_pair else ''
|
||||
|
||||
# Build row data
|
||||
if check_in_record and check_out_record:
|
||||
row_data = [
|
||||
day_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 '',
|
||||
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
|
||||
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
|
||||
|
||||
day_border = border_day_last if is_last_pair else border_day_middle
|
||||
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
|
||||
current_row += 1
|
||||
|
||||
# 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:
|
||||
# Apply orange background to Missed Punch cell
|
||||
if col == 7 and value == 'Missed Punch':
|
||||
cell.fill = missed_punch_fill
|
||||
current_row += 1
|
||||
|
||||
|
||||
+69
-15
@@ -151,7 +151,16 @@ class RecordPairBuilder:
|
||||
|
||||
@staticmethod
|
||||
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:
|
||||
return []
|
||||
|
||||
@@ -163,35 +172,65 @@ class RecordPairBuilder:
|
||||
while i < len(sorted_records):
|
||||
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
|
||||
if i + 1 < len(sorted_records):
|
||||
next_record = sorted_records[i + 1]
|
||||
# Simple pairing: assume alternating check-in/check-out
|
||||
if current_record.record_type == 'check_in' and next_record.record_type == 'check_out':
|
||||
is_miss_punch = False
|
||||
j = i + 1
|
||||
|
||||
# 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
|
||||
i += 2 # Skip both records
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# Check if there's a missed punch between IN and OUT
|
||||
# (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:
|
||||
# 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
|
||||
|
||||
# Create pair
|
||||
if current_record.record_type == 'check_in':
|
||||
# Create the pair
|
||||
pair = RecordPair(
|
||||
check_in=current_record,
|
||||
check_out=check_out_record,
|
||||
is_miss_punch=(check_out_record is None)
|
||||
is_miss_punch=is_miss_punch
|
||||
)
|
||||
pairs.append(pair)
|
||||
|
||||
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(
|
||||
check_in=None,
|
||||
check_out=current_record,
|
||||
is_miss_punch=True
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -237,13 +276,28 @@ class WorkingHoursCalculator:
|
||||
record_id = record['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(
|
||||
id=record_id,
|
||||
employee_id=emp_id,
|
||||
check_in_date=date_val,
|
||||
check_in_time=time_val,
|
||||
location_name=location,
|
||||
record_type='check_in' # Default, could be enhanced
|
||||
record_type=record_type # Now properly determined
|
||||
)
|
||||
records.append(att_record)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user