"""Generate XLSX: Invoice/billing schedule.""" from pathlib import Path from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter def generate(output_path: Path, rows: list, firm: dict): """Generate an invoice schedule Excel file.""" wb = Workbook() ws = wb.active ws.title = "Invoice Schedule" # Styles header_font = Font(name="Arial", size=12, bold=True, color="003366") col_header_font = Font(name="Arial", size=10, bold=True, color="FFFFFF") col_header_fill = PatternFill( start_color="003366", end_color="003366", fill_type="solid" ) data_font = Font(name="Arial", size=10) currency_format = "#,##0" thin_border = Border( left=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin"), ) # Title ws.merge_cells("A1:G1") ws["A1"] = f"{firm['name']} - Invoice Schedule" ws["A1"].font = header_font ws["A1"].alignment = Alignment(horizontal="center") ws.merge_cells("A2:G2") ws["A2"] = firm["address"] ws["A2"].font = Font(name="Arial", size=8, color="666666") ws["A2"].alignment = Alignment(horizontal="center") # Column headers headers = [ "Client", "Matter Ref", "TM Number", "Description", "Amount (HKD)", "Due Date", "Status", ] col_widths = [25, 15, 14, 40, 15, 14, 12] for i, (header, width) in enumerate(zip(headers, col_widths), start=1): cell = ws.cell(row=4, column=i, value=header) cell.font = col_header_font cell.fill = col_header_fill cell.alignment = Alignment(horizontal="center") cell.border = thin_border ws.column_dimensions[get_column_letter(i)].width = width # Data rows for row_idx, row_data in enumerate(rows, start=5): ws.cell(row=row_idx, column=1, value=row_data["client"]).font = data_font ws.cell(row=row_idx, column=2, value=row_data["matter_ref"]).font = data_font ws.cell(row=row_idx, column=3, value=row_data["tm_number"]).font = data_font ws.cell(row=row_idx, column=4, value=row_data["description"]).font = data_font amount_cell = ws.cell(row=row_idx, column=5, value=row_data["amount_hkd"]) amount_cell.font = data_font amount_cell.number_format = currency_format amount_cell.alignment = Alignment(horizontal="right") ws.cell(row=row_idx, column=6, value=row_data["due_date"]).font = data_font status_cell = ws.cell(row=row_idx, column=7, value=row_data["status"]) status_cell.font = data_font status_cell.alignment = Alignment(horizontal="center") # Color-code status status_colors = { "Draft": "FFF3CD", "Sent": "D1ECF1", "Overdue": "F8D7DA", "Paid": "D4EDDA", } if row_data["status"] in status_colors: status_cell.fill = PatternFill( start_color=status_colors[row_data["status"]], end_color=status_colors[row_data["status"]], fill_type="solid", ) # Apply borders for col in range(1, 8): ws.cell(row=row_idx, column=col).border = thin_border # Total row total_row = 5 + len(rows) ws.cell(row=total_row, column=4, value="TOTAL").font = Font( name="Arial", size=10, bold=True ) total_cell = ws.cell( row=total_row, column=5, value=sum(r["amount_hkd"] for r in rows), ) total_cell.font = Font(name="Arial", size=10, bold=True) total_cell.number_format = currency_format total_cell.alignment = Alignment(horizontal="right") wb.save(str(output_path))