Invoice & Document PDF Generation
Generate professional business documents — invoices, contracts, receipts, forms — as polished PDFs. This skill covers templates, data-driven generation, batch processing, digital signatures, and workflow automation.
Invoice PDF Structure
A professional invoice follows a standard layout that makes it easy to process, pay, and reconcile.
Standard Invoice Anatomy
┌───────────────────────────────────────────┐
│ [LOGO] INVOICE │
│ Your Company #INV-2025-0042 │
│ 123 Business Rd │
│ City, State ZIP │
├─────────────────┬─────────────────────────┤
│ Bill To: │ Invoice Details: │
│ Client Name │ Date: 2025-01-15 │
│ Client Address │ Due: 2025-02-14 │
│ City, State │ Terms: Net 30 │
│ │ PO #: PO-2025-001 │
├─────────────────┴─────────────────────────┤
│ # │ Description │ Qty │ Rate│ Amt │
│ ───┼───────────────────┼─────┼─────┼─────┤
│ 1 │ Web Development │ 40 │ 150 │$6,000│
│ 2 │ UI/UX Design │ 20 │ 125 │$2,500│
│ 3 │ DevOps Setup │ 8 │ 175 │$1,400│
├────────────────────────┴─────┴─────┴─────┤
│ Subtotal: $9,900 │
│ Tax (8%): $792 │
│ Discount (5%): -$495 │
│ Total: $10,197 │
├───────────────────────────────────────────┤
│ Payment Information: │
│ Bank: First National Bank │
│ Account: XXXX-XXXX-1234 │
│ Routing: 021000021 │
│ PayPal: pay@company.com │
├───────────────────────────────────────────┤
│ Terms & Notes: │
│ Payment due within 30 days. │
│ Late payment subject to 1.5%/mo fee. │
└───────────────────────────────────────────┘
Invoice Data Model
"""Invoice data model with validation."""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Optional, list
import uuid
@dataclass
class LineItem:
"""A single line item on an invoice."""
description: str
quantity: Decimal
unit_price: Decimal
sku: Optional[str] = None
@property
def amount(self) -> Decimal:
return self.quantity * self.unit_price
@dataclass
class InvoiceData:
"""Complete invoice data structure."""
# Invoice identifiers
invoice_number: str
po_number: Optional[str] = None
# Dates
issue_date: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d"))
due_date: Optional[str] = None
payment_terms: str = "Net 30"
# Seller (Your company)
seller_name: str = ""
seller_address: str = ""
seller_city: str = ""
seller_state: str = ""
seller_zip: str = ""
seller_phone: str = ""
seller_email: str = ""
seller_logo_path: Optional[str] = None
tax_id: Optional[str] = None
# Buyer (Client)
client_name: str = ""
client_address: str = ""
client_city: str = ""
client_state: str = ""
client_zip: str = ""
client_email: Optional[str] = None
# Line items
line_items: list[LineItem] = field(default_factory=list)
# Financial
tax_rate: Decimal = Decimal("0")
discount_rate: Decimal = Decimal("0")
discount_description: str = "Discount"
currency_symbol: str = "$"
# Payment
bank_name: Optional[str] = None
bank_account: Optional[str] = None
bank_routing: Optional[str] = None
payment_instructions: Optional[str] = None
# Notes
notes: Optional[str] = None
terms: Optional[str] = None
def __post_init__(self):
if not self.due_date:
due = datetime.now() + timedelta(days=30)
self.due_date = due.strftime("%Y-%m-%d")
if not self.invoice_number:
self.invoice_number = f"INV-{datetime.now().strftime('%Y%m')}-{uuid.uuid4().hex[:6].upper()}"
@property
def subtotal(self) -> Decimal:
return sum(item.amount for item in self.line_items)
@property
def tax_amount(self) -> Decimal:
return self.subtotal * self.tax_rate
@property
def discount_amount(self) -> Decimal:
return self.subtotal * self.discount_rate
@property
def total(self) -> Decimal:
return self.subtotal + self.tax_amount - self.discount_amount
2. HTML/CSS Print Templates for Invoices
Invoice Print Template
<!-- templates/invoice.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Invoice {{ invoice.invoice_number }}</title>
<style>
/* Page setup */
@page {
size: A4;
margin: 1.5cm 2cm 2cm 2cm;
@bottom-center {
content: "Invoice {{ invoice.invoice_number }} — Page " counter(page);
font-size: 8pt;
color: #999;
font-family: 'Helvetica', 'Arial', sans-serif;
}
}
body {
font-family: 'Helvetica', 'Arial', sans-serif;
font-size: 9.5pt;
color: #333;
line-height: 1.5;
margin: 0;
padding: 0;
}
/* Header section */
.invoice-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 2cm;
border-bottom: 3px solid #1a1a2e;
padding-bottom: 0.5cm;
}
.invoice-header .seller-info {
flex: 1;
}
.invoice-header .seller-info h1 {
font-size: 11pt;
color: #1a1a2e;
margin: 0 0 4px 0;
font-weight: bold;
}
.invoice-header .seller-info p {
margin: 1px 0;
font-size: 9pt;
color: #666;
}
.invoice-header .invoice-title {
text-align: right;
}
.invoice-header .invoice-title h2 {
font-size: 24pt;
color: #1a1a2e;
margin: 0;
letter-spacing: 3px;
text-transform: uppercase;
}
.invoice-header .invoice-title .invoice-number {
font-size: 12pt;
color: #e94560;
font-weight: bold;
margin-top: 4px;
}
.invoice-header .invoice-title .invoice-number span {
color: #333;
font-weight: normal;
}
/* Billing section */
.billing-section {
display: flex;
justify-content: space-between;
margin-bottom: 1.5cm;
}
.bill-to, .invoice-details {
width: 45%;
}
.bill-to h3, .invoice-details h3 {
font-size: 9pt;
color: #999;
text-transform: uppercase;
letter-spacing: 1px;
margin: 0 0 6px 0;
border-bottom: 1px solid #ddd;
padding-bottom: 4px;
}
.bill-to p, .invoice-details p {
margin: 2px 0;
font-size: 9pt;
}
.invoice-details table {
width: 100%;
border-collapse: collapse;
}
.invoice-details td {
padding: 2px 0;
font-size: 9pt;
}
.invoice-details td:first-child {
color: #999;
width: 40%;
}
.invoice-details td:last-child {
text-align: right;
font-weight: bold;
}
/* Line items table */
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1cm;
}
.items-table thead th {
background: #1a1a2e;
color: white;
padding: 8px 10px;
font-size: 8.5pt;
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: left;
}
.items-table thead th:last-child {
text-align: right;
}
.items-table thead th:nth-child(3),
.items-table thead th:nth-child(4) {
text-align: right;
}
.items-table tbody td {
padding: 8px 10px;
border-bottom: 1px solid #eee;
font-size: 9pt;
}
.items-table tbody td:last-child {
text-align: right;
font-weight: bold;
}
.items-table tbody td:nth-child(3),
.items-table tbody td:nth-child(4) {
text-align: right;
}
.items-table tbody tr:nth-child(even) {
background: #f8f9fa;
}
.items-table tbody tr:last-child td {
border-bottom: 2px solid #1a1a2e;
}
/* Totals section */
.totals-section {
display: flex;
justify-content: flex-end;
margin-bottom: 1.5cm;
}
.totals-table {
width: 40%;
border-collapse: collapse;
}
.totals-table td {
padding: 4px 10px;
font-size: 9pt;
}
.totals-table td:first-child {
text-align: right;
color: #666;
width: 50%;
}
.totals-table td:last-child {
text-align: right;
font-weight: bold;
width: 50%;
}
.totals-table .total-row td {
border-top: 2px solid #1a1a2e;
font-size: 11pt;
font-weight: bold;
padding-top: 8px;
color: #1a1a2e;
}
.totals-table .discount-row td {
color: #e94560;
}
/* Payment section */
.payment-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: 12px 16px;
margin-bottom: 1cm;
page-break-inside: avoid;
}
.payment-section h3 {
font-size: 9pt;
color: #1a1a2e;
text-transform: uppercase;
letter-spacing: 1px;
margin: 0 0 8px 0;
}
.payment-section p {
margin: 2px 0;
font-size: 8.5pt;
}
/* Notes section */
.notes-section {
font-size: 8.5pt;
color: #666;
margin-top: 1cm;
border-top: 1px solid #ddd;
padding-top: 0.5cm;
}
.notes-section h3 {
font-size: 8pt;
text-transform: uppercase;
letter-spacing: 1px;
color: #999;
margin: 0 0 4px 0;
}
/* Logo */
.logo {
max-height: 1.5cm;
max-width: 4cm;
}
/* QR Code placement */
.qr-code {
position: absolute;
top: 1cm;
right: 2cm;
width: 2cm;
height: 2cm;
}
</style>
</head>
<body>
<div class="invoice-header">
<div class="seller-info">
{% if invoice.seller_logo_path %}
<img src="{{ invoice.seller_logo_path }}" class="logo" alt="Logo">
{% endif %}
<h1>{{ invoice.seller_name }}</h1>
<p>{{ invoice.seller_address }}</p>
<p>{{ invoice.seller_city }}, {{ invoice.seller_state }} {{ invoice.seller_zip }}</p>
{% if invoice.seller_phone %}<p>Phone: {{ invoice.seller_phone }}</p>{% endif %}
{% if invoice.seller_email %}<p>Email: {{ invoice.seller_email }}</p>{% endif %}
{% if invoice.tax_id %}<p>Tax ID: {{ invoice.tax_id }}</p>{% endif %}
</div>
<div class="invoice-title">
<h2>Invoice</h2>
<div class="invoice-number">#<span>{{ invoice.invoice_number }}</span></div>
</div>
</div>
<div class="billing-section">
<div class="bill-to">
<h3>Bill To</h3>
<p><strong>{{ invoice.client_name }}</strong></p>
<p>{{ invoice.client_address }}</p>
<p>{{ invoice.client_city }}, {{ invoice.client_state }} {{ invoice.client_zip }}</p>
{% if invoice.client_email %}<p>{{ invoice.client_email }}</p>{% endif %}
</div>
<div class="invoice-details">
<h3>Invoice Details</h3>
<table>
<tr><td>Invoice Date</td><td>{{ invoice.issue_date }}</td></tr>
<tr><td>Due Date</td><td>{{ invoice.due_date }}</td></tr>
<tr><td>Payment Terms</td><td>{{ invoice.payment_terms }}</td></tr>
{% if invoice.po_number %}
<tr><td>PO Number</td><td>{{ invoice.po_number }}</td></tr>
{% endif %}
</table>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th style="width: 5%;">#</th>
<th style="width: 45%;">Description</th>
<th style="width: 10%;">Qty</th>
<th style="width: 15%;">Rate</th>
<th style="width: 15%;">Amount</th>
</tr>
</thead>
<tbody>
{% for item in invoice.line_items %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ item.description }}{% if item.sku %}<br><small>SKU: {{ item.sku }}</small>{% endif %}</td>
<td>{{ item.quantity }}</td>
<td>{{ invoice.currency_symbol }}{{ "{:,.2f}".format(item.unit_price) }}</td>
<td>{{ invoice.currency_symbol }}{{ "{:,.2f}".format(item.amount) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="totals-section">
<table class="totals-table">
<tr><td>Subtotal</td><td>{{ invoice.currency_symbol }}{{ "{:,.2f}".format(invoice.subtotal) }}</td></tr>
{% if invoice.discount_rate > 0 %}
<tr class="discount-row">
<td>{{ invoice.discount_description }} ({{ "{:.0%}".format(invoice.discount_rate) }})</td>
<td>-{{ invoice.currency_symbol }}{{ "{:,.2f}".format(invoice.discount_amount) }}</td>
</tr>
{% endif %}
{% if invoice.tax_rate > 0 %}
<tr><td>Tax ({{ "{:.0%}".format(invoice.tax_rate) }})</td><td>{{ invoice.currency_symbol }}{{ "{:,.2f}".format(invoice.tax_amount) }}</td></tr>
{% endif %}
<tr class="total-row"><td>Total</td><td>{{ invoice.currency_symbol }}{{ "{:,.2f}".format(invoice.total) }}</td></tr>
</table>
</div>
<div class="payment-section">
<h3>Payment Information</h3>
{% if invoice.bank_name %}<p><strong>Bank:</strong> {{ invoice.bank_name }}</p>{% endif %}
{% if invoice.bank_account %}<p><strong>Account:</strong> {{ invoice.bank_account }}</p>{% endif %}
{% if invoice.bank_routing %}<p><strong>Routing:</strong> {{ invoice.bank_routing }}</p>{% endif %}
{% if invoice.payment_instructions %}<p>{{ invoice.payment_instructions }}</p>{% endif %}
</div>
<div class="notes-section">
{% if invoice.notes %}
<h3>Notes</h3>
<p>{{ invoice.notes }}</p>
{% endif %}
{% if invoice.terms %}
<h3>Terms</h3>
<p>{{ invoice.terms }}</p>
{% endif %}
</div>
</body>
</html>
3. Python Invoice Generation
ReportLab Invoice Generator
"""Professional invoice generation with ReportLab."""
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm, mm
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
Image, PageBreak, KeepTogether
)
from reportlab.lib.enums import TA_RIGHT, TA_LEFT, TA_CENTER
from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime, timedelta
class InvoiceGenerator:
"""Generate professional invoice PDFs using ReportLab."""
def __init__(self, company_info: dict):
self.company = company_info
self.styles = getSampleStyleSheet()
self._setup_styles()
def _setup_styles(self):
"""Set up custom paragraph styles."""
self.styles.add(ParagraphStyle(
'InvoiceTitle',
fontName='Helvetica-Bold',
fontSize=28,
textColor=colors.HexColor('#1a1a2e'),
alignment=TA_RIGHT,
spaceAfter=4,
))
self.styles.add(ParagraphStyle(
'CompanyName',
fontName='Helvetica-Bold',
fontSize=11,
textColor=colors.HexColor('#1a1a2e'),
spaceAfter=2,
))
self.styles.add(ParagraphStyle(
'SmallText',
fontName='Helvetica',
fontSize=8.5,
textColor=colors.HexColor('#666666'),
spaceAfter=1,
))
self.styles.add(ParagraphStyle(
'SectionLabel',
fontName='Helvetica-Bold',
fontSize=8,
textColor=colors.HexColor('#999999'),
spaceBefore=8,
spaceAfter=4,
))
def _header_table(self, invoice_data: dict) -> Table:
"""Create the invoice header with company info and title."""
# Company info
company_cells = [
[Paragraph(self.company['name'], self.styles['CompanyName'])],
[Paragraph(self.company['address'], self.styles['SmallText'])],
[Paragraph(
f"{self.company['city']}, {self.company['state']} {self.company['zip']}",
self.styles['SmallText']
)],
]
if self.company.get('phone'):
company_cells.append([Paragraph(f"Phone: {self.company['phone']}", self.styles['SmallText'])])
if self.company.get('email'):
company_cells.append([Paragraph(f"Email: {self.company['email']}", self.styles['SmallText'])])
company_table = Table(company_cells, colWidths=[8*cm])
company_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 1),
('BOTTOMPADDING', (0, 0), (-1, -1), 1),
]))
# Invoice title
title_cells = [
[Paragraph('INVOICE', self.styles['InvoiceTitle'])],
[Paragraph(f"# {invoice_data['invoice_number']}", ParagraphStyle(
'InvNumber', parent=self.styles['SmallText'],
fontSize=12, textColor=colors.HexColor('#e94560'),
alignment=TA_RIGHT, fontName='Helvetica-Bold',
))],
]
title_table = Table(title_cells, colWidths=[8*cm])
title_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
]))
header = Table([[company_table, title_table]], colWidths=[8*cm, 8*cm])
header.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LINEBELOW', (0, 0), (-1, 0), 3, colors.HexColor('#1a1a2e')),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
]))
return header
def _billing_section(self, invoice_data: dict) -> Table:
"""Create the bill-to and invoice details section."""
bill_to = [
[Paragraph('BILL TO', self.styles['SectionLabel'])],
[Paragraph(f"<b>{invoice_data['client_name']}</b>", self.styles['SmallText'])],
[Paragraph(invoice_data['client_address'], self.styles['SmallText'])],
[Paragraph(
f"{invoice_data['client_city']}, {invoice_data['client_state']} {invoice_data['client_zip']}",
self.styles['SmallText']
)],
]
if invoice_data.get('client_email'):
bill_to.append([Paragraph(invoice_data['client_email'], self.styles['SmallText'])])
bill_table = Table(bill_to, colWidths=[8*cm])
bill_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 1),
('BOTTOMPADDING', (0, 0), (-1, -1), 1),
('LINEBELOW', (0, 0), (0, 0), 1, colors.HexColor('#dddddd')),
]))
# Invoice details
details_data = [
[Paragraph('INVOICE DETAILS', self.styles['SectionLabel']),
Paragraph('', self.styles['SmallText'])],
]
detail_fields = [
('Invoice Date', invoice_data['issue_date']),
('Due Date', invoice_data['due_date']),
('Payment Terms', invoice_data.get('payment_terms', 'Net 30')),
]
if invoice_data.get('po_number'):
detail_fields.append(('PO Number', invoice_data['po_number']))
for label, value in detail_fields:
details_data.append([
Paragraph(label, self.styles['SmallText']),
Paragraph(f"<b>{value}</b>", ParagraphStyle(
'DetailValue', parent=self.styles['SmallText'],
alignment=TA_RIGHT,
)),
])
details_table = Table(details_data, colWidths=[3.5*cm, 4.5*cm])
details_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('TOPPADDING', (0, 0), (-1, -1), 1),
('BOTTOMPADDING', (0, 0), (-1, -1), 1),
('LINEBELOW', (0, 0), (-1, 0), 1, colors.HexColor('#dddddd')),
]))
billing = Table([[bill_table, details_table]], colWidths=[8*cm, 8*cm])
billing.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'TOP'),
]))
return billing
def _items_table(self, items: list[dict], currency: str = "$") -> Table:
"""Create the line items table."""
header_row = ['#', 'Description', 'Qty', 'Rate', 'Amount']
data = [header_row]
for idx, item in enumerate(items, 1):
data.append([
str(idx),
item['description'],
str(item['quantity']),
f"{currency}{item['unit_price']:,.2f}",
f"{currency}{item['amount']:,.2f}",
])
col_widths = [0.8*cm, 8.5*cm, 1.5*cm, 2.5*cm, 2.7*cm]
table = Table(data, colWidths=col_widths, repeatRows=1)
style_cmds = [
# Header
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1a1a2e')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 8.5),
('ALIGN', (0, 0), (0, -1), 'CENTER'),
('ALIGN', (2, 0), (-1, -1), 'RIGHT'),
('ALIGN', (1, 1), (1, -1), 'LEFT'),
# Body
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 9),
('TOPPADDING', (0, 0), (-1, -1), 6),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('LEFTPADDING', (0, 0), (-1, -1), 6),
('RIGHTPADDING', (0, 0), (-1, -1), 6),
# Grid
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#dee2e6')),
('LINEBELOW', (0, 0), (-1, 0), 2, colors.HexColor('#1a1a2e')),
('LINEBELOW', (0, -1), (-1, -1), 2, colors.HexColor('#1a1a2e')),
# Alternating colors
*[('BACKGROUND', (0, i), (-1, i),
colors.HexColor('#f8f9fa') if i % 2 == 0 else colors.white)
for i in range(1, len(data))],
]
table.setStyle(TableStyle(style_cmds))
return table
def _totals_section(self, subtotal: Decimal, tax_rate: Decimal,
discount_rate: Decimal, currency: str = "$") -> Table:
"""Create the totals section."""
data = [
['Subtotal', f"{currency}{subtotal:,.2f}"],
]
if discount_rate > 0:
discount_amt = subtotal * discount_rate
data.append([
f'Discount ({discount_rate*100:.0f}%)',
f"-{currency}{discount_amt:,.2f}",
])
if tax_rate > 0:
tax_amt = subtotal * tax_rate
data.append([
f'Tax ({tax_rate*100:.0f}%)',
f"{currency}{tax_amt:,.2f}",
])
total = subtotal * (1 + tax_rate - discount_rate)
data.append(['Total', f"{currency}{total:,.2f}"])
table = Table(data, colWidths=[5*cm, 3*cm])
style_cmds = [
('ALIGN', (0, 0), (0, -1), 'RIGHT'),
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
('FONTNAME', (0, 0), (-1, -2), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -2), 9),
('TOPPADDING', (0, 0), (-1, -1), 3),
('BOTTOMPADDING', (0, 0), (-1, -1), 3),
# Total row
('LINEABOVE', (0, -1), (-1, -1), 2, colors.HexColor('#1a1a2e')),
('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
('FONTSIZE', (0, -1), (-1, -1), 11),
('TEXTCOLOR', (0, -1), (-1, -1), colors.HexColor('#1a1a2e')),
]
if discount_rate > 0:
style_cmds += [
('TEXTCOLOR', (0, 1), (1, 1), colors.HexColor('#e94560')),
]
table.setStyle(TableStyle(style_cmds))
return table
def _payment_section(self, invoice_data: dict) -> Table:
"""Create the payment information section."""
cells = [[Paragraph('PAYMENT INFORMATION', self.styles['SectionLabel'])]]
if invoice_data.get('bank_name'):
cells.append([Paragraph(
f"<b>Bank:</b> {invoice_data['bank_name']}",
self.styles['SmallText']
)])
if invoice_data.get('bank_account'):
cells.append([Paragraph(
f"<b>Account:</b> {invoice_data['bank_account']}",
self.styles['SmallText']
)])
if invoice_data.get('bank_routing'):
cells.append([Paragraph(
f"<b>Routing:</b> {invoice_data['bank_routing']}",
self.styles['SmallText']
)])
if invoice_data.get('payment_instructions'):
cells.append([Paragraph(
invoice_data['payment_instructions'],
self.styles['SmallText']
)])
table = Table(cells, colWidths=[16*cm])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#f8f9fa')),
('BOX', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 12),
('RIGHTPADDING', (0, 0), (-1, -1), 12),
('TOPPADDING', (0, 0), (0, 0), 8),
]))
return table
def generate(self, invoice_data: dict, output_path: str):
"""Generate the complete invoice PDF."""
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=2*cm,
rightMargin=2*cm,
topMargin=1.5*cm,
bottomMargin=2*cm,
title=f"Invoice {invoice_data['invoice_number']}",
author=self.company['name'],
)
story = []
# Header
story.append(self._header_table(invoice_data))
story.append(Spacer(1, 0.5*cm))
# Billing section
story.append(self._billing_section(invoice_data))
story.append(Spacer(1, 0.5*cm))
# Line items
for item in invoice_data.get('line_items', []):
item['amount'] = item['quantity'] * item['unit_price']
story.append(self._items_table(
invoice_data.get('line_items', []),
invoice_data.get('currency_symbol', '$')
))
story.append(Spacer(1, 0.3*cm))
# Totals
story.append(self._totals_section(
invoice_data['subtotal'],
invoice_data.get('tax_rate', Decimal('0')),
invoice_data.get('discount_rate', Decimal('0')),
invoice_data.get('currency_symbol', '$'),
))
story.append(Spacer(1, 0.5*cm))
# Payment info
story.append(self._payment_section(invoice_data))
story.append(Spacer(1, 0.3*cm))
# Notes
if invoice_data.get('notes') or invoice_data.get('terms'):
notes_cells = []
if invoice_data.get('notes'):
notes_cells.append([Paragraph('NOTES', self.styles['SectionLabel'])])
notes_cells.append([Paragraph(invoice_data['notes'], self.styles['SmallText'])])
if invoice_data.get('terms'):
notes_cells.append([Paragraph('TERMS', self.styles['SectionLabel'])])
notes_cells.append([Paragraph(invoice_data['terms'], self.styles['SmallText'])])
notes_table = Table(notes_cells, colWidths=[16*cm])
notes_table.setStyle(TableStyle([
('LINEABOVE', (0, 0), (-1, 0), 1, colors.HexColor('#dddddd')),
('TOPPADDING', (0, 0), (-1, -1), 4),
]))
story.append(notes_table)
doc.build(story)
print(f"Invoice generated: {output_path}")
# Usage
if __name__ == '__main__':
generator = InvoiceGenerator({
'name': 'Cosmic Stack Labs',
'address': '123 Innovation Drive',
'city': 'San Francisco',
'state': 'CA',
'zip': '94105',
'phone': '(555) 123-4567',
'email': 'billing@cosmicstack.com',
})
invoice = {
'invoice_number': 'INV-2025-0042',
'issue_date': '2025-01-15',
'due_date': '2025-02-14',
'payment_terms': 'Net 30',
'po_number': 'PO-2025-001',
'client_name': 'Acme Corporation',
'client_address': '456 Business Ave, Suite 200',
'client_city': 'New York',
'client_state': 'NY',
'client_zip': '10001',
'client_email': 'ap@acmecorp.com',
'line_items': [
{'description': 'Web Development - Frontend React Implementation', 'quantity': 40, 'unit_price': Decimal('150.00')},
{'description': 'UI/UX Design - Dashboard Redesign', 'quantity': 20, 'unit_price': Decimal('125.00')},
{'description': 'DevOps Setup - CI/CD Pipeline Configuration', 'quantity': 8, 'unit_price': Decimal('175.00')},
{'description': 'Database Optimization - Query Performance Tuning', 'quantity': 12, 'unit_price': Decimal('200.00')},
],
'subtotal': Decimal('9900.00'),
'tax_rate': Decimal('0.08'),
'discount_rate': Decimal('0.05'),
'currency_symbol': '$',
'bank_name': 'First National Bank',
'bank_account': 'XXXX-XXXX-1234',
'bank_routing': '021000021',
'payment_instructions': 'Please include invoice number with payment.',
'notes': 'Thank you for your business!',
'terms': 'Payment due within 30 days. Late payment subject to 1.5% monthly fee.',
}
generator.generate(invoice, 'invoice_2025_0042.pdf')
FPDF Invoice Generator (Lightweight Alternative)
"""Lightweight invoice generation with FPDF2."""
from fpdf import FPDF
from decimal import Decimal
class InvoicePDF(FPDF):
"""Simple invoice PDF generator using FPDF."""
def header(self):
self.set_font('Helvetica', 'B', 10)
self.set_text_color(26, 26, 46)
self.cell(0, 8, 'INVOICE', align='R', new_x="LMARGIN", new_y="NEXT")
self.line(10, self.get_y(), 200, self.get_y())
self.ln(5)
def footer(self):
self.set_y(-15)
self.set_font('Helvetica', 'I', 8)
self.set_text_color(128, 128, 128)
self.cell(0, 10, f'Page {self.page_no()}/{{nb}}', align='C')
def company_info(self, name, address, city, state, zip_code, phone, email):
self.set_font('Helvetica', 'B', 12)
self.set_text_color(26, 26, 46)
self.cell(0, 6, name, new_x="LMARGIN", new_y="NEXT")
self.set_font('Helvetica', '', 9)
self.set_text_color(102, 102, 102)
self.cell(0, 4, address, new_x="LMARGIN", new_y="NEXT")
self.cell(0, 4, f'{city}, {state} {zip_code}', new_x="LMARGIN", new_y="NEXT")
self.cell(0, 4, f'Phone: {phone} | Email: {email}', new_x="LMARGIN", new_y="NEXT")
self.ln(5)
def bill_to(self, client_name, client_address, client_city, client_state, client_zip):
self.set_font('Helvetica', 'B', 9)
self.set_text_color(153, 153, 153)
self.cell(0, 5, 'BILL TO', new_x="LMARGIN", new_y="NEXT")
self.set_font('Helvetica', 'B', 10)
self.set_text_color(51, 51, 51)
self.cell(0, 5, client_name, new_x="LMARGIN", new_y="NEXT")
self.set_font('Helvetica', '', 9)
self.cell(0, 4, client_address, new_x="LMARGIN", new_y="NEXT")
self.cell(0, 4, f'{client_city}, {client_state} {client_zip}', new_x="LMARGIN", new_y="NEXT")
self.ln(5)
def invoice_details(self, inv_number, issue_date, due_date, terms, po_number=None):
self.set_font('Helvetica', 'B', 9)
self.set_text_color(153, 153, 153)
self.cell(0, 5, 'INVOICE DETAILS', new_x="LMARGIN", new_y="NEXT")
details = [
('Invoice #:', inv_number),
('Date:', issue_date),
('Due:', due_date),
('Terms:', terms),
]
if po_number:
details.append(('PO #:', po_number))
for label, value in details:
self.set_font('Helvetica', '', 9)
self.set_text_color(102, 102, 102)
self.cell(30, 5, label)
self.set_font('Helvetica', 'B', 9)
self.set_text_color(51, 51, 51)
self.cell(0, 5, value, new_x="LMARGIN", new_y="NEXT")
self.ln(5)
def line_items(self, items, currency='$'):
"""Add line items table."""
# Table header
self.set_fill_color(26, 26, 46)
self.set_text_color(255, 255, 255)
self.set_font('Helvetica', 'B', 9)
col_widths = [10, 85, 20, 30, 35]
headers = ['#', 'Description', 'Qty', 'Rate', 'Amount']
for i, header in enumerate(headers):
self.cell(col_widths[i], 8, header, border=1, align='C' if i != 1 else 'L', fill=True)
self.ln()
# Table body
self.set_text_color(51, 51, 51)
fill = False
for idx, item in enumerate(items, 1):
if fill:
self.set_fill_color(248, 249, 250)
else:
self.set_fill_color(255, 255, 255)
self.set_font('Helvetica', '', 9)
self.cell(col_widths[0], 7, str(idx), border=1, align='C', fill=True)
self.cell(col_widths[1], 7, item['description'], border=1, align='L', fill=True)
self.cell(col_widths[2], 7, str(item['quantity']), border=1, align='R', fill=True)
self.cell(col_widths[3], 7, f"{currency}{item['unit_price']:,.2f}", border=1, align='R', fill=True)
self.cell(col_widths[4], 7, f"{currency}{item['amount']:,.2f}", border=1, align='R', fill=True)
self.ln()
fill = not fill
# Bottom line
self.line(10, self.get_y(), 200, self.get_y())
self.ln(3)
def totals(self, subtotal, tax_rate, discount_rate, currency='$'):
"""Add totals section."""
self.set_font('Helvetica', '', 9)
items = [
('Subtotal:', f"{currency}{subtotal:,.2f}"),
]
if discount_rate > 0:
discount_amt = subtotal * discount_rate
items.append((f'Discount ({discount_rate*100:.0f}%):', f"-{currency}{discount_amt:,.2f}"))
if tax_rate > 0:
tax_amt = subtotal * tax_rate
items.append((f'Tax ({tax_rate*100:.0f}%):', f"{currency}{tax_amt:,.2f}"))
total = subtotal * (1 + tax_rate - discount_rate)
items.append(('Total:', f"{currency}{total:,.2f}"))
# Right-align totals
x_start = 130
for label, value in items:
self.set_x(x_start)
self.set_text_color(102, 102, 102)
self.cell(35, 6, label, align='R')
self.set_text_color(51, 51, 51)
self.set_font('Helvetica', 'B' if 'Total' in label else '', 9)
self.cell(35, 6, value, align='R', new_x="LMARGIN", new_y="NEXT")
self.ln(5)
def payment_info(self, bank_name, account, routing, instructions=None):
"""Add payment information block."""
self.set_fill_color(248, 249, 250)
self.set_font('Helvetica', 'B', 9)
self.set_text_color(26, 26, 46)
self.cell(0, 6, 'PAYMENT INFORMATION', border=1, fill=True, new_x="LMARGIN", new_y="NEXT")
self.set_font('Helvetica', '', 9)
self.set_text_color(51, 51, 51)
info = [
(f'Bank: {bank_name}', ''),
(f'Account: {account}', ''),
(f'Routing: {routing}', ''),
]
if instructions:
info.append((instructions, ''))
for label, _ in info:
self.cell(0, 5, label, border=1, new_x="LMARGIN", new_y="NEXT")
self.ln(5)
# Usage
pdf = InvoicePDF()
pdf.alias_nb_pages()
pdf.add_page()
pdf.company_info('Cosmic Stack Labs', '123 Innovation Drive',
'San Francisco', 'CA', '94105', '(555) 123-4567', 'billing@cosmicstack.com')
pdf.bill_to('Acme Corporation', '456 Business Ave, Suite 200', 'New York', 'NY', '10001')
pdf.invoice_details('INV-2025-0042', '2025-01-15', '2025-02-14', 'Net 30', 'PO-2025-001')
items = [
{'description': 'Web Development Services', 'quantity': 40, 'unit_price': 150.00,
'amount': 6000.00},
{'description': 'UI/UX Design', 'quantity': 20, 'unit_price': 125.00, 'amount': 2500.00},
{'description': 'DevOps Setup', 'quantity': 8, 'unit_price': 175.00, 'amount': 1400.00},
]
pdf.line_items(items)
pdf.totals(Decimal('9900.00'), Decimal('0.08'), Decimal('0.05'))
pdf.payment_info('First National Bank', 'XXXX-XXXX-1234', '021000021',
'Please include invoice number with payment.')
pdf.output('fpdf_invoice.pdf')
print("FPDF invoice generated: fpdf_invoice.pdf")
4. Receipt and Contract Templates
Receipt Template
<!-- templates/receipt.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
@page { size: A5; margin: 1cm; }
body { font-family: 'Helvetica', sans-serif; font-size: 9pt; color: #333; }
.header { text-align: center; margin-bottom: 0.5cm; }
.header h1 { font-size: 16pt; color: #1a1a2e; margin: 0; }
.header p { color: #999; font-size: 8pt; margin: 2px 0; }
.receipt-info { margin-bottom: 0.5cm; }
.receipt-info table { width: 100%; }
.receipt-info td { padding: 2px 0; font-size: 8pt; }
.receipt-info td:last-child { text-align: right; font-weight: bold; }
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 0.5cm; }
.items-table th { background: #1a1a2e; color: white; padding: 4px 6px; font-size: 7pt; text-align: left; }
.items-table th:last-child, .items-table th:nth-child(3) { text-align: right; }
.items-table td { padding: 4px 6px; border-bottom: 1px solid #eee; font-size: 8pt; }
.items-table td:last-child, .items-table td:nth-child(3) { text-align: right; }
.total { text-align: right; font-size: 11pt; font-weight: bold; color: #1a1a2e; }
.thank-you { text-align: center; color: #999; font-size: 8pt; margin-top: 0.5cm; border-top: 1px dashed #ddd; padding-top: 0.3cm; }
</style>
</head>
<body>
<div class="header">
<h1>Receipt</h1>
<p>{{ store_name }} | {{ store_location }}</p>
</div>
<div class="receipt-info">
<table>
<tr><td>Receipt #:</td><td>{{ receipt_number }}</td></tr>
<tr><td>Date:</td><td>{{ date }}</td></tr>
<tr><td>Cashier:</td><td>{{ cashier }}</td></tr>
</table>
</div>
<table class="items-table">
<thead><tr>
<th>Item</th><th>Qty</th><th>Price</th><th>Total</th>
</tr></thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.qty }}</td>
<td>\${{ "{:,.2f}".format(item.price) }}</td>
<td>\${{ "{:,.2f}".format(item.total) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="total">Total: \${{ "{:,.2f}".format(total) }}</div>
<div class="thank-you">Thank you for your purchase!</div>
</body>
</html>
Contract PDF Generation
"""Generate professional contract PDFs with signature blocks."""
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
PageBreak, KeepTogether
)
from reportlab.lib.styles import ParagraphStyle
from datetime import datetime
def generate_contract(output_path: str, contract_data: dict):
"""Generate a PDF contract with signature blocks."""
doc = SimpleDocTemplate(
output_path, pagesize=A4,
leftMargin=2.5*cm, rightMargin=2.5*cm,
topMargin=2.5*cm, bottomMargin=2.5*cm,
title=contract_data.get('title', 'Agreement'),
author=contract_data.get('party_a', ''),
)
styles = {}
styles['title'] = ParagraphStyle(
'ContractTitle', fontName='Helvetica-Bold',
fontSize=18, textColor=colors.HexColor('#1a1a2e'),
spaceAfter=6, alignment=1,
)
styles['subtitle'] = ParagraphStyle(
'ContractSubtitle', fontName='Helvetica',
fontSize=10, textColor=colors.HexColor('#666666'),
spaceAfter=20, alignment=1,
)
styles['h2'] = ParagraphStyle(
'ContractH2', fontName='Helvetica-Bold',
…(truncated)