Last active
March 3, 2026 05:42
-
-
Save BexTuychiev/3bf6d7e13887726ac23dedbb2cf8bb94 to your computer and use it in GitHub Desktop.
Invoice generator script for OpenClaw skill tutorial
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Generate a PDF invoice from client details and line items.""" | |
| import argparse, json, os | |
| from datetime import date | |
| from reportlab.lib.pagesizes import letter | |
| from reportlab.lib.colors import HexColor | |
| from reportlab.lib.units import inch | |
| from reportlab.platypus import ( | |
| SimpleDocTemplate, Table, TableStyle, | |
| Spacer, Paragraph, HRFlowable, | |
| ) | |
| from reportlab.lib.styles import ParagraphStyle | |
| DARK = HexColor("#2C3E50") | |
| GRAY = HexColor("#7F8C8D") | |
| WHITE = HexColor("#FFFFFF") | |
| LIGHT_BG = HexColor("#F8F9FA") | |
| def build_invoice(client, items, output_path): | |
| doc = SimpleDocTemplate( | |
| output_path, pagesize=letter, | |
| leftMargin=0.75 * inch, rightMargin=0.75 * inch, | |
| topMargin=0.6 * inch, bottomMargin=0.6 * inch, | |
| ) | |
| s = { | |
| "title": ParagraphStyle("t", fontSize=28, | |
| fontName="Helvetica-Bold", textColor=DARK, leading=34), | |
| "label": ParagraphStyle("l", fontSize=10, | |
| fontName="Helvetica-Bold", textColor=GRAY, leading=14), | |
| "value": ParagraphStyle("v", fontSize=10, | |
| fontName="Helvetica", textColor=DARK, leading=14), | |
| "header": ParagraphStyle("h", fontSize=9, | |
| fontName="Helvetica-Bold", textColor=WHITE, leading=12), | |
| "cell": ParagraphStyle("c", fontSize=10, | |
| fontName="Helvetica", textColor=DARK, leading=14), | |
| "bold": ParagraphStyle("b", fontSize=10, | |
| fontName="Helvetica-Bold", textColor=DARK, leading=14), | |
| "total": ParagraphStyle("tot", fontSize=12, | |
| fontName="Helvetica-Bold", textColor=DARK, leading=16), | |
| } | |
| elements = [Paragraph("INVOICE", s["title"]), Spacer(1, 20)] | |
| # Invoice metadata | |
| today = date.today() | |
| inv_num = f"INV-{today:%Y%m}-001" | |
| meta_data = [ | |
| [Paragraph("<b>Invoice Number:</b>", s["label"]), | |
| Paragraph(inv_num, s["value"])], | |
| [Paragraph("<b>Date:</b>", s["label"]), | |
| Paragraph(f"{today:%B %d, %Y}", s["value"])], | |
| [Paragraph("<b>Due Date:</b>", s["label"]), | |
| Paragraph("Upon Receipt", s["value"])], | |
| ] | |
| meta_table = Table(meta_data, colWidths=[120, 300]) | |
| meta_table.setStyle(TableStyle([ | |
| ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), | |
| ("BOTTOMPADDING", (0, 0), (-1, -1), 4), | |
| ("TOPPADDING", (0, 0), (-1, -1), 4), | |
| ])) | |
| elements.append(meta_table) | |
| elements.append(Spacer(1, 25)) | |
| # From / Bill To | |
| section_header = ParagraphStyle("sh", fontSize=9, | |
| fontName="Helvetica-Bold", textColor=GRAY, leading=12) | |
| from_to_data = [ | |
| [Paragraph("FROM", section_header), | |
| Paragraph("BILL TO", section_header)], | |
| [Paragraph( | |
| "<b>Jane Smith</b><br/>" | |
| "Smith Consulting LLC<br/>" | |
| "123 Main St, Suite 400<br/>" | |
| "San Francisco, CA 94105<br/>" | |
| "jane@smithconsulting.com", s["value"]), | |
| Paragraph( | |
| f"<b>{client}</b><br/>" | |
| "billing@acmecorp.com", s["value"])], | |
| ] | |
| from_to_table = Table(from_to_data, | |
| colWidths=[doc.width / 2.0] * 2) | |
| from_to_table.setStyle(TableStyle([ | |
| ("VALIGN", (0, 0), (-1, -1), "TOP"), | |
| ("TOPPADDING", (0, 0), (-1, -1), 4), | |
| ("BOTTOMPADDING", (0, 0), (-1, -1), 4), | |
| ])) | |
| elements.append(from_to_table) | |
| elements.append(Spacer(1, 30)) | |
| # Line items table | |
| col_w = [doc.width * w for w in (0.50, 0.15, 0.15, 0.20)] | |
| rows = [[Paragraph(h, s["header"]) | |
| for h in ("DESCRIPTION", "HOURS", "RATE", "AMOUNT")]] | |
| subtotal = 0.0 | |
| for it in items: | |
| amt = it["hours"] * it["rate"] | |
| subtotal += amt | |
| rows.append([ | |
| Paragraph(it["description"], s["cell"]), | |
| Paragraph(str(it["hours"]), s["cell"]), | |
| Paragraph(f"${it['rate']:.2f}", s["cell"]), | |
| Paragraph(f"${amt:.2f}", s["cell"]), | |
| ]) | |
| tbl = Table(rows, colWidths=col_w) | |
| tbl.setStyle(TableStyle([ | |
| ("BACKGROUND", (0, 0), (-1, 0), DARK), | |
| ("TEXTCOLOR", (0, 0), (-1, 0), WHITE), | |
| ("ALIGN", (1, 0), (-1, -1), "CENTER"), | |
| ("TOPPADDING", (0, 0), (-1, -1), 10), | |
| ("BOTTOMPADDING", (0, 0), (-1, -1), 10), | |
| ("BACKGROUND", (0, 1), (-1, -1), LIGHT_BG), | |
| ("LINEBELOW", (0, -1), (-1, -1), 0.5, GRAY), | |
| ])) | |
| elements.extend([tbl, Spacer(1, 10)]) | |
| # Summary rows | |
| tax = subtotal * 0.10 | |
| for label, val in [("Subtotal:", subtotal), | |
| ("Tax (10%):", tax)]: | |
| elements.append(Table( | |
| [["", "", Paragraph(f"<b>{label}</b>", s["bold"]), | |
| Paragraph(f"${val:.2f}", s["value"])]], | |
| colWidths=col_w, | |
| )) | |
| elements.append( | |
| HRFlowable(width="100%", thickness=1, color=DARK)) | |
| total = subtotal + tax | |
| elements.append(Table( | |
| [["", "", | |
| Paragraph("<b>TOTAL DUE:</b>", s["total"]), | |
| Paragraph(f"<b>${total:.2f} USD</b>", s["total"])]], | |
| colWidths=col_w, | |
| )) | |
| doc.build(elements) | |
| print(f"Created: {output_path}") | |
| if __name__ == "__main__": | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--client", required=True) | |
| p.add_argument("--items", required=True, | |
| help='JSON: [{"description":"...","hours":N,"rate":N}]') | |
| p.add_argument("--output", required=True) | |
| a = p.parse_args() | |
| build_invoice( | |
| a.client, json.loads(a.items), | |
| os.path.expanduser(a.output), | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment