Update payroll export
This commit is contained in:
@@ -4541,7 +4541,7 @@ def export_payroll_excel():
|
|||||||
excel_file = exporter.create_template_format_report(
|
excel_file = exporter.create_template_format_report(
|
||||||
start_date, end_date, attendance_records, employee_names, include_travel_time, project_name
|
start_date, end_date, attendance_records, employee_names, include_travel_time, project_name
|
||||||
)
|
)
|
||||||
filename_prefix = 'template_hours_report'
|
filename_prefix = 'time_attendance_report'
|
||||||
else:
|
else:
|
||||||
excel_file = exporter.create_payroll_report(
|
excel_file = exporter.create_payroll_report(
|
||||||
start_date, end_date, attendance_records, employee_names, include_travel_time
|
start_date, end_date, attendance_records, employee_names, include_travel_time
|
||||||
|
|||||||
+291
-81
@@ -705,22 +705,22 @@ class PayrollExcelExporter:
|
|||||||
return current_row
|
return current_row
|
||||||
|
|
||||||
def _write_employee_section(self, worksheet, employee_id: str, employee_name: str,
|
def _write_employee_section(self, worksheet, employee_id: str, employee_name: str,
|
||||||
emp_data: Dict, start_date: datetime, end_date: datetime,
|
emp_data: Dict, start_date: datetime, end_date: datetime,
|
||||||
attendance_records: List[Dict], start_row: int) -> int:
|
attendance_records: List[Dict], start_row: int) -> int:
|
||||||
"""Write individual employee section and return next row number"""
|
"""Write individual employee section with multiple pairs per day, daily totals, weekly totals, and grand total"""
|
||||||
current_row = start_row
|
current_row = start_row
|
||||||
|
|
||||||
# Employee info header (merged across columns A-O)
|
# Employee info header (merged across columns A-O)
|
||||||
employee_info = f"Employee ID {employee_id}: {employee_name}"
|
employee_info = f"Employee ID {employee_id}: {employee_name}"
|
||||||
cell = worksheet.cell(row=current_row, column=1, value=employee_info)
|
cell = worksheet.cell(row=current_row, column=1, value=employee_info)
|
||||||
self._apply_style(cell, self.template_employee_style)
|
self._apply_style(cell, self.template_employee_style)
|
||||||
worksheet.merge_cells(f'A{current_row}:Q{current_row}')
|
worksheet.merge_cells(f'A{current_row}:N{current_row}')
|
||||||
current_row += 1
|
current_row += 1
|
||||||
|
|
||||||
# Column headers for this employee
|
# Column headers for this employee
|
||||||
headers = ["Day", "Date", "In", "Out", "Location", "Zone", "Hours/Building",
|
headers = ["Day", "Date", "In", "Out", "Location", "Zone", "Hours/Building",
|
||||||
"Daily Total", "Regular Hours", "OT Hours", "Building Address",
|
"Daily Total", "Regular Hours", "OT Hours", "Building Address",
|
||||||
"Recorded Location", "Distance", "Possible Violation"]
|
"Recorded Location", "Distance", "Possible Violation"]
|
||||||
|
|
||||||
for col, header in enumerate(headers, 1):
|
for col, header in enumerate(headers, 1):
|
||||||
cell = worksheet.cell(row=current_row, column=col, value=header)
|
cell = worksheet.cell(row=current_row, column=col, value=header)
|
||||||
@@ -751,6 +751,20 @@ class PayrollExcelExporter:
|
|||||||
day_info['location'] = sorted_records[0].qr_code.location or ''
|
day_info['location'] = sorted_records[0].qr_code.location or ''
|
||||||
day_info['building_address'] = sorted_records[0].qr_code.location_address or ''
|
day_info['building_address'] = sorted_records[0].qr_code.location_address or ''
|
||||||
|
|
||||||
|
# Initialize weekly totals tracking
|
||||||
|
weekly_regular_hours = 0
|
||||||
|
weekly_ot_hours = 0
|
||||||
|
weekly_total_hours = 0
|
||||||
|
|
||||||
|
# Track overall totals for grand total
|
||||||
|
grand_regular_hours = 0
|
||||||
|
grand_ot_hours = 0
|
||||||
|
grand_total_hours = 0
|
||||||
|
total_pairs_written = 0 # Track total pairs across all days
|
||||||
|
|
||||||
|
# Track week boundaries (assuming payroll period starts on Monday)
|
||||||
|
current_week_start = None
|
||||||
|
|
||||||
# Write data rows using the calculated daily hours from emp_data
|
# Write data rows using the calculated daily hours from emp_data
|
||||||
for date_str in sorted(emp_data['daily_hours'].keys()):
|
for date_str in sorted(emp_data['daily_hours'].keys()):
|
||||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
|
||||||
@@ -760,90 +774,286 @@ class PayrollExcelExporter:
|
|||||||
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) or day_hours_data.get('total_hours', 0) <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Calculate week boundaries
|
||||||
|
week_start = date_obj - timedelta(days=date_obj.weekday()) # Monday of the week
|
||||||
|
|
||||||
|
# Check if we've moved to a new week and need to write weekly total
|
||||||
|
if current_week_start is not None and week_start != current_week_start:
|
||||||
|
# Write weekly total row for previous week
|
||||||
|
current_row = self._write_weekly_total_row(worksheet, current_row,
|
||||||
|
weekly_regular_hours, weekly_ot_hours, weekly_total_hours)
|
||||||
|
|
||||||
|
# Reset weekly counters
|
||||||
|
weekly_regular_hours = 0
|
||||||
|
weekly_ot_hours = 0
|
||||||
|
weekly_total_hours = 0
|
||||||
|
|
||||||
|
current_week_start = week_start
|
||||||
|
|
||||||
total_hours = day_hours_data.get('total_hours', 0)
|
total_hours = day_hours_data.get('total_hours', 0)
|
||||||
regular_hours = min(total_hours, 8.0) # Max 8 regular hours per day
|
regular_hours = min(total_hours, 8.0) # Max 8 regular hours per day
|
||||||
ot_hours = max(0, total_hours - 8.0)
|
ot_hours = max(0, total_hours - 8.0)
|
||||||
|
|
||||||
|
# Add to weekly and grand totals
|
||||||
|
weekly_regular_hours += regular_hours
|
||||||
|
weekly_ot_hours += ot_hours
|
||||||
|
weekly_total_hours += total_hours
|
||||||
|
|
||||||
|
grand_regular_hours += regular_hours
|
||||||
|
grand_ot_hours += ot_hours
|
||||||
|
grand_total_hours += total_hours
|
||||||
|
|
||||||
# Get location info for this date
|
# Get location info for this date
|
||||||
location_info = daily_location_data.get(date_str, {})
|
location_info = daily_location_data.get(date_str, {})
|
||||||
location = location_info.get('location', '')
|
location = location_info.get('location', '')
|
||||||
building_address = location_info.get('building_address', '')
|
building_address = location_info.get('building_address', '')
|
||||||
|
|
||||||
# Get check-in/out times from records
|
# Get all records for this day, sorted by time
|
||||||
|
day_records = []
|
||||||
|
if date_str in daily_location_data:
|
||||||
|
day_records = sorted(daily_location_data[date_str]['records'], key=lambda x: x.check_in_time)
|
||||||
|
|
||||||
|
# Write multiple pairs for the day (like template shows FRIDAY appearing twice)
|
||||||
|
pairs_written = 0
|
||||||
|
if len(day_records) >= 2:
|
||||||
|
# Create pairs from consecutive records
|
||||||
|
for i in range(0, len(day_records) - 1, 2):
|
||||||
|
if i + 1 < len(day_records):
|
||||||
|
start_record = day_records[i]
|
||||||
|
end_record = day_records[i + 1]
|
||||||
|
|
||||||
|
current_row = self._write_record_pair_row(
|
||||||
|
worksheet, current_row, date_obj, start_record, end_record,
|
||||||
|
location, building_address, total_hours, regular_hours, ot_hours,
|
||||||
|
pairs_written, total_hours # Pass total_hours for daily total on last pair
|
||||||
|
)
|
||||||
|
pairs_written += 1
|
||||||
|
total_pairs_written += 1 # Track total pairs
|
||||||
|
else:
|
||||||
|
# Single record or no records - write one row
|
||||||
|
start_record = day_records[0] if day_records else None
|
||||||
|
current_row = self._write_single_record_row(
|
||||||
|
worksheet, current_row, date_obj, start_record,
|
||||||
|
location, building_address, total_hours, regular_hours, ot_hours
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write final weekly total if we have data
|
||||||
|
if weekly_total_hours > 0:
|
||||||
|
current_row = self._write_weekly_total_row(worksheet, current_row,
|
||||||
|
weekly_regular_hours, weekly_ot_hours, weekly_total_hours)
|
||||||
|
|
||||||
|
# Write grand total row
|
||||||
|
current_row = self._write_grand_total_row(worksheet, current_row,
|
||||||
|
grand_regular_hours, grand_ot_hours)
|
||||||
|
|
||||||
|
# Add space between employees
|
||||||
|
current_row += 2
|
||||||
|
|
||||||
|
# Log the export action (use print for now since logger_handler import is complex)
|
||||||
|
print(f"✅ Template format export: Employee {employee_id} ({employee_name}) data written with {total_pairs_written} record pairs, weekly totals, and grand total")
|
||||||
|
|
||||||
|
return current_row
|
||||||
|
|
||||||
|
def _write_record_pair_row(self, worksheet, current_row: int, date_obj: datetime,
|
||||||
|
start_record, end_record, location: str, building_address: str,
|
||||||
|
day_total_hours: float, regular_hours: float, ot_hours: float,
|
||||||
|
pair_index: int, daily_total_hours: float) -> int:
|
||||||
|
"""Write a single record pair row (in/out times)"""
|
||||||
|
day_name = date_obj.strftime('%A').upper()
|
||||||
|
date_str = date_obj.strftime('%m/%d/%Y')
|
||||||
|
|
||||||
|
# Calculate hours for this specific pair
|
||||||
|
if start_record and end_record:
|
||||||
|
start_time = start_record.check_in_time
|
||||||
|
end_time = end_record.check_in_time
|
||||||
|
start_datetime = datetime.combine(start_record.check_in_date, start_time)
|
||||||
|
end_datetime = datetime.combine(end_record.check_in_date, end_time)
|
||||||
|
pair_duration = (end_datetime - start_datetime).total_seconds() / 3600
|
||||||
|
pair_hours = round(pair_duration, 2)
|
||||||
|
else:
|
||||||
|
start_time = start_record.check_in_time if start_record else None
|
||||||
|
end_time = end_record.check_in_time if end_record else None
|
||||||
|
pair_hours = 0
|
||||||
|
|
||||||
|
# Format times
|
||||||
|
check_in_time = start_time.strftime('%I:%M:%S %p') if start_time else ''
|
||||||
|
check_out_time = end_time.strftime('%I:%M:%S %p') if end_time else ''
|
||||||
|
|
||||||
|
# Get location accuracy info
|
||||||
|
location_accuracy = None
|
||||||
|
recorded_location = ''
|
||||||
|
distance_value = ''
|
||||||
|
possible_violation = 'No'
|
||||||
|
|
||||||
|
if start_record and hasattr(start_record, 'location_accuracy'):
|
||||||
|
location_accuracy = getattr(start_record, 'location_accuracy', None)
|
||||||
|
if location_accuracy is not None:
|
||||||
|
try:
|
||||||
|
accuracy_value = float(location_accuracy)
|
||||||
|
distance_value = f"{accuracy_value:.3f}"
|
||||||
|
|
||||||
|
if accuracy_value < 0.3:
|
||||||
|
recorded_location = building_address
|
||||||
|
possible_violation = "No"
|
||||||
|
else:
|
||||||
|
recorded_location = f"GPS Location (Accuracy: {distance_value} miles)"
|
||||||
|
possible_violation = "Yes" if accuracy_value > 0.5 else "Possible"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
distance_value = "Unknown"
|
||||||
|
recorded_location = "GPS Location (Unknown Accuracy)"
|
||||||
|
possible_violation = "Possible"
|
||||||
|
|
||||||
|
# Create building address hyperlink if available
|
||||||
|
building_address_display = building_address
|
||||||
|
if building_address and "," in building_address:
|
||||||
|
# Create Google Maps hyperlink
|
||||||
|
maps_url = f"https://www.google.com/maps/place/{building_address.replace(' ', '+')}"
|
||||||
|
building_address_display = f'=HYPERLINK("{maps_url}","{building_address}")'
|
||||||
|
|
||||||
|
# Create recorded location hyperlink if it's a GPS location
|
||||||
|
recorded_location_display = recorded_location
|
||||||
|
if recorded_location and "GPS Location" not in recorded_location and "," in recorded_location:
|
||||||
|
maps_url = f"https://www.google.com/maps/place/{recorded_location.replace(' ', '+')}"
|
||||||
|
recorded_location_display = f'=HYPERLINK("{maps_url}","{recorded_location}")'
|
||||||
|
|
||||||
|
# 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
|
||||||
|
daily_total_display = daily_total_hours if is_last_pair else ""
|
||||||
|
|
||||||
|
row_data = [
|
||||||
|
day_name, # A - Day
|
||||||
|
date_str, # B - Date
|
||||||
|
check_in_time, # C - In
|
||||||
|
check_out_time, # D - Out
|
||||||
|
location, # E - Location
|
||||||
|
"", # F - Zone (empty)
|
||||||
|
pair_hours, # G - Hours/Building
|
||||||
|
daily_total_display, # H - Daily Total (only on last pair)
|
||||||
|
regular_hours if is_last_pair else "", # I - Regular Hours (only on last pair)
|
||||||
|
ot_hours if is_last_pair else "", # J - OT Hours (only on last pair)
|
||||||
|
building_address_display, # K - Building Address
|
||||||
|
recorded_location_display, # L - Recorded Location
|
||||||
|
distance_value, # M - Distance
|
||||||
|
possible_violation # N - Possible Violation
|
||||||
|
]
|
||||||
|
|
||||||
|
# Write the row
|
||||||
|
for col, value in enumerate(row_data, 1):
|
||||||
|
cell = worksheet.cell(row=current_row, column=col, value=value)
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
|
||||||
|
return current_row + 1
|
||||||
|
|
||||||
|
def _write_single_record_row(self, worksheet, current_row: int, date_obj: datetime,
|
||||||
|
record, location: str, building_address: str,
|
||||||
|
total_hours: float, regular_hours: float, ot_hours: float) -> int:
|
||||||
|
"""Write a single record row when there's only one check-in"""
|
||||||
|
day_name = date_obj.strftime('%A').upper()
|
||||||
|
date_str = date_obj.strftime('%m/%d/%Y')
|
||||||
|
|
||||||
|
# Single record - estimate check-out time
|
||||||
|
if record:
|
||||||
|
check_in_time = record.check_in_time.strftime('%I:%M:%S %p')
|
||||||
|
# Estimate check-out based on total hours
|
||||||
|
check_in_datetime = datetime.combine(record.check_in_date, record.check_in_time)
|
||||||
|
estimated_checkout = check_in_datetime + timedelta(hours=total_hours)
|
||||||
|
check_out_time = estimated_checkout.strftime('%I:%M:%S %p')
|
||||||
|
else:
|
||||||
check_in_time = ''
|
check_in_time = ''
|
||||||
check_out_time = ''
|
check_out_time = ''
|
||||||
if date_str in daily_location_data:
|
|
||||||
records = daily_location_data[date_str]['records']
|
|
||||||
sorted_records = sorted(records, key=lambda x: x.check_in_time)
|
|
||||||
if sorted_records:
|
|
||||||
check_in_time = sorted_records[0].check_in_time.strftime('%I:%M:%S %p')
|
|
||||||
|
|
||||||
# For single check-in system, determine check-out time
|
|
||||||
if len(sorted_records) > 1:
|
|
||||||
check_out_time = sorted_records[-1].check_in_time.strftime('%I:%M:%S %p')
|
|
||||||
else:
|
|
||||||
# Single check-in - estimate check-out based on total hours
|
|
||||||
check_in_datetime = datetime.combine(sorted_records[0].check_in_date, sorted_records[0].check_in_time)
|
|
||||||
estimated_checkout = check_in_datetime + timedelta(hours=total_hours)
|
|
||||||
check_out_time = estimated_checkout.strftime('%I:%M:%S %p')
|
|
||||||
|
|
||||||
location_accuracy = None
|
|
||||||
recorded_location = ''
|
|
||||||
distance_value = ''
|
|
||||||
possible_violation = ''
|
|
||||||
|
|
||||||
# Find attendance record for this date to get location accuracy
|
|
||||||
if date_str in daily_location_data:
|
|
||||||
records = daily_location_data[date_str]['records']
|
|
||||||
if records:
|
|
||||||
# Get location accuracy from the first record of the day
|
|
||||||
location_accuracy = getattr(records[0], 'location_accuracy', None)
|
|
||||||
|
|
||||||
# Determine recorded location based on accuracy (same logic as attendance report)
|
|
||||||
if location_accuracy is not None:
|
|
||||||
try:
|
|
||||||
accuracy_value = float(location_accuracy)
|
|
||||||
distance_value = f"{accuracy_value:.3f}"
|
|
||||||
|
|
||||||
if accuracy_value < 0.3:
|
|
||||||
# High accuracy - use QR code address
|
|
||||||
recorded_location = building_address # This is already the QR location_address
|
|
||||||
possible_violation = "No"
|
|
||||||
else:
|
|
||||||
# Lower accuracy - use actual check-in address
|
|
||||||
recorded_location = getattr(records[0], 'address', '') or ''
|
|
||||||
possible_violation = "Yes"
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
# If accuracy can't be converted, use actual address
|
|
||||||
recorded_location = getattr(records[0], 'address', '') or ''
|
|
||||||
distance_value = 'N/A'
|
|
||||||
possible_violation = 'Unknown'
|
|
||||||
else:
|
|
||||||
# No location accuracy data
|
|
||||||
recorded_location = getattr(records[0], 'address', '') or ''
|
|
||||||
distance_value = 'N/A'
|
|
||||||
possible_violation = 'Unknown'
|
|
||||||
|
|
||||||
row_data = [
|
|
||||||
date_obj.strftime('%A').upper(), # Day name
|
|
||||||
date_obj.strftime('%m/%d/%Y'), # Date
|
|
||||||
check_in_time, # Check in
|
|
||||||
check_out_time, # Check out
|
|
||||||
location, # Location
|
|
||||||
'', # Zone
|
|
||||||
round(total_hours, 2), # Hours/Building
|
|
||||||
round(total_hours, 2), # Daily Total
|
|
||||||
round(regular_hours, 2) if regular_hours > 0 else '', # Regular Hours
|
|
||||||
round(ot_hours, 2) if ot_hours > 0 else '', # OT Hours
|
|
||||||
building_address, # Building Address
|
|
||||||
recorded_location, # Recorded Location
|
|
||||||
distance_value, # Distance
|
|
||||||
possible_violation # Possible Violation
|
|
||||||
]
|
|
||||||
|
|
||||||
for col, value in enumerate(row_data, 1):
|
|
||||||
cell = worksheet.cell(row=current_row, column=col, value=value)
|
|
||||||
self._apply_style(cell, self.template_data_style)
|
|
||||||
current_row += 1
|
|
||||||
|
|
||||||
return current_row
|
# Get location accuracy info (same logic as pair row)
|
||||||
|
location_accuracy = None
|
||||||
|
recorded_location = ''
|
||||||
|
distance_value = ''
|
||||||
|
possible_violation = 'No'
|
||||||
|
|
||||||
|
if record and hasattr(record, 'location_accuracy'):
|
||||||
|
location_accuracy = getattr(record, 'location_accuracy', None)
|
||||||
|
if location_accuracy is not None:
|
||||||
|
try:
|
||||||
|
accuracy_value = float(location_accuracy)
|
||||||
|
distance_value = f"{accuracy_value:.3f}"
|
||||||
|
|
||||||
|
if accuracy_value < 0.3:
|
||||||
|
recorded_location = building_address
|
||||||
|
possible_violation = "No"
|
||||||
|
else:
|
||||||
|
recorded_location = f"GPS Location (Accuracy: {distance_value} miles)"
|
||||||
|
possible_violation = "Yes" if accuracy_value > 0.5 else "Possible"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
distance_value = "Unknown"
|
||||||
|
recorded_location = "GPS Location (Unknown Accuracy)"
|
||||||
|
possible_violation = "Possible"
|
||||||
|
|
||||||
|
row_data = [
|
||||||
|
day_name, # A - Day
|
||||||
|
date_str, # B - Date
|
||||||
|
check_in_time, # C - In
|
||||||
|
check_out_time, # D - Out
|
||||||
|
location, # E - Location
|
||||||
|
"", # F - Zone
|
||||||
|
total_hours, # G - Hours/Building
|
||||||
|
total_hours, # H - Daily Total
|
||||||
|
regular_hours, # I - Regular Hours
|
||||||
|
ot_hours, # J - OT Hours
|
||||||
|
building_address, # K - Building Address
|
||||||
|
recorded_location, # L - Recorded Location
|
||||||
|
distance_value, # M - Distance
|
||||||
|
possible_violation # N - Possible Violation
|
||||||
|
]
|
||||||
|
|
||||||
|
# Write the row
|
||||||
|
for col, value in enumerate(row_data, 1):
|
||||||
|
cell = worksheet.cell(row=current_row, column=col, value=value)
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
|
||||||
|
return current_row + 1
|
||||||
|
|
||||||
|
def _write_weekly_total_row(self, worksheet, current_row: int,
|
||||||
|
weekly_regular: float, weekly_ot: float, weekly_total: float) -> int:
|
||||||
|
"""Write weekly total row matching template format"""
|
||||||
|
|
||||||
|
# Weekly Total row (columns G-J)
|
||||||
|
cell = worksheet.cell(row=current_row, column=7, value="Weekly Total: ") # G
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
cell.font = Font(name="Arial", size=10, bold=True)
|
||||||
|
|
||||||
|
cell = worksheet.cell(row=current_row, column=8, value=round(weekly_total, 2)) # H - Daily Total
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
cell.font = Font(name="Arial", size=10, bold=True)
|
||||||
|
|
||||||
|
cell = worksheet.cell(row=current_row, column=9, value=round(weekly_regular, 2)) # I - Regular Hours
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
cell.font = Font(name="Arial", size=10, bold=True)
|
||||||
|
|
||||||
|
cell = worksheet.cell(row=current_row, column=10, value=round(weekly_ot, 2)) # J - OT Hours
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
cell.font = Font(name="Arial", size=10, bold=True)
|
||||||
|
|
||||||
|
# Log weekly total
|
||||||
|
print(f"✅ Template export: Weekly total written - Regular: {weekly_regular:.2f}, OT: {weekly_ot:.2f}, Total: {weekly_total:.2f}")
|
||||||
|
|
||||||
|
return current_row + 1
|
||||||
|
|
||||||
|
def _write_grand_total_row(self, worksheet, current_row: int,
|
||||||
|
grand_regular: float, grand_ot: float) -> int:
|
||||||
|
"""Write grand total row matching template format"""
|
||||||
|
|
||||||
|
# Grand Total row (columns G, I, J)
|
||||||
|
cell = worksheet.cell(row=current_row, column=7, value="GRAND TOTAL: ") # G
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
cell.font = Font(name="Arial", size=10, bold=True)
|
||||||
|
|
||||||
|
cell = worksheet.cell(row=current_row, column=9, value=round(grand_regular, 2)) # I - Regular Hours
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
cell.font = Font(name="Arial", size=10, bold=True)
|
||||||
|
|
||||||
|
cell = worksheet.cell(row=current_row, column=10, value=round(grand_ot, 2)) # J - OT Hours
|
||||||
|
self._apply_style(cell, self.template_data_style)
|
||||||
|
cell.font = Font(name="Arial", size=10, bold=True)
|
||||||
|
|
||||||
|
# Log grand total
|
||||||
|
print(f"✅ Template export: Grand total written - Regular: {grand_regular:.2f}, OT: {grand_ot:.2f}")
|
||||||
|
|
||||||
|
return current_row + 1
|
||||||
Reference in New Issue
Block a user