Skip to content

Instantly share code, notes, and snippets.

@spinningcat
Last active March 4, 2026 09:44
Show Gist options
  • Select an option

  • Save spinningcat/326eaa3b2167d5fa9f4146cbea6aba4d to your computer and use it in GitHub Desktop.

Select an option

Save spinningcat/326eaa3b2167d5fa9f4146cbea6aba4d to your computer and use it in GitHub Desktop.
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