Updated exported file information
This commit is contained in:
@@ -7550,19 +7550,25 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
cell.alignment = Alignment(horizontal='center', vertical='center')
|
cell.alignment = Alignment(horizontal='center', vertical='center')
|
||||||
current_row += 1
|
current_row += 1
|
||||||
|
|
||||||
# Group records by date
|
# Group records by date AND location for separate rows per location
|
||||||
daily_location_data = {}
|
daily_location_data = {}
|
||||||
employee_records = [r for r in converted_records if str(r.employee_id) == employee_id]
|
employee_records = [r for r in converted_records if str(r.employee_id) == employee_id]
|
||||||
|
|
||||||
for record in employee_records:
|
for record in employee_records:
|
||||||
date_key = record.check_in_date.strftime('%Y-%m-%d')
|
date_key = record.check_in_date.strftime('%Y-%m-%d')
|
||||||
|
location_key = record.location_name or 'Unknown Location'
|
||||||
|
|
||||||
|
# Create nested structure: date -> location -> records
|
||||||
if date_key not in daily_location_data:
|
if date_key not in daily_location_data:
|
||||||
daily_location_data[date_key] = {
|
daily_location_data[date_key] = {}
|
||||||
|
|
||||||
|
if location_key not in daily_location_data[date_key]:
|
||||||
|
daily_location_data[date_key][location_key] = {
|
||||||
'records': [],
|
'records': [],
|
||||||
'locations': set()
|
'location_name': location_key
|
||||||
}
|
}
|
||||||
daily_location_data[date_key]['records'].append(record)
|
|
||||||
daily_location_data[date_key]['locations'].add(record.location_name)
|
daily_location_data[date_key][location_key]['records'].append(record)
|
||||||
|
|
||||||
# Track weekly hours for overtime calculation
|
# Track weekly hours for overtime calculation
|
||||||
weekly_total_hours = 0
|
weekly_total_hours = 0
|
||||||
@@ -7601,145 +7607,160 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
|
|
||||||
current_week_start = week_start
|
current_week_start = week_start
|
||||||
|
|
||||||
# Get day records
|
# Get all locations for this date
|
||||||
day_records = daily_location_data.get(date_str, {}).get('records', [])
|
date_locations = daily_location_data.get(date_str, {})
|
||||||
sorted_records = sorted(day_records, key=lambda x: x.check_in_time)
|
total_locations = len(date_locations)
|
||||||
|
|
||||||
total_hours = day_data['total_hours']
|
total_hours = day_data['total_hours']
|
||||||
is_miss_punch = day_data.get('is_miss_punch', False)
|
is_miss_punch = day_data.get('is_miss_punch', False)
|
||||||
|
|
||||||
# Check if different locations (login and logout at different places)
|
|
||||||
locations = daily_location_data.get(date_str, {}).get('locations', set())
|
|
||||||
has_different_locations = len(locations) > 1
|
|
||||||
|
|
||||||
# Calculate total hours for the day (even if miss punch)
|
# Calculate total hours for the day (even if miss punch)
|
||||||
if not is_miss_punch and total_hours > 0:
|
if not is_miss_punch and total_hours > 0:
|
||||||
weekly_total_hours += total_hours
|
weekly_total_hours += total_hours
|
||||||
elif is_miss_punch and len(sorted_records) >= 2:
|
elif is_miss_punch and total_locations > 0:
|
||||||
# Calculate hours even for miss punch (different locations)
|
# Calculate hours even for miss punch (different locations)
|
||||||
first_time = sorted_records[0].check_in_time
|
all_records_for_day = []
|
||||||
last_time = sorted_records[-1].check_in_time
|
for loc_data in date_locations.values():
|
||||||
first_datetime = datetime.combine(date_obj, first_time)
|
all_records_for_day.extend(loc_data['records'])
|
||||||
last_datetime = datetime.combine(date_obj, last_time)
|
|
||||||
calculated_hours = (last_datetime - first_datetime).total_seconds() / 3600.0
|
|
||||||
calculated_hours = round(calculated_hours * 4) / 4 # Round to quarter hour
|
|
||||||
weekly_total_hours += calculated_hours
|
|
||||||
total_hours = calculated_hours
|
|
||||||
|
|
||||||
# Calculate precise hours for Hours/Building column
|
if len(all_records_for_day) >= 2:
|
||||||
if len(sorted_records) >= 2 and not is_miss_punch:
|
sorted_all_records = sorted(all_records_for_day, key=lambda x: x.check_in_time)
|
||||||
first_time = sorted_records[0].check_in_time
|
first_time = sorted_all_records[0].check_in_time
|
||||||
last_time = sorted_records[-1].check_in_time
|
last_time = sorted_all_records[-1].check_in_time
|
||||||
first_datetime = datetime.combine(date_obj, first_time)
|
|
||||||
last_datetime = datetime.combine(date_obj, last_time)
|
|
||||||
duration = (last_datetime - first_datetime).total_seconds() / 3600.0
|
|
||||||
precise_hours = round(duration, 2)
|
|
||||||
else:
|
|
||||||
precise_hours = round(total_hours, 2) if total_hours > 0 else 0
|
|
||||||
|
|
||||||
# Round daily total for display
|
first_datetime = datetime.combine(date_obj, first_time)
|
||||||
daily_total_display = round(total_hours * 2) / 2
|
last_datetime = datetime.combine(date_obj, last_time)
|
||||||
if daily_total_display == int(daily_total_display):
|
calculated_hours = (last_datetime - first_datetime).total_seconds() / 3600.0
|
||||||
daily_total_display = int(daily_total_display)
|
calculated_hours = round(calculated_hours, 2)
|
||||||
|
|
||||||
|
weekly_total_hours += calculated_hours
|
||||||
|
total_hours = calculated_hours
|
||||||
|
|
||||||
|
# Daily total display (only shown on last location's last row)
|
||||||
|
daily_total_display = round(total_hours, 2) if total_hours > 0 else ''
|
||||||
|
|
||||||
|
# FIXED: Get all records for the day and sort by time FIRST, then group by location
|
||||||
|
all_day_records = []
|
||||||
|
for loc_data in date_locations.values():
|
||||||
|
all_day_records.extend(loc_data['records'])
|
||||||
|
|
||||||
|
# Sort all records by time chronologically
|
||||||
|
all_day_records_sorted = sorted(all_day_records, key=lambda x: x.check_in_time)
|
||||||
|
|
||||||
|
# Group consecutive records by location while maintaining time order
|
||||||
|
location_groups = []
|
||||||
|
current_location = None
|
||||||
|
current_group = []
|
||||||
|
|
||||||
|
for record in all_day_records_sorted:
|
||||||
|
if current_location is None or record.location_name == current_location:
|
||||||
|
# Same location or first record
|
||||||
|
current_location = record.location_name
|
||||||
|
current_group.append(record)
|
||||||
|
else:
|
||||||
|
# Different location - save current group and start new one
|
||||||
|
if current_group:
|
||||||
|
location_groups.append({
|
||||||
|
'location': current_location,
|
||||||
|
'records': current_group
|
||||||
|
})
|
||||||
|
current_location = record.location_name
|
||||||
|
current_group = [record]
|
||||||
|
|
||||||
|
# Add the last group
|
||||||
|
if current_group:
|
||||||
|
location_groups.append({
|
||||||
|
'location': current_location,
|
||||||
|
'records': current_group
|
||||||
|
})
|
||||||
|
|
||||||
|
# Process each location group in chronological order
|
||||||
|
total_groups = len(location_groups)
|
||||||
|
for group_index, group_data in enumerate(location_groups):
|
||||||
|
location_count = group_index + 1
|
||||||
|
is_last_location = (location_count == total_groups)
|
||||||
|
|
||||||
|
location_name = group_data['location']
|
||||||
|
sorted_records = group_data['records']
|
||||||
|
|
||||||
# HANDLE MISSED PUNCH CASES
|
|
||||||
if is_miss_punch:
|
|
||||||
if len(sorted_records) == 1:
|
if len(sorted_records) == 1:
|
||||||
# Single record - show as Missed Punch in both columns with ORANGE background
|
# Single record for this location
|
||||||
record = sorted_records[0]
|
single_record = sorted_records[0]
|
||||||
row_data = [
|
|
||||||
date_obj.strftime('%A').upper(),
|
|
||||||
date_obj.strftime('%m/%d/%Y'),
|
|
||||||
record.check_in_time.strftime('%I:%M:%S %p'),
|
|
||||||
'', # No check out
|
|
||||||
record.location_name,
|
|
||||||
'',
|
|
||||||
'Missed Punch',
|
|
||||||
'Missed Punch',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
record.event_description or '', # Building Address from event_description
|
|
||||||
record.recorded_address or '',
|
|
||||||
getattr(record, 'distance', None) or '',
|
|
||||||
calculate_possible_violation(getattr(record, 'distance', None))
|
|
||||||
]
|
|
||||||
|
|
||||||
# Single row for this day
|
# Get the original TimeAttendance record to check action_description
|
||||||
day_border = border_day_single
|
original_record = None
|
||||||
|
for rec in records:
|
||||||
|
if (rec.employee_id == single_record.employee_id and
|
||||||
|
rec.attendance_date == single_record.check_in_date and
|
||||||
|
rec.attendance_time == single_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 record
|
||||||
|
day_display = date_obj.strftime('%A').upper() if location_count == 1 else ''
|
||||||
|
date_display = date_obj.strftime('%m/%d/%Y') if location_count == 1 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:
|
||||||
|
# Orphaned check-out
|
||||||
|
row_data = [
|
||||||
|
day_display,
|
||||||
|
date_display,
|
||||||
|
'', # No check-in time
|
||||||
|
single_record.check_in_time.strftime('%I:%M:%S %p'), # Out
|
||||||
|
single_record.location_name,
|
||||||
|
'',
|
||||||
|
'Missed Punch',
|
||||||
|
current_daily_total,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
single_record.event_description or '',
|
||||||
|
single_record.recorded_address or '',
|
||||||
|
getattr(single_record, 'distance', None) or '',
|
||||||
|
calculate_possible_violation(getattr(single_record, 'distance', None))
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# Orphaned check-in
|
||||||
|
row_data = [
|
||||||
|
day_display,
|
||||||
|
date_display,
|
||||||
|
single_record.check_in_time.strftime('%I:%M:%S %p'), # In
|
||||||
|
'', # No check-out time
|
||||||
|
single_record.location_name,
|
||||||
|
'',
|
||||||
|
'Missed Punch',
|
||||||
|
current_daily_total,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
single_record.event_description or '',
|
||||||
|
single_record.recorded_address or '',
|
||||||
|
getattr(single_record, 'distance', None) or '',
|
||||||
|
calculate_possible_violation(getattr(single_record, 'distance', None))
|
||||||
|
]
|
||||||
|
|
||||||
|
day_border = border_day_last if is_last_location 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
|
||||||
# Apply orange background to Missed Punch cells (columns G and H)
|
|
||||||
if col in [7, 8]: # Hours/Building and Daily Total columns
|
|
||||||
cell.fill = missed_punch_fill
|
|
||||||
current_row += 1
|
|
||||||
|
|
||||||
elif has_different_locations:
|
|
||||||
# Different locations - show TWO rows with ORANGE background
|
|
||||||
# First row: Check-in with Missed Punch
|
|
||||||
first_record = sorted_records[0]
|
|
||||||
row_data = [
|
|
||||||
date_obj.strftime('%A').upper(),
|
|
||||||
date_obj.strftime('%m/%d/%Y'),
|
|
||||||
first_record.check_in_time.strftime('%I:%M:%S %p'),
|
|
||||||
'', # Empty Out
|
|
||||||
first_record.location_name,
|
|
||||||
'',
|
|
||||||
'Missed Punch',
|
|
||||||
'', # Empty Daily Total on first row
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
first_record.event_description or '',
|
|
||||||
first_record.recorded_address or '',
|
|
||||||
getattr(first_record, 'distance', None) or '',
|
|
||||||
calculate_possible_violation(getattr(first_record, 'distance', None))
|
|
||||||
]
|
|
||||||
|
|
||||||
day_border_first = border_day_first
|
|
||||||
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_first
|
|
||||||
# Apply orange background to Missed Punch cell (column G)
|
# Apply orange background to Missed Punch cell (column G)
|
||||||
if col == 7:
|
if col == 7:
|
||||||
cell.fill = missed_punch_fill
|
cell.fill = missed_punch_fill
|
||||||
current_row += 1
|
current_row += 1
|
||||||
|
|
||||||
# Second row: Check-out with Missed Punch and Daily Total
|
|
||||||
last_record = sorted_records[-1]
|
|
||||||
row_data = [
|
|
||||||
date_obj.strftime('%A').upper(),
|
|
||||||
date_obj.strftime('%m/%d/%Y'),
|
|
||||||
'', # Empty In
|
|
||||||
last_record.check_in_time.strftime('%I:%M:%S %p'),
|
|
||||||
last_record.location_name,
|
|
||||||
'',
|
|
||||||
'Missed Punch',
|
|
||||||
daily_total_display, # Show calculated 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
|
|
||||||
current_row += 1
|
|
||||||
else:
|
else:
|
||||||
# Multiple records same location - show pairs, only mark unpaired records as Missed Punch
|
# Multiple records for this location - show pairs
|
||||||
# Show pairs in same row (In and Out), last unpaired record gets its own row
|
|
||||||
num_complete_pairs = len(sorted_records) // 2
|
num_complete_pairs = len(sorted_records) // 2
|
||||||
|
|
||||||
# Write complete pairs (2 records per row) - these are NOT missed punches
|
# Write complete pairs (2 records per row)
|
||||||
for i in range(0, num_complete_pairs * 2, 2):
|
for i in range(0, num_complete_pairs * 2, 2):
|
||||||
check_in_record = sorted_records[i]
|
check_in_record = sorted_records[i]
|
||||||
check_out_record = sorted_records[i + 1]
|
check_out_record = sorted_records[i + 1]
|
||||||
@@ -7750,15 +7771,25 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
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)
|
||||||
|
|
||||||
|
# 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 ''
|
||||||
|
|
||||||
row_data = [
|
row_data = [
|
||||||
date_obj.strftime('%A').upper(),
|
day_display,
|
||||||
date_obj.strftime('%m/%d/%Y'),
|
date_display,
|
||||||
check_in_record.check_in_time.strftime('%I:%M:%S %p'), # In
|
check_in_record.check_in_time.strftime('%I:%M:%S %p'), # In
|
||||||
check_out_record.check_in_time.strftime('%I:%M:%S %p'), # Out
|
check_out_record.check_in_time.strftime('%I:%M:%S %p'), # Out
|
||||||
check_in_record.location_name,
|
check_in_record.location_name,
|
||||||
'',
|
'',
|
||||||
pair_hours, # Show calculated hours instead of Missed Punch
|
pair_hours,
|
||||||
'', # Daily Total shown only on last row
|
current_daily_total,
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
check_in_record.event_description or '',
|
check_in_record.event_description or '',
|
||||||
@@ -7767,16 +7798,14 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
calculate_possible_violation(getattr(check_in_record, 'distance', None))
|
calculate_possible_violation(getattr(check_in_record, 'distance', None))
|
||||||
]
|
]
|
||||||
|
|
||||||
# Calculate total rows for this day
|
# Determine border style
|
||||||
total_rows_for_day = num_complete_pairs + (1 if len(sorted_records) % 2 == 1 else 0)
|
is_last_pair_overall = is_last_location and is_last_pair_of_location and not has_unpaired
|
||||||
row_position = i // 2 # Current pair index
|
day_border = border_day_last if is_last_pair_overall else border_day_middle
|
||||||
|
|
||||||
day_border = get_day_border(row_position, total_rows_for_day)
|
|
||||||
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
|
||||||
# No orange background for complete pairs
|
|
||||||
current_row += 1
|
current_row += 1
|
||||||
|
|
||||||
# Write the last unpaired record if odd number of records
|
# Write the last unpaired record if odd number of records
|
||||||
@@ -7792,113 +7821,65 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
original_record = rec
|
original_record = rec
|
||||||
break
|
break
|
||||||
|
|
||||||
# Determine if this is a check-in or check-out based on action_description
|
# Determine if this is a check-in or check-out
|
||||||
is_check_out = False
|
is_check_out = False
|
||||||
if original_record and original_record.action_description:
|
if original_record and original_record.action_description:
|
||||||
action_lower = original_record.action_description.lower()
|
action_lower = original_record.action_description.lower()
|
||||||
is_check_out = 'out' in action_lower or 'checkout' in action_lower
|
is_check_out = 'out' in action_lower or 'checkout' in action_lower
|
||||||
|
|
||||||
# Place time in correct column based on action
|
# 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:
|
if is_check_out:
|
||||||
# Check-out: empty In, time in Out
|
# Unpaired check-out
|
||||||
in_time = ''
|
row_data = [
|
||||||
out_time = last_record.check_in_time.strftime('%I:%M:%S %p')
|
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:
|
else:
|
||||||
# Check-in: time in In, empty Out
|
# Unpaired check-in
|
||||||
in_time = last_record.check_in_time.strftime('%I:%M:%S %p')
|
row_data = [
|
||||||
out_time = ''
|
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))
|
||||||
|
]
|
||||||
|
|
||||||
row_data = [
|
day_border_last = border_day_last
|
||||||
date_obj.strftime('%A').upper(),
|
|
||||||
date_obj.strftime('%m/%d/%Y'),
|
|
||||||
in_time,
|
|
||||||
out_time,
|
|
||||||
last_record.location_name,
|
|
||||||
'',
|
|
||||||
'Missed Punch',
|
|
||||||
daily_total_display, # Show Daily Total on last row
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
last_record.event_description or '',
|
|
||||||
last_record.recorded_address or '',
|
|
||||||
getattr(last_record, 'distance', None) or '',
|
|
||||||
calculate_possible_violation(getattr(last_record, 'distance', None))
|
|
||||||
]
|
|
||||||
|
|
||||||
# This is the last row for this day
|
|
||||||
total_rows_for_day = num_complete_pairs + 1
|
|
||||||
row_position = total_rows_for_day - 1 # Last position
|
|
||||||
|
|
||||||
day_border = get_day_border(row_position, total_rows_for_day)
|
|
||||||
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_last
|
||||||
# Apply orange background to Missed Punch cell (column G)
|
# Apply orange background to Missed Punch cell (column G)
|
||||||
if col == 7:
|
if col == 7:
|
||||||
cell.fill = missed_punch_fill
|
cell.fill = missed_punch_fill
|
||||||
current_row += 1
|
current_row += 1
|
||||||
else:
|
|
||||||
# NORMAL RECORDS (no miss punch)
|
|
||||||
if len(sorted_records) >= 2:
|
|
||||||
for i in range(0, len(sorted_records) - 1, 2):
|
|
||||||
start_record = sorted_records[i]
|
|
||||||
end_record = sorted_records[i + 1]
|
|
||||||
|
|
||||||
row_data = [
|
|
||||||
date_obj.strftime('%A').upper(),
|
|
||||||
date_obj.strftime('%m/%d/%Y'),
|
|
||||||
start_record.check_in_time.strftime('%I:%M:%S %p'),
|
|
||||||
end_record.check_in_time.strftime('%I:%M:%S %p'),
|
|
||||||
start_record.location_name,
|
|
||||||
'',
|
|
||||||
precise_hours if i == 0 else '',
|
|
||||||
daily_total_display if i == 0 else '',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
start_record.event_description or '',
|
|
||||||
start_record.recorded_address or '',
|
|
||||||
getattr(start_record, 'distance', None) or '',
|
|
||||||
calculate_possible_violation(getattr(start_record, 'distance', None))
|
|
||||||
]
|
|
||||||
|
|
||||||
# Calculate total pairs for this day
|
|
||||||
total_pairs = len(sorted_records) // 2
|
|
||||||
pair_index = i // 2
|
|
||||||
|
|
||||||
day_border = get_day_border(pair_index, total_pairs)
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
# Single record (rare case)
|
|
||||||
start_record = sorted_records[0] if sorted_records else None
|
|
||||||
row_data = [
|
|
||||||
date_obj.strftime('%A').upper(),
|
|
||||||
date_obj.strftime('%m/%d/%Y'),
|
|
||||||
start_record.check_in_time.strftime('%I:%M:%S %p') if start_record else '',
|
|
||||||
'',
|
|
||||||
start_record.location_name if start_record else '',
|
|
||||||
'',
|
|
||||||
precise_hours,
|
|
||||||
daily_total_display,
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
start_record.event_description or '' if start_record else '',
|
|
||||||
start_record.recorded_address or '' if start_record else '',
|
|
||||||
getattr(start_record, 'distance', None) if start_record else '',
|
|
||||||
calculate_possible_violation(getattr(start_record, 'distance', None))
|
|
||||||
]
|
|
||||||
|
|
||||||
# Single row for this day
|
|
||||||
day_border = border_day_single
|
|
||||||
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 final weekly total for this employee
|
# Write final weekly total for this employee
|
||||||
if weekly_total_hours > 0:
|
if weekly_total_hours > 0:
|
||||||
|
|||||||
Reference in New Issue
Block a user