Spaces:
Running
Running
| """ | |
| make_complex_pdf.py — generate a BIG, complex, multi-page bank statement PDF | |
| that must still reconcile and pass the (unedited) converter. | |
| Complexity: ~120 transactions across multiple pages, large comma-separated | |
| amounts, long/messy descriptions, mixed debits & credits. Balances are computed | |
| in integer cents so reconciliation is exact (no float drift). The header row is | |
| repeated on every page so the unedited converter parses every page's table. | |
| """ | |
| from __future__ import annotations | |
| import random | |
| from pathlib import Path | |
| 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 | |
| OUT_PDF = HERE / "complex_statement.pdf" | |
| random.seed(42) | |
| MERCHANTS = [ | |
| "AMAZON MARKETPLACE EU", "WOOLWORTHS 1234 SYDNEY NSW", "UBER *TRIP HELP.UBER.COM", | |
| "DIRECT DEBIT - AGL ENERGY LTD", "SALARY - ACME CORP PTY LTD PAYROLL", | |
| "TFR TO 062-000 12345678 RENT", "PAYPAL *STEAMGAMES 35314369001", | |
| "BPAY To ASIC INV 9920183", "STRIPE TRANSFER ST-X8K2Q9", "ATM WDL CBA GEORGE ST", | |
| "INTEREST PAID - CMA ACCOUNT", "NETFLIX.COM SUBSCRIPTION", "COLES 0456 CHATSWOOD", | |
| "INTL TXN GITHUB.COM USD FEE", "DIVIDEND - VAS ETF DISTRIBUTION", | |
| "TFR FROM SAVINGS 062-111 99887766", "QANTAS AIRWAYS DOMESTIC FARE", | |
| "MEDICARE BENEFIT DEPOSIT", "OPTUS MOBILE POSTPAID 0400111222", | |
| "REFUND - APPLE STORE ONLINE", | |
| ] | |
| REFS = ["REF", "INV", "TXN", "ORD", "PMT", "RCPT"] | |
| def gen_transactions(n=120): | |
| txns = [] | |
| day = 1 | |
| month = 1 | |
| year = 2025 | |
| for i in range(n): | |
| # advance the date a little each row | |
| day += random.randint(0, 3) | |
| if day > 28: | |
| day -= 28 | |
| month += 1 | |
| if month > 12: | |
| month = 1 | |
| year += 1 | |
| merchant = random.choice(MERCHANTS) | |
| ref = f"{random.choice(REFS)}{random.randint(10000, 99999)}" | |
| desc = f"{merchant} {ref}" | |
| is_credit = random.random() < 0.42 | |
| if "SALARY" in merchant or "DIVIDEND" in merchant or "INTEREST" in merchant or "REFUND" in merchant or "BENEFIT" in merchant: | |
| is_credit = True | |
| if "BPAY" in merchant or "ATM" in merchant or "DIRECT DEBIT" in merchant or "RENT" in merchant: | |
| is_credit = False | |
| cents = random.randint(150, 850000) # $1.50 .. $8,500.00 | |
| txns.append({ | |
| "date": f"{day:02d}/{month:02d}/{year}", | |
| "description": desc, | |
| "debit_c": 0 if is_credit else cents, | |
| "credit_c": cents if is_credit else 0, | |
| }) | |
| return txns | |
| def money(cents: int) -> str: | |
| return f"${cents / 100:,.2f}" | |
| def build_pdf(txns, out_path: Path, opening_cents: int = 5_000_000): | |
| styles = getSampleStyleSheet() | |
| doc = SimpleDocTemplate( | |
| str(out_path), pagesize=A4, | |
| topMargin=16 * mm, bottomMargin=16 * mm, | |
| leftMargin=14 * mm, rightMargin=14 * mm, | |
| ) | |
| running = opening_cents | |
| rows = [["Date", "Description", "Debit", "Credit", "Balance"]] | |
| for t in txns: | |
| running += t["credit_c"] - t["debit_c"] | |
| tag = "CR" if running >= 0 else "DR" | |
| rows.append([ | |
| t["date"], | |
| t["description"], | |
| money(t["debit_c"]) if t["debit_c"] else "", | |
| money(t["credit_c"]) if t["credit_c"] else "", | |
| f"{money(abs(running))} {tag}", | |
| ]) | |
| closing = running | |
| period = f"{txns[0]['date']} to {txns[-1]['date']}" | |
| story = [ | |
| Paragraph("<b>Macquarie Cash Management Account</b>", styles["Title"]), | |
| Spacer(1, 6), | |
| Paragraph("Account: 987654321 - BUSINESS CASH MANAGEMENT ACCOUNT", styles["Normal"]), | |
| Paragraph("Account Holder: COMPLEXICORP HOLDINGS PTY LTD", styles["Normal"]), | |
| Paragraph(f"Statement Period: {period}", styles["Normal"]), | |
| Paragraph(f"Opening Balance: {money(opening_cents)}", styles["Normal"]), | |
| Paragraph(f"Closing Balance: {money(closing)}", styles["Normal"]), | |
| Spacer(1, 10), | |
| ] | |
| tbl = Table( | |
| rows, | |
| colWidths=[22 * mm, 78 * mm, 26 * mm, 26 * mm, 30 * mm], | |
| repeatRows=1, # repeat header on every page so each page's table parses | |
| ) | |
| tbl.setStyle(TableStyle([ | |
| ("GRID", (0, 0), (-1, -1), 0.4, 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), 7), | |
| ("ALIGN", (2, 0), (-1, -1), "RIGHT"), | |
| ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), | |
| ])) | |
| story.append(tbl) | |
| doc.build(story) | |
| credits = sum(t["credit_c"] for t in txns) | |
| debits = sum(t["debit_c"] for t in txns) | |
| return closing, credits, debits | |
| if __name__ == "__main__": | |
| txns = gen_transactions(120) | |
| closing, credits, debits = build_pdf(txns, OUT_PDF) | |
| print(f"Wrote {OUT_PDF}") | |
| print(f"{len(txns)} transactions") | |
| print(f"opening $50,000.00 + credits {money(credits)} - debits {money(debits)} = closing {money(closing)}") | |