What is ReportLab and PDF Generation in Python
ReportLab is the most mature, feature-rich open-source library for creating PDF documents directly from Python. Unlike other approaches that convert HTML or use templating engines, ReportLab gives you pixel-perfect control over every element on the page. You can draw lines, shapes, images, tables, charts, and precisely position text using a powerful coordinate system. The library consists of two main layers: a low-level canvas API for drawing primitive graphics, and a high-level platypus (Page Layout and Typography Using Scripts) framework that works with document templates and reusable page elements like headers, footers, paragraphs, and tables.
Why PDF Generation Matters
Automated PDF generation solves critical business needs across industries. Instead of manually creating invoices, reports, certificates, or contracts one by one, you can programmatically generate thousands of perfectly formatted documents from data sources. This eliminates human error, ensures brand consistency, and dramatically reduces turnaround time from hours to seconds. Common use cases include:
- Financial services: generating bank statements, invoices, and portfolio reports
- E-commerce: creating order confirmations, packing slips, and return labels
- Healthcare: producing lab results, prescriptions, and insurance forms
- Education: generating certificates, transcripts, and syllabus documents
- Legal: automating contracts, NDAs, and compliance documents
PDF is the universal document format — it preserves layout across devices, can be digitally signed, and is accepted by governments and enterprises worldwide.
Getting Started: Installation and Setup
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Install ReportLab using pip. The library has zero external dependencies beyond Python itself, which makes deployment straightforward in any environment.
pip install reportlab
To verify the installation and check your version, run this in a Python shell:
import reportlab
print(reportlab.Version)
ReportLab is compatible with Python 3.6+ and works across Windows, macOS, and Linux. For this tutorial, we'll use Python 3.9 or later to take advantage of modern language features. All code examples assume you have ReportLab installed and a working Python environment.
Understanding the Two API Layers
ReportLab provides two distinct ways to build PDFs, and knowing when to use each is essential:
1. The Canvas API (Low-Level)
The canvas.Canvas object is a drawing surface where you place elements using absolute coordinates. The coordinate system places (0, 0) at the bottom-left corner of the page, with X increasing to the right and Y increasing upward. You manually manage page breaks, line wrapping, and layout calculations. This is ideal for highly custom single-page documents like certificates, tickets, or artwork.
2. Platypus (High-Level Document Framework)
Platypus works like a word processor's layout engine. You create Flowables — objects like paragraphs, tables, images, and spacers — and add them to a story list. The framework automatically handles page breaks, word wrapping, orphan/widow control, and keeps your content flowing across multiple pages. You define page templates with headers and footers using BaseDocTemplate or SimpleDocTemplate. This is the recommended approach for multi-page reports, invoices, and any content-heavy documents.
Creating Your First PDF: The Canvas Approach
Let's start with the canvas API to understand the fundamentals. We'll create a simple "Hello World" PDF and then build on it.
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
# Create a new PDF document
c = canvas.Canvas("hello_world.pdf", pagesize=letter)
# Set font and size
c.setFont("Helvetica", 24)
# Draw text at position x=1 inch, y=9 inches from bottom
c.drawString(1 * inch, 9 * inch, "Hello, World!")
# Draw a subtitle
c.setFont("Helvetica", 14)
c.drawString(1 * inch, 8.5 * inch, "This is my first PDF generated with ReportLab")
# Draw a horizontal line
c.line(1 * inch, 8 * inch, 7 * inch, 8 * inch)
# Save the PDF (this writes the file to disk)
c.showPage()
c.save()
print("PDF created successfully!")
This creates a PDF with text and a line. Notice that we control everything manually: font, position, and line drawing. For a single-page certificate, this level of control is perfect. But for a multi-page report with flowing text, we'd quickly get lost in coordinate calculations.
Moving to Platypus: The Document Template System
For real-world document generation, Platypus saves enormous amounts of time. Here's the same content using the high-level framework:
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.lib import colors
# Create the document template
doc = SimpleDocTemplate(
"platypus_hello.pdf",
pagesize=letter,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72
)
# Get default styles and create a custom one
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=24,
spaceAfter=12,
textColor=colors.HexColor("#1a1a1a")
)
body_style = styles['Normal']
# Build the story (content list)
story = []
# Add content as flowables
story.append(Paragraph("Hello, World!", title_style))
story.append(Spacer(1, 0.25 * inch))
story.append(Paragraph(
"This is my first PDF generated with ReportLab's Platypus framework. "
"The framework handles page breaks and layout automatically.",
body_style
))
story.append(Spacer(1, 0.5 * inch))
story.append(Paragraph(
"ReportLab gives you professional-grade PDF output with minimal code.",
body_style
))
# Build the PDF
doc.build(story)
print("Platypus PDF created successfully!")
The key difference: we don't specify coordinates. Platypus flows content like a word processor, stacking paragraphs vertically and breaking pages when needed. The styles object gives us professional typography defaults.
Working with Styles and Typography
ReportLab's stylesheet system is the foundation of professional-looking documents. You can use predefined styles or create custom ones that inherit from parents, forming a style hierarchy.
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
# Get the built-in stylesheet
styles = getSampleStyleSheet()
# Create a custom heading style
heading_style = ParagraphStyle(
'CustomHeading',
parent=styles['Heading2'],
fontName='Helvetica-Bold',
fontSize=18,
textColor=colors.HexColor('#2c3e50'),
spaceBefore=24,
spaceAfter=12,
alignment=TA_LEFT
)
# Create a body text style with justified alignment
body_style = ParagraphStyle(
'CustomBody',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=11,
leading=16, # line height
alignment=TA_JUSTIFY,
spaceAfter=8,
textColor=colors.HexColor('#333333')
)
# Style for code or monospace content
code_style = ParagraphStyle(
'CodeBlock',
parent=styles['Normal'],
fontName='Courier',
fontSize=9,
leading=12,
leftIndent=24,
rightIndent=24,
backColor=colors.HexColor('#f5f5f5'),
borderPadding=8,
spaceAfter=12
)
# Style for a callout box
callout_style = ParagraphStyle(
'Callout',
parent=styles['Normal'],
fontName='Helvetica-Oblique',
fontSize=10,
leading=14,
leftIndent=18,
rightIndent=18,
backColor=colors.HexColor('#e8f4f8'),
borderColor=colors.HexColor('#2980b9'),
borderWidth=1,
borderPadding=10,
spaceAfter=16
)
print("Custom styles defined successfully")
Important style properties include fontName (Helvetica, Courier, Times-Roman and their bold/oblique variants), fontSize, leading (line spacing), spaceBefore/spaceAfter (paragraph spacing), alignment, leftIndent/rightIndent, and color properties. You can also set backColor for background fills and borderColor/borderWidth for bordered containers.
Adding Tables to Your Documents
Tables are one of the most powerful flowables in ReportLab. They handle column widths, cell styling, grid lines, and even cell spanning. Here's how to create a professional data table:
from reportlab.platypus import Table, TableStyle
from reportlab.lib import colors
from reportlab.lib.units import inch
# Define table data with a header row
table_data = [
['Product', 'SKU', 'Quantity', 'Unit Price', 'Total'],
['Widget Alpha', 'WA-100', '5', '$12.50', '$62.50'],
['Widget Beta', 'WB-200', '3', '$18.75', '$56.25'],
['Gadget Pro', 'GP-300', '2', '$45.00', '$90.00'],
['Super Tool', 'ST-400', '10', '$8.25', '$82.50'],
['Deluxe Kit', 'DK-500', '1', '$99.00', '$99.00'],
]
# Create the table with specified column widths
col_widths = [2*inch, 1*inch, 0.8*inch, 1*inch, 1*inch]
table = Table(table_data, colWidths=col_widths, repeatRows=1)
# Apply styling
table_style = TableStyle([
# Header styling
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('BOTTOMPADDING', (0, 0), (-1, 0), 8),
('TOPPADDING', (0, 0), (-1, 0), 8),
# Body styling
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 9),
('ALIGN', (2, 1), (-1, -1), 'RIGHT'), # numbers align right
('ALIGN', (0, 1), (1, -1), 'LEFT'), # text aligns left
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('TOPPADDING', (0, 1), (-1, -1), 6),
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
# Grid lines
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#bdc3c7')),
# Alternating row colors
('BACKGROUND', (0, 1), (-1, 1), colors.HexColor('#f9f9f9')),
('BACKGROUND', (0, 2), (-1, 2), colors.white),
('BACKGROUND', (0, 3), (-1, 3), colors.HexColor('#f9f9f9')),
('BACKGROUND', (0, 4), (-1, 4), colors.white),
('BACKGROUND', (0, 5), (-1, 5), colors.HexColor('#f9f9f9')),
# Highlight the total row if needed (last row)
('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
])
table.setStyle(table_style)
# Now add this table to your story list
# story.append(table)
print("Table created - add it to your story list to include in the document")
The TableStyle commands use coordinate tuples: (column, row) where 0 is the first column/row and -1 means the last. Commands cascade in order, so you can set a base style and then override specific cells. The repeatRows=1 parameter makes the header row repeat on every page when the table spans multiple pages — essential for long data tables.
Including Images and Graphics
ReportLab supports JPEG and PNG images natively. You can control their size, aspect ratio, and placement with precision.
from reportlab.platypus import Image
from reportlab.lib.units import inch
import os
# Method 1: Image as a flowable in your story
logo = Image("company_logo.png")
logo.drawHeight = 0.75 * inch # set desired height
logo.drawWidth = 2.5 * inch # set desired width (aspect ratio preserved if you set both)
# For automatic aspect ratio, set only one dimension:
# logo.drawHeight = 0.75 * inch # width will auto-calculate
# story.append(logo)
# Method 2: Using the canvas for precise placement
def add_watermark(canvas_obj, doc):
"""Draw a watermark on every page"""
canvas_obj.saveState()
canvas_obj.setFont('Helvetica', 60)
canvas_obj.setFillColorRGB(0.95, 0.95, 0.95) # light gray
canvas_obj.drawCentredString(
doc.width / 2 + doc.leftMargin,
doc.height / 2,
"DRAFT"
)
canvas_obj.restoreState()
# Method 3: Drawing shapes directly
def draw_custom_logo(canvas_obj, x, y, size):
"""Draw a simple logo with shapes"""
canvas_obj.setFillColor(colors.HexColor('#3498db'))
canvas_obj.circle(x, y, size, fill=1)
canvas_obj.setFillColor(colors.HexColor('#2c3e50'))
canvas_obj.rect(x - size*0.6, y - size*0.6, size*1.2, size*1.2, fill=1)
# Add text inside
canvas_obj.setFillColor(colors.white)
canvas_obj.setFont('Helvetica-Bold', size * 0.8)
canvas_obj.drawCentredString(x, y - 2, "BRAND")
print("Image handling patterns defined")
For images, always use reasonable resolutions. A 300 DPI image for a 2-inch wide logo should be about 600 pixels wide. Oversized images bloat PDF file size without improving print quality.
Page Templates: Headers, Footers, and Multi-Page Documents
Professional documents need consistent headers, footers, and page numbers. This is where BaseDocTemplate with page templates shines. Here's a complete example:
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame, Paragraph, PageBreak
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from datetime import datetime
class ProfessionalReport:
def __init__(self, output_path):
self.output_path = output_path
self.styles = getSampleStyleSheet()
self.doc = BaseDocTemplate(
output_path,
pagesize=letter,
leftMargin=72,
rightMargin=72,
topMargin=72,
bottomMargin=72,
title='Professional Report',
author='ReportLab Generator'
)
# Define frames for content
# Frame(x, y, width, height) - coordinates from bottom-left
content_frame = Frame(
self.doc.leftMargin,
self.doc.bottomMargin + 40, # leave room for footer
self.doc.width,
self.doc.height - 40, # leave room for header
id='main_frame'
)
# Create page template with header/footer callbacks
page_template = PageTemplate(
id='ReportPages',
frames=[content_frame],
onPage=self._add_header_footer
)
self.doc.addPageTemplates([page_template])
def _add_header_footer(self, canvas_obj, doc):
"""Called automatically for every page"""
page_width = doc.width + doc.leftMargin + doc.rightMargin
page_height = doc.height + doc.topMargin + doc.bottomMargin
# ---- HEADER ----
canvas_obj.saveState()
# Header background line
canvas_obj.setStrokeColor(colors.HexColor('#2c3e50'))
canvas_obj.setLineWidth(2)
canvas_obj.line(
doc.leftMargin,
page_height - doc.topMargin + 10,
doc.leftMargin + doc.width,
page_height - doc.topMargin + 10
)
# Company name / report title
canvas_obj.setFont('Helvetica-Bold', 9)
canvas_obj.setFillColor(colors.HexColor('#2c3e50'))
canvas_obj.drawString(
doc.leftMargin,
page_height - doc.topMargin + 15,
"ACME CORPORATION - CONFIDENTIAL REPORT"
)
# Date on the right
canvas_obj.setFont('Helvetica', 8)
canvas_obj.setFillColor(colors.HexColor('#7f8c8d'))
today = datetime.now().strftime("%B %d, %Y")
canvas_obj.drawRightString(
doc.leftMargin + doc.width,
page_height - doc.topMargin + 15,
f"Generated: {today}"
)
# ---- FOOTER ----
# Footer line
canvas_obj.setStrokeColor(colors.HexColor('#bdc3c7'))
canvas_obj.setLineWidth(0.5)
canvas_obj.line(
doc.leftMargin,
doc.bottomMargin - 10,
doc.leftMargin + doc.width,
doc.bottomMargin - 10
)
# Page number
canvas_obj.setFont('Helvetica', 8)
canvas_obj.setFillColor(colors.HexColor('#95a5a6'))
canvas_obj.drawCentredString(
doc.leftMargin + doc.width / 2,
doc.bottomMargin - 22,
f"Page {canvas_obj.getPageNumber()}"
)
# Confidentiality notice
canvas_obj.drawRightString(
doc.leftMargin + doc.width,
doc.bottomMargin - 22,
"© ACME Corp - All Rights Reserved"
)
canvas_obj.restoreState()
def build(self, story):
self.doc.build(story)
# Usage example
report = ProfessionalReport("professional_report.pdf")
story = []
title_style = ParagraphStyle(
'ReportTitle',
parent=report.styles['Heading1'],
fontSize=22,
spaceAfter=6,
textColor=colors.HexColor('#2c3e50')
)
story.append(Paragraph("Quarterly Financial Analysis", title_style))
story.append(Paragraph("Executive Summary", report.styles['Heading2']))
story.append(Paragraph(
"This report provides a comprehensive overview of the company's financial "
"performance across all business units. The data presented has been verified "
"by our accounting team and reflects all transactions processed through Q4.",
report.styles['Normal']
))
# Add more content...
story.append(PageBreak())
story.append(Paragraph("Section 2: Detailed Metrics", report.styles['Heading2']))
story.append(Paragraph("Additional analysis content here...", report.styles['Normal']))
report.build(story)
print("Professional report with headers/footers created!")
This pattern gives you complete control. The onPage callback fires for every page, letting you draw headers, footers, watermarks, or page numbers. The Frame defines where content flows, and you can have multiple frames per page (like a sidebar layout).
Real-World Example: Building an Invoice Generator
Let's combine everything into a complete, production-ready invoice generator. This example demonstrates all the concepts we've covered and produces a document suitable for actual business use.
from reportlab.platypus import (BaseDocTemplate, PageTemplate, Frame,
Paragraph, Table, TableStyle, Spacer, Image, PageBreak, HRFlowable)
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch, mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.enums import TA_RIGHT, TA_CENTER, TA_LEFT
from datetime import datetime, timedelta
import random
import os
class InvoiceGenerator:
"""Professional invoice PDF generator"""
def __init__(self, output_dir="invoices"):
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
self.styles = getSampleStyleSheet()
self._setup_custom_styles()
def _setup_custom_styles(self):
"""Create all custom styles for the invoice"""
self.invoice_title = ParagraphStyle(
'InvoiceTitle',
parent=self.styles['Heading1'],
fontName='Helvetica-Bold',
fontSize=26,
textColor=colors.HexColor('#1a1a2e'),
spaceAfter=4
)
self.company_name_style = ParagraphStyle(
'CompanyName',
parent=self.styles['Normal'],
fontName='Helvetica-Bold',
fontSize=14,
textColor=colors.HexColor('#16213e'),
spaceAfter=2
)
self.info_label_style = ParagraphStyle(
'InfoLabel',
parent=self.styles['Normal'],
fontName='Helvetica-Bold',
fontSize=8,
textColor=colors.HexColor('#7f8c8d'),
leading=10
)
self.info_value_style = ParagraphStyle(
'InfoValue',
parent=self.styles['Normal'],
fontName='Helvetica',
fontSize=10,
textColor=colors.HexColor('#2c3e50'),
leading=14
)
self.section_heading = ParagraphStyle(
'SectionHeading',
parent=self.styles['Heading2'],
fontName='Helvetica-Bold',
fontSize=14,
textColor=colors.HexColor('#16213e'),
spaceBefore=20,
spaceAfter=8
)
self.body_text = ParagraphStyle(
'BodyText',
parent=self.styles['Normal'],
fontName='Helvetica',
fontSize=10,
leading=15,
alignment=TA_JUSTIFY,
spaceAfter=8
)
self.total_label = ParagraphStyle(
'TotalLabel',
parent=self.styles['Normal'],
fontName='Helvetica-Bold',
fontSize=14,
textColor=colors.HexColor('#1a1a2e'),
alignment=TA_RIGHT
)
self.total_value = ParagraphStyle(
'TotalValue',
parent=self.styles['Normal'],
fontName='Helvetica-Bold',
fontSize=18,
textColor=colors.HexColor('#e74c3c'),
alignment=TA_RIGHT
)
self.terms_style = ParagraphStyle(
'Terms',
parent=self.styles['Normal'],
fontName='Helvetica',
fontSize=8,
textColor=colors.HexColor('#95a5a6'),
leading=12,
spaceBefore=16
)
def _build_header_footer(self, canvas_obj, doc):
"""Page header and footer for all invoice pages"""
canvas_obj.saveState()
# Subtle header line
canvas_obj.setStrokeColor(colors.HexColor('#e74c3c'))
canvas_obj.setLineWidth(3)
page_top = doc.height + doc.topMargin + doc.bottomMargin
canvas_obj.line(
doc.leftMargin, page_top - doc.topMargin + 8,
doc.leftMargin + doc.width, page_top - doc.topMargin + 8
)
# Footer with page info
canvas_obj.setFont('Helvetica', 7)
canvas_obj.setFillColor(colors.HexColor('#bdc3c7'))
canvas_obj.drawCentredString(
doc.leftMargin + doc.width / 2,
doc.bottomMargin - 15,
f"Page {canvas_obj.getPageNumber()} | Generated by ReportLab Invoice System"
)
canvas_obj.restoreState()
def _create_invoice_info_table(self, invoice_data):
"""Create the invoice header info section"""
# Two-column layout for invoice metadata
left_data = [
[Paragraph("INVOICE TO:", self.info_label_style)],
[Paragraph(invoice_data['client_name'], self.info_value_style)],
[Paragraph(invoice_data['client_address'], self.info_value_style)],
[Paragraph(invoice_data['client_email'], self.info_value_style)]
]
right_data = [
[Paragraph("INVOICE DETAILS:", self.info_label_style)],
[Paragraph(f"Invoice #: {invoice_data['invoice_number']}", self.info_value_style)],
[Paragraph(f"Date: {invoice_data['date']}", self.info_value_style)],
[Paragraph(f"Due Date: {invoice_data['due_date']}", self.info_value_style)]
]
# Wrap each side in its own table for layout
left_table = Table(left_data, colWidths=[3.2*inch])
left_table.setStyle(TableStyle([
('LEFTPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 2),
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
]))
right_table = Table(right_data, colWidths=[3.2*inch])
right_table.setStyle(TableStyle([
('LEFTPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 2),
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
]))
# Combine into a single row table
combined = Table([[left_table, right_table]], colWidths=[3.5*inch, 3.5*inch])
combined.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 0),
]))
return combined
def _create_line_items_table(self, line_items, currency="$"):
"""Create the detailed line items table"""
header = ['Item', 'Description', 'Qty', 'Rate', 'Amount']
data = [header]
subtotal = 0
for item in line_items:
amount = item['qty'] * item['rate']
subtotal += amount
data.append([
item['item_name'],
item['description'],
str(item['qty']),
f"{currency}{item['rate']:,.2f}",
f"{currency}{amount:,.2f}"
])
# Calculate totals
tax_rate = 0.08 # 8% tax
tax_amount = subtotal * tax_rate
total = subtotal + tax_amount
# Add spacer and summary rows
data.append(['', '', '', '', '']) # spacer
data.append(['', '', '', 'Subtotal:', f"{currency}{subtotal:,.2f}"])
data.append(['', '', '', f'Tax (8%):', f"{currency}{tax_amount:,.2f}"])
data.append(['', '', '', 'TOTAL:', f"{currency}{total:,.2f}"])
col_widths = [1.2*inch, 3*inch, 0.6*inch, 0.9*inch, 1.1*inch]
table = Table(data, colWidths=col_widths, repeatRows=1)
style_commands = [
# 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), 9),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('TOPPADDING', (0, 0), (-1, 0), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
# Body cells
('FONTNAME', (0, 1), (-1, -5), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -5), 9),
('TOPPADDING', (0, 1), (-1, -5), 8),
('BOTTOMPADDING', (0, 1), (-1, -5), 8),
# Alignment
('ALIGN', (0, 1), (1, -1), 'LEFT'),
('ALIGN', (2, 1), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
# Grid
('GRID', (0, 0), (-1, -5), 0.5, colors.HexColor('#ecf0f1')),
('LINEBELOW', (0, 0), (-1, 0), 1, colors.HexColor('#1a1a2e')),
# Alternating rows
('BACKGROUND', (0, 1), (-1, 1), colors.HexColor('#f8f9fa')),
('BACKGROUND', (0, 3), (-1, 3), colors.HexColor('#f8f9fa')),
('BACKGROUND', (0, 5), (-1, 5), colors.HexColor('#f8f9fa')),
# Subtotal row
('FONTNAME', (3, -4), (3, -1), 'Helvetica-Bold'),
('FONTNAME', (4, -4), (4, -1), 'Helvetica-Bold'),
('LINEABOVE', (3, -4), (-1, -4), 0.5, colors.HexColor('#bdc3c7')),
# Total row emphasis
('BACKGROUND', (3, -1), (-1, -1), colors.HexColor('#fdf2f2')),
('FONTSIZE', (4, -1), (-1, -1), 13),
('TEXTCOLOR', (4, -1), (-1, -1), colors.HexColor('#e74c3c')),
('LINEABOVE', (3, -1), (-1, -1), 1, colors.HexColor('#e74c3c')),
]
table.setStyle(TableStyle(style_commands))
return table, total
def generate_invoice(self, invoice_data, line_items):
"""Generate a complete invoice PDF"""
filename = f"invoice_{invoice_data['invoice_number']}.pdf"
filepath = os.path.join(self.output_dir, filename)
doc = BaseDocTemplate(
filepath,
pagesize=letter,
leftMargin=54,
rightMargin=54,
topMargin=54,
bottomMargin=54,
title=f"Invoice {invoice_data['invoice_number']}",
author="ACME Invoice System"
)
# Create frames
main_frame = Frame(
doc.leftMargin,
doc.bottomMargin + 30,
doc.width,
doc.height - 30,
id='invoice_frame'
)
page_template = PageTemplate(
id='InvoicePage',
frames=[main_frame],
onPage=self._build_header_footer
)
doc.addPageTemplates([page_template])
# Build story
story = []
# Company branding
story.append(Paragraph("ACME CORPORATION", self.company_name_style))
story.append(Paragraph("123 Business Avenue, Suite 400, Metropolis, NY 10001", self.info_value_style))
story.append(Paragraph("Phone: (555) 123-4567 | Email: billing@acmecorp.com", self.info_value_style))
story.append(Spacer(1, 0.3*inch))
# Separator line
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#e74c3c')))
story.append(Spacer(1, 0.2*inch))
# Invoice title
story.append(Paragraph("INVOICE", self.invoice_title))
story.append(Spacer(1, 0.2*inch))
# Invoice info section
story.append(self._create_invoice_info_table(invoice_data))
story.append(Spacer(1, 0.4*inch))
# Line items heading
story.append(Paragraph("Services & Charges", self.section_heading))
# Line items table
items_table, total_amount = self._