SfSkills Domains Docs GitHub

Home / apex / quote-pdf-customization

quote-pdf-customization

Customizing Salesforce Quote PDFs using Visualforce: custom VF-based quote templates, dynamic section rendering, multi-language layouts, logo placement via static resources, and programmatic PDF generation with PageReference.getContentAsPDF(). Use when standard declarative quote templates are insufficient or when CPQ/OmniStudio is not licensed. NOT for LWC-based document generation (use omnistudio/document-generation-omnistudio). NOT for OmniStudio DocGen templates. NOT for standard quote template drag-and-drop editor.

Open the skill folder on GitHub


Quote PDF Customization

This skill activates when a Salesforce org needs to generate customized Quote PDFs that go beyond what the standard declarative quote template editor supports. It covers Visualforce-based template authoring, dynamic section rendering, logo and CSS placement, multi-language layouts, and programmatic PDF generation via PageReference.getContentAsPDF().


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Concept 1: Visualforce renderAs="pdf" and the Flying Saucer Renderer

Adding renderAs="pdf" to <apex:page> causes Salesforce to pass the rendered HTML through the Flying Saucer library (based on iText) before returning the response to the browser. The critical consequences are:

Concept 2: Controller Choices — Standard Controller vs. Custom Apex

A VF quote PDF page can use either a standard controller (standardController="Quote") with an extension, or a fully custom controller.

Controller TypeUse WhenWhat You Get
Standard Controller + ExtensionStandard Quote + QuoteLineItem; simple branded layoutRecord context auto-bound from URL Id; FLS on bound fields; less boilerplate
Custom Apex ControllerCPQ Quotes (SBQQ__QuoteLine__c); complex aggregation; multi-object dataFull SOQL flexibility; must manually enforce FLS/CRUD

Use WITH USER_MODE on SOQL queries (available Summer '23+) to enforce FLS at the database level automatically and avoid manual Schema checks.

Concept 3: Programmatic PDF Generation with PageReference

PageReference.getContentAsPDF() allows server-side Apex code to generate a PDF binary from a VF page and store or email it without user interaction. This is used in scheduled batch jobs, trigger-initiated quote finalization, or Send Quote button overrides.

Key behaviors:

Concept 4: Multi-Language and Conditional Section Rendering

Enterprise quote PDFs often require per-customer language variants and conditional sections (e.g., show discount table only if any line has a non-zero discount).

Implement this using:


Common Patterns

Pattern 1: Custom VF Quote Template with Standard Controller Extension

When to use: The org uses standard Salesforce Quotes (not CPQ) and needs a branded, formatted PDF beyond what the declarative template editor supports.

How it works:

  1. Create a Visualforce page with <apex:page standardController="Quote" extensions="QuotePdfController" renderAs="pdf" showHeader="false" sidebar="false">.
  2. In QuotePdfController, query QuoteLineItem records with WITH USER_MODE and expose Boolean section-toggle properties.
  3. Build the logo URL as an absolute URL in Apex — do not use {!$Resource.Logo} directly in <img src>.
  4. Use inline CSS table layout for line-item columns (avoid Flexbox/Grid).
  5. Use rendered="{!property}" on conditional sections — never display:none.

Why not the alternative: The standard declarative quote template editor does not support conditional sections, custom grouping logic, or CPQ line items. JavaScript for dynamic behavior produces blank sections in the PDF because the renderer skips JS entirely.

Pattern 2: Programmatic PDF Attachment via Queueable

When to use: The PDF must be automatically generated and attached to the Quote when it reaches a certain stage, without user interaction.

How it works:

  1. A process (Flow or trigger) enqueues a QuotePdfAttachmentJob Queueable class implementing Database.AllowsCallouts, passing the Quote Id.
  2. The Queueable builds a PageReference to the VF page, calls .getContentAsPDF(), checks for null, creates a ContentVersion, and links it to the Quote via ContentDocumentLink.
  3. Error handling must log failures — check for null blob explicitly before inserting.

Why not the alternative: Calling getContentAsPDF() inside a trigger violates the callout-after-DML restriction and throws a CalloutException. It must be offloaded to an async context.

Pattern 3: Multi-Language Quote PDF

When to use: Sales operates across multiple countries and each quote must be rendered in the customer's language.

How it works:

  1. Store all label strings in Salesforce Custom Labels. Enable Translation Workbench and add translations per supported locale.
  2. Reference labels via {!$Label.QuoteTemplate_ProductColumn} — Salesforce renders the label in the page's locale context.
  3. Set the page locale dynamically: <apex:page language="{!pageLanguage}"> where pageLanguage is resolved in Apex from the Contact's LanguageLocaleKey or a custom field.
  4. For non-Latin scripts, embed a Unicode font (@font-face + base64) in a CSS static resource.

Why not the alternative: Hard-coding label text in the VF markup creates one template per language, multiplying maintenance burden. Custom Labels with Translation Workbench keep all text in a single template that adapts at render time.


Decision Guidance

SituationRecommended ApproachReason
Standard Quote, basic brandingStandard Controller + VF extension, renderAs="pdf"Minimal complexity; FLS on bound fields handled automatically
CPQ Quote with SBQQ__QuoteLine__cCustom Apex controller querying CPQ objectsStandard controller cannot traverse CPQ line objects
Auto-attach PDF on stage changeQueueable implementing Database.AllowsCalloutsCallouts not allowed in trigger context; async required
Multi-language outputCustom Labels + Translation Workbench + <apex:page language=...>Single template, platform-managed translations
Logo not appearing in PDFEmbed as base64 data URI or build absolute URL in ApexPDF renderer cannot follow relative Salesforce resource URLs
Complex column layout neededCSS table layout (display:table, display:table-cell)Flexbox and Grid not supported by Flying Saucer renderer

Step-by-step instructions for an AI agent or practitioner working on this task:

  1. Confirm the quoting object — Determine whether the org uses standard Quote + QuoteLineItem or Salesforce CPQ SBQQ__Quote__c + SBQQ__QuoteLine__c. This controls which controller pattern is required.
  2. Design the controller — For standard Quotes, scaffold a standardController="Quote" page with a read-only Apex extension. For CPQ Quotes, write a custom controller. Use WITH USER_MODE in all SOQL to enforce FLS automatically.
  3. Set up static resources — Upload logo images and custom fonts as static resources. Plan to embed images as base64 data URIs inside a CSS static resource, or build an absolute URL in Apex pointing to the static resource.
  4. Author the VF page — Add renderAs="pdf" showHeader="false" sidebar="false" to <apex:page>. Use CSS 2.1 table-based layout. Add rendered="{!booleanProp}" on conditional sections. Include no <script> tags.
  5. Test PDF output iteratively — Append ?id=<QuoteId> to the VF page URL and view in a browser. Toggle renderAs="pdf" on and off to compare HTML vs. PDF rendering. Verify logo visibility, column alignment, and page break behavior.
  6. Implement delivery mechanism — If the PDF must be programmatically generated, build a Queueable implementing Database.AllowsCallouts that calls getContentAsPDF(), checks for a non-null blob, and inserts a ContentVersion + ContentDocumentLink linked to the Quote.
  7. Validate with a non-admin user — Run the page as a user with only the minimum Quote Read permission. Confirm no data leaks through the controller and no FLS violations appear in debug logs.

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. {!$Resource.Logo} produces a relative URL — the PDF renderer cannot follow it — $Resource.Logo resolves to a path like /resource/1234/Logo.png. The Flying Saucer renderer treats this as a local file reference and fails silently. Build an absolute URL in Apex using URL.getSalesforceBaseUrl().toExternalForm() or embed the image as a base64 data URI.
  2. getContentAsPDF() returns null without throwing when the VF page errors — If the VF page throws an unhandled exception, getContentAsPDF() returns null instead of re-throwing. Code that skips the null check will silently insert a 0-byte ContentVersion. Always assert blob != null before proceeding.
  3. Standard Quote templates cannot render CPQ line items — The standard VF template or standardController="Quote" only sees QuoteLineItem. CPQ stores lines in SBQQ__QuoteLine__c. A custom Apex controller is mandatory for any CPQ quote PDF.
  4. page-break-inside: avoid is unreliable on <tr> elements — Flying Saucer inconsistently honors this property on table rows. Wrap each repeated row's content in a <div style="page-break-inside: avoid;"> inside the <apex:repeat> to prevent mid-row page breaks.
  5. getContentAsPDF() counts as a callout against the 100-callout limit — Each call counts as one callout. Batch generation in a single transaction will hit this cap. Use scope size of 1 in Batch Apex or chain Queueables so each transaction generates only one PDF.

Output Artifacts

ArtifactDescription
Visualforce page.page file with renderAs="pdf", conditional sections, and CSS table layout
Apex controller/extensionClass querying Quote + lines with FLS-safe SOQL and Boolean section-toggle properties
CSS static resourceStylesheet with table-based layout, @font-face for non-Latin scripts, and logo as base64
Queueable PDF attachment classAsync Apex calling getContentAsPDF(), checking null, and linking a ContentVersion to the Quote
Checker script outputList of metadata issues found by check_quote_pdf_customization.py

Package files