exportstatement / tests /generators /make_sample_pdf.py
rudrahf's picture
Claude Opus 4.8 (1M context)
Initial commit — Export Bank Statement extractor (FastAPI)
0ebef7d
Raw
History Blame Contribute Delete
4.42 kB
"""
make_sample_pdf.py — build a realistic bank-statement PDF from the sample xlsx.
This gives us a *test input* (a text-based PDF that looks like a real statement)
so we can prove the Python-first converter works end-to-end.
It reads the transactions out of Sample-CSV-Bank-Statement.xlsx, sorts them
chronologically, recomputes a clean running balance from a $0.00 opening, and
renders a header + a ruled transaction table.
"""
from __future__ import annotations
import datetime as dt
from pathlib import Path
import openpyxl
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
)
HERE = Path(__file__).resolve().parent
XLSX = HERE.parent / "Sample-CSV-Bank-Statement.xlsx"
OUT_PDF = HERE / "sample_statement.pdf"
def _to_float(v):
if v is None or v == "":
return None
try:
return float(str(v).replace(",", "").replace("$", "").strip())
except ValueError:
return None
def load_transactions(xlsx_path: Path):
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
# find the header row (the one containing "Date")
header_idx = next(
i for i, r in enumerate(rows)
if r and any(str(c).strip().lower() == "date" for c in r if c is not None)
)
header = [str(c).strip().lower() if c is not None else "" for c in rows[header_idx]]
col = {name: header.index(name) for name in ("date", "description", "debit", "credit") if name in header}
txns = []
for r in rows[header_idx + 1:]:
if not r or r[col["date"]] is None:
continue
d = r[col["date"]]
if isinstance(d, dt.datetime):
date = d.date()
else:
continue
txns.append({
"date": date,
"description": str(r[col["description"]] or "").strip(),
"debit": _to_float(r[col["debit"]]),
"credit": _to_float(r[col["credit"]]),
})
txns.sort(key=lambda t: t["date"])
return txns
def build_pdf(txns, out_path: Path, opening: float = 0.00):
styles = getSampleStyleSheet()
doc = SimpleDocTemplate(str(out_path), pagesize=A4,
topMargin=18 * mm, bottomMargin=18 * mm,
leftMargin=16 * mm, rightMargin=16 * mm)
# recompute a clean running balance so the statement reconciles
running = opening
table_rows = [["Date", "Description", "Debit", "Credit", "Balance"]]
for t in txns:
running += (t["credit"] or 0.0) - (t["debit"] or 0.0)
table_rows.append([
t["date"].strftime("%d/%m/%Y"),
t["description"],
f"${t['debit']:,.2f}" if t["debit"] else "",
f"${t['credit']:,.2f}" if t["credit"] else "",
f"${running:,.2f} CR",
])
closing = running
period = f"{txns[0]['date'].strftime('%d/%m/%Y')} to {txns[-1]['date'].strftime('%d/%m/%Y')}"
story = [
Paragraph("<b>Macquarie Cash Management Account</b>", styles["Title"]),
Spacer(1, 6),
Paragraph("Account: 123456789 - CASH MANAGEMENT ACCOUNT", styles["Normal"]),
Paragraph("Account Holder: ABC Super Fund", styles["Normal"]),
Paragraph(f"Statement Period: {period}", styles["Normal"]),
Paragraph(f"Opening Balance: ${opening:,.2f}", styles["Normal"]),
Paragraph(f"Closing Balance: ${closing:,.2f}", styles["Normal"]),
Spacer(1, 12),
]
tbl = Table(table_rows, colWidths=[24 * mm, 70 * mm, 24 * mm, 24 * mm, 28 * mm])
tbl.setStyle(TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1f3a5f")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8),
("ALIGN", (2, 0), (-1, -1), "RIGHT"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
]))
story.append(tbl)
doc.build(story)
return closing
if __name__ == "__main__":
txns = load_transactions(XLSX)
closing = build_pdf(txns, OUT_PDF)
print(f"Wrote {OUT_PDF}")
print(f"{len(txns)} transactions, closing balance ${closing:,.2f}")