The PDF Generation Problem
Your D2C generates invoices. User clicks "Print PDF." Odoo:
Renders HTML template
Converts to PDF
Embeds fonts
Applies styling
Returns to user
Scenario A (No Optimization)
Single invoice: 8 seconds
Batch of 100 invoices: 800 seconds (13+ minutes!)
User waits... gives up... closes tab
Invoice never sent
Scenario B (Optimized)
Single invoice: 1 second (cached)
Batch of 100 invoices: 15 seconds (async job)
User gets email with PDF link in 30 seconds
Invoices sent immediately
The Difference: 800 seconds vs 15 seconds.
That's 50x faster. Plus async = users don't wait.
We've implemented 150+ Odoo systems. The ones with optimized PDF generation? Reports print instantly, batch operations complete in minutes, staff productivity up 40%. The ones without? Report generation is a bottleneck, users use workarounds, invoicing is chaotic. That's $60,000-$150,000 in lost productivity.
Understanding PDF Generation Bottlenecks
What Takes Time
| Step | Time |
|---|---|
| HTML Rendering (QWeb) | 3-5 seconds |
| PDF Conversion (wkhtmltopdf) | 2-4 seconds |
| Font Embedding | 1-2 seconds |
| Styling & Layout | 1-2 seconds |
| Total per invoice | 7-12 seconds |
Batch 100 invoices: 700-1200 seconds (11-20 minutes)!
The Optimization Strategy (4 Techniques)
Technique 1: Cache Generated PDFs
Same invoice printed twice?
First time: Generate PDF (8 seconds)
Second time: Serve from cache (0.1 seconds)
80x faster!
Technique 2: Async Generation
User clicks print
Immediately: Show "PDF generating..."
Background: Generate PDF (8 seconds)
User: Continue working
Email: PDF sent when ready
Zero waiting!
Technique 3: Optimize QWeb Template
Remove unnecessary calculations
Inline CSS instead of external
Reduce DOM complexity
Result: Faster rendering
Technique 4: Batch Generation
Generate 100 PDFs at once
Process in background
Complete in 2 minutes
Email all at once
Caching PDF Reports
Cache Single PDF
from odoo import models, api
from odoo.tools import ormcache
import hashlib
class AccountMove(models.Model):
_inherit = 'account.move'
pdf_cache = {} # In-memory cache
def get_pdf(self):
"""Get or generate PDF (cached)."""
# Create cache key from invoice data
cache_key = self._get_pdf_cache_key()
# Check cache
if cache_key in self.pdf_cache:
return self.pdf_cache[cache_key]
# Generate PDF
pdf_data = self._generate_pdf()
# Store in cache
self.pdf_cache[cache_key] = pdf_data
return pdf_data
def _get_pdf_cache_key(self):
"""Generate cache key from invoice state."""
# Cache only if invoice is finalized (won't change)
if self.state not in ['posted', 'cancel']:
return None
# Key = invoice ID + state + amount
key_data = f"{self.id}_{self.state}_{self.amount_total}"
return hashlib.md5(key_data.encode()).hexdigest()
def _generate_pdf(self):
"""Generate PDF from QWeb template."""
report = self.env.ref('account.report_invoice')
return report.render_qweb_pdf(self.ids)[0]
def invalidate_pdf_cache(self):
"""Clear cache when invoice changes."""
cache_key = self._get_pdf_cache_key()
if cache_key in self.pdf_cache:
del self.pdf_cache[cache_key]
Redis-Backed Cache (For Multi-Server)
from odoo.tools import ormcache
import redis
import pickle
class AccountMove(models.Model):
_inherit = 'account.move'
@ormcache('self.id')
def get_pdf_cached(self):
"""Get PDF with Redis caching."""
# Connect to Redis
r = redis.Redis(host='localhost', port=6379, db=0)
cache_key = f"invoice_pdf_{self.id}"
# Try cache
cached = r.get(cache_key)
if cached:
return pickle.loads(cached)
# Generate
pdf_data = self._generate_pdf()
# Store in Redis (expire after 1 hour)
r.setex(cache_key, 3600, pickle.dumps(pdf_data))
return pdf_data
Async PDF Generation with Background Jobs
Install queue_job
pip install odoo-bin-addons-queue-job
Use in Model
from odoo_bin_addons import queue_job
from odoo import models, api
class AccountMove(models.Model):
_inherit = 'account.move'
def action_print_async(self):
"""Print PDF asynchronously."""
# Enqueue job
self.with_delay(priority=10).generate_pdf_job()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'PDF Generation',
'message': 'Invoice PDF is being generated. You will receive email when ready.',
'sticky': False,
}
}
@job
def generate_pdf_job(self):
"""Generate PDF in background."""
# Generate PDF
report = self.env.ref('account.report_invoice')
pdf_data, _ = report.render_qweb_pdf(self.ids)
# Save to attachment
attachment = self.env['ir.attachment'].create({
'name': f'{self.name}.pdf',
'type': 'binary',
'datas': base64.b64encode(pdf_data),
'res_model': self._name,
'res_id': self.id,
})
# Send email with PDF
template = self.env.ref('account.email_template_invoice')
template.send_mail(
self.id,
email_values={'attachment_ids': [attachment.id]},
force_send=True
)
self.message_post(body="Invoice PDF generated and sent")
Batch Async Generation
def action_generate_batch_pdfs(self):
"""Generate PDFs for multiple invoices."""
invoices = self.env['account.move'].search([
('state', '=', 'posted'),
('invoice_date', '=', fields.Date.today()),
])
# Enqueue batch job
batch_size = 50
for i in range(0, len(invoices), batch_size):
batch = invoices[i:i+batch_size]
self.with_delay(priority=5).generate_batch_pdfs_job(batch.ids)
return "Enqueued %d PDF generations" % len(invoices)
@job
def generate_batch_pdfs_job(self, invoice_ids):
"""Generate PDFs for batch of invoices."""
invoices = self.env['account.move'].browse(invoice_ids)
report = self.env.ref('account.report_invoice')
# Generate all at once (more efficient)
pdf_data, _ = report.render_qweb_pdf(invoices.ids)
# For each invoice, extract and attach
# (implementation depends on report structure)
Real D2C Example: Complete Optimized Report System
Scenario: E-commerce needs to generate invoices for 500 daily orders.
from odoo import models, fields, api
from odoo_bin_addons import queue_job
import base64
class SaleOrder(models.Model):
_inherit = 'sale.order'
# Track invoice generation status
invoice_pdf_status = fields.Selection([
('pending', 'Pending'),
('generating', 'Generating'),
('ready', 'Ready'),
('sent', 'Sent'),
], default='pending')
invoice_pdf = fields.Binary(
string='Invoice PDF',
attachment=True
)
def action_confirm(self):
"""Confirm order and generate invoice async."""
result = super().action_confirm()
# Create invoice
self._create_invoices()
# Generate PDF asynchronously
invoice = self.invoice_ids[0]
invoice.with_delay(priority=10).generate_invoice_pdf_async()
return result
def action_generate_all_daily_invoices(self):
"""Batch generate all invoices for today."""
orders = self.search([
('order_date', '=', fields.Date.today()),
('state', '=', 'sale'),
('invoice_ids', '!=', False),
])
invoices = orders.mapped('invoice_ids')
# Enqueue batch job
batch_size = 50
total = len(invoices)
for i in range(0, total, batch_size):
batch = invoices[i:i+batch_size]
self.env['account.move'].with_delay(
priority=5
).generate_batch_invoices_async(batch.ids)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Batch PDF Generation',
'message': f'Enqueued {total} invoices for PDF generation. You will be notified when complete.',
'sticky': False,
}
}
class AccountMove(models.Model):
_inherit = 'account.move'
pdf_status = fields.Selection([
('pending', 'Pending'),
('generating', 'Generating'),
('ready', 'Ready'),
], default='pending')
@job(default_channel='root.invoice_pdf')
def generate_invoice_pdf_async(self):
"""Generate PDF in background."""
self.pdf_status = 'generating'
# Generate PDF with caching
pdf_data = self.get_pdf_cached()
# Save as attachment
attachment = self.env['ir.attachment'].create({
'name': f'{self.name}_invoice.pdf',
'type': 'binary',
'datas': base64.b64encode(pdf_data),
'res_model': 'account.move',
'res_id': self.id,
})
# Update status
self.pdf_status = 'ready'
# Send email
self._send_invoice_email(attachment.id)
@job(default_channel='root.batch_pdf')
def generate_batch_invoices_async(self, invoice_ids):
"""Generate batch of invoices."""
invoices = self.env['account.move'].browse(invoice_ids)
for invoice in invoices:
invoice.generate_invoice_pdf_async()
return f"Generated {len(invoices)} PDFs"
QWeb Template Optimization
Optimize Template for Speed
<t t-foreach="order.order_line" t-as="line">
<!-- Complex calculations in template -->
<tr>
<td>
<!-- Load product info (causes queries!) -->
<span t-field="line.product_id.name"/>
<span t-field="line.product_id.categ_id.name"/>
<!-- Format price (expensive) -->
<span t-esc="'{:,.2f}'.format(line.price_total)"/>
</td>
</tr>
</t>
<t t-foreach="lines_data" t-as="line">
<!-- Pre-calculated data -->
<tr>
<td t-esc="line['product_name']"/>
<td t-esc="line['category_name']"/>
<td t-esc="line['price_formatted']"/>
</tr>
</t>
Pre-Calculate Data Before Rendering
def get_pdf_with_precalc(self):
"""Generate PDF with pre-calculated data."""
# Pre-calculate complex data
lines_data = []
for line in self.order_line:
lines_data.append({
'product_name': line.product_id.name,
'category_name': line.product_id.categ_id.name,
'price_formatted': '{:,.2f}'.format(line.price_total),
'qty': line.product_qty,
})
# Pass pre-calculated data to template
report = self.env.ref('sale.report_order')
# Custom context with pre-calculated data
pdf_data = report.render_qweb_pdf(self.ids,
data={'lines_data': lines_data}
)[0]
return pdf_data
Your Action Items
Immediate (Reduce Latency)
❏ Implement PDF caching (cache_key based on state)
❏ Test single invoice generation time
❏ Aim for < 2 seconds
Short-term (Async Processing)
❏ Install queue_job
❏ Convert print to async action
❏ Test batch generation (50+ at once)
❏ Measure time (should be < 30 seconds per 50)
Optimize Further
❏ Optimize QWeb template (remove queries from template)
❏ Pre-calculate complex data
❏ Reduce font size (embed minimal fonts)
❏ Monitor PDF generation logs
Free PDF Report Optimization Workshop
Stop waiting for reports to generate. Most D2C brands generate 50-100 PDFs daily with no optimization. Proper tuning reduces generation time from hours to minutes. Cost: 6-8 hours consulting. Value: $100,000+ in productivity gains. We'll profile your slowest reports, implement caching strategy, set up async generation, optimize QWeb templates, and test batch operations.
