Last active
March 4, 2026 09:44
-
-
Save spinningcat/326eaa3b2167d5fa9f4146cbea6aba4d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import re | |
| from openpyxl import load_workbook | |
| def rgb_to_hex(rgb): | |
| if not rgb or rgb == '00000000': | |
| return None | |
| return rgb[-6:] | |
| def excel_to_html(excel_file, html_file): | |
| wb = load_workbook(excel_file, data_only=True) | |
| ws = wb.active | |
| # Find header row | |
| header_row = None | |
| for row in range(1, ws.max_row + 1): | |
| if str(ws.cell(row, 2).value).strip() == "Name": | |
| header_row = row | |
| break | |
| if not header_row: return | |
| # Find total row | |
| total_row = None | |
| for row in range(header_row + 1, ws.max_row + 1): | |
| val = ws.cell(row, 1).value | |
| if val and "Total" in str(val): | |
| total_row = row | |
| break | |
| data_start = header_row + 1 | |
| data_end = total_row - 1 if total_row else ws.max_row | |
| max_col = 2 | |
| for col in range(3, ws.max_column + 1): | |
| if ws.cell(header_row, col).value: max_col = col | |
| daily_t = ws['B1'].value or '' | |
| monthly_t = ws['B2'].value or '' | |
| yearly_t = ws['B3'].value or '' | |
| # Base cell style for "Squares" | |
| base_td_style = ( | |
| "padding: 6px 10px; white-space: nowrap; height: 24px; " | |
| "font-family: 'Segoe UI', Tahoma, sans-serif; font-size: 11px; " | |
| "border: 1px solid #e0e0e0; mso-table-lspace: 0pt; mso-table-rspace: 0pt;" | |
| ) | |
| name_col_style = "width: 150px; background-color: #ffffff; border-right: 2px solid #0078d4;" | |
| target_pill_style = "display: inline-block; padding: 4px 12px; margin-right: 8px; border: 1px solid #ddd; font-family: 'Segoe UI', sans-serif; border-radius: 4px;" | |
| def get_cell_attrs(row, col, is_name_col=False): | |
| cell = ws.cell(row, col) | |
| val = cell.value if cell.value is not None else '' | |
| align = 'right' if isinstance(val, (int, float)) else 'left' | |
| bg = f'background-color: #{rgb_to_hex(cell.fill.fgColor.rgb)};' if cell.fill and cell.fill.fgColor and cell.fill.fgColor.rgb and rgb_to_hex(cell.fill.fgColor.rgb) not in [None, 'FFFFFF', '000000'] else '' | |
| bld = 'font-weight: bold;' if cell.font and cell.font.bold else '' | |
| full_style = f"{base_td_style} text-align: {align}; {bg} {bld}" | |
| if is_name_col: | |
| full_style = f"{full_style} {name_col_style}" | |
| return val, full_style | |
| html_lines = [] | |
| html_lines.append('<!DOCTYPE html>') | |
| html_lines.append('<html lang="en" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml">') | |
| html_lines.append('<head>') | |
| html_lines.append('<meta charset="UTF-8">') | |
| html_lines.append('<meta http-equiv="X-UA-Compatible" content="IE=edge">') | |
| html_lines.append('<meta name="viewport" content="width=device-width, initial-scale=1.0">') | |
| # Fix for Outlook spacing | |
| html_lines.append('') | |
| html_lines.append('</head>') | |
| html_lines.append('<body style="margin:0;padding:15px;background-color:#ffffff;font-family:Arial,sans-serif;mso-line-height-rule:exactly;">') | |
| # 1. Targets (Wrapped for Outlook Compatibility) | |
| html_lines.append('<div style="margin-bottom: 15px;">') | |
| html_lines.append('') | |
| html_lines.append(f'<div style="{target_pill_style}"><b>Daily:</b> {daily_t}</div>') | |
| html_lines.append('') | |
| html_lines.append(f'<div style="{target_pill_style}"><b>Monthly:</b> {monthly_t}</div>') | |
| html_lines.append('') | |
| html_lines.append(f'<div style="{target_pill_style}"><b>Yearly:</b> {yearly_t}</div>') | |
| html_lines.append('') | |
| html_lines.append('</div>') | |
| # 2. The Scrolling Table (Bella's Scroll Pattern with Ghost Table) | |
| html_lines.append('<div class="scroll-container" style="width: 100%; max-width: 1500px; overflow-x: auto;">') | |
| # MSO Reset Table - Outlook needs a fixed width wrapper | |
| html_lines.append('') | |
| html_lines.append('<table border="0" cellspacing="0" cellpadding="0" width="1000" style="table-layout: fixed; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt;">') | |
| # Headers | |
| html_lines.append('<tr style="background-color: #fcfcfc;">') | |
| name_h = ws.cell(header_row, 2).value or "Name" | |
| html_lines.append(f'<td style="{base_td_style} {name_col_style} text-align:left; color:#0078d4; font-weight: bold;">{name_h}</td>') | |
| for col in range(3, max_col + 1): | |
| v = ws.cell(header_row, col).value or "" | |
| html_lines.append(f'<td style="{base_td_style} width: 70px; text-align: center; font-weight: bold; color: #0078d4;">{v}</td>') | |
| html_lines.append('</tr>') | |
| # Data Rows | |
| for row in range(data_start, data_end + 1): | |
| if not ws.cell(row, 2).value: continue | |
| html_lines.append('<tr>') | |
| val, style = get_cell_attrs(row, 2, is_name_col=True) | |
| html_lines.append(f'<td style="{style}">{val}</td>') | |
| for col in range(3, max_col + 1): | |
| val, style = get_cell_attrs(row, col) | |
| html_lines.append(f'<td style="{style}">{val}</td>') | |
| html_lines.append('</tr>') | |
| # Total Row | |
| if total_row: | |
| html_lines.append('<tr>') | |
| val, style = get_cell_attrs(total_row, 2, is_name_col=True) | |
| total_style = f"{style} font-weight:bold; border-top: 2px solid #0078d4;" | |
| html_lines.append(f'<td style="{total_style}">{val}</td>') | |
| for col in range(3, max_col + 1): | |
| val, style = get_cell_attrs(total_row, col) | |
| total_data_style = f"{style} font-weight:bold; border-top: 2px solid #0078d4;" | |
| html_lines.append(f'<td style="{total_data_style}">{val}</td>') | |
| html_lines.append('</tr>') | |
| html_lines.append('</table>') | |
| html_lines.append('') | |
| html_lines.append('</div></body></html>') | |
| with open(html_file, 'w', encoding='utf-8') as f: | |
| f.write('\n'.join(html_lines)) | |
| print(f"Outlook-fixed HTML saved to {html_file}") | |
| if __name__ == "__main__": | |
| excel_to_html("billable_feb_26.xlsx", "billable_feb_26.html") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment