112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
"""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))
|