Skip to main content

How to Build Custom Quote PDF Preview, Auto-Save, and Email in Salesforce

Salesforce offers a standard PDF generation tool for Quotes, but it often falls short when you need complex branding, custom data tables, or dynamic terms and conditions. While building a custom Visualforce PDF solves the design problem, you still need a seamless way for sales reps to generate, save, and email that document to the customer without jumping through manual hoops.

In plain words: Instead of making your sales team manually generate a PDF, download it, open their email client, attach it, and type a message—you can build a custom Quick Action in Salesforce Lightning. With one click, Apex code will generate your custom PDF, attach it to the Quote record, and email it directly to the customer.
Custom Quick Action Button to Save and Send Quote PDF in Salesforce Lightning

Key Points for Modern Implementation

  • No More JavaScript Buttons: In Salesforce Classic, developers used OnClick JavaScript buttons. These do not work in Salesforce Lightning. Today, we use Lightning Web Components (LWC) Quick Actions or Screen Flows.
  • Visualforce is Still Relevant: While LWC is the standard for UI, Visualforce remains the officially supported way to generate server-side PDFs using the renderAs="pdf" attribute.
  • Automation Saves Time: You can use Apex to insert the generated PDF binary directly into the QuoteDocument object and send it via SingleEmailMessage in a single transaction.

Step 1: Write the Lightning-Ready Apex Controller

We need an Apex class that can be called from our modern Lightning UI. It will compile the Visualforce PDF, save it to the Quote, and send an email. Notice the use of @AuraEnabled instead of the outdated webservice keyword.

Real-Life Example: Save and Email Handler
This controller pulls the Quote and Contact info, generates the PDF Blob from your custom Visualforce page, and dispatches the email.
public with sharing class QuotePDFController {

    @AuraEnabled
    public static String attachAndSendQuotePDF(Id quoteId) {
        try {
            // 1. Fetch Quote and related Contact details
            Quote objQuote = [SELECT Id, Name, Status, Contact.Name, Email 
                              FROM Quote WHERE Id = :quoteId LIMIT 1];
            
            // Basic validation
            if (String.isBlank(objQuote.Email)) {
                return 'Error: Please ensure the Quote has an associated Email address.';
            }
            if (objQuote.Status != 'Approved') {
                return 'Error: Quote must be Approved before sending.';
            }

            // 2. Reference your custom Visualforce PDF Page
            PageReference pageRef = new PageReference('/apex/Your_Custom_Quote_PDF?Id=' + quoteId);
            Blob pdfContent;
            
            // Test classes cannot run getContentAsPDF(), so we mock it
            if (Test.isRunningTest()) {
                pdfContent = Blob.valueOf('Mock PDF Content for Testing');
            } else {
                pdfContent = pageRef.getContentAsPDF();
            }
            
            // 3. Save PDF to the QuoteDocument Related List
            QuoteDocument doc = new QuoteDocument(
                Document = pdfContent, 
                QuoteId = quoteId
            );
            insert doc;

            // 4. Configure and Send Email
            Messaging.SingleEmailMessage emailMsg = new Messaging.SingleEmailMessage();
            Messaging.EmailFileAttachment emailAttach = new Messaging.EmailFileAttachment();
            
            emailAttach.setFileName(objQuote.Name + ' - Quotation.pdf');
            emailAttach.setBody(pdfContent);
            
            String emailBody = 'Hi ' + objQuote.Contact.Name + ',\n\n'
                             + 'Please find your requested quote attached: ' + objQuote.Name + '.\n\n'
                             + 'Best regards,\nYour Sales Team';
            
            emailMsg.setSubject('Your Quotation: ' + objQuote.Name);
            emailMsg.setToAddresses(new List<String>{ objQuote.Email });
            emailMsg.setPlainTextBody(emailBody);
            emailMsg.setFileAttachments(new Messaging.EmailFileAttachment[] { emailAttach });
            
            Messaging.sendEmail(new Messaging.SingleEmailMessage[] { emailMsg });
            
            return 'Success! Quote generated and sent to ' + objQuote.Email;
            
        } catch (Exception ex) {
            return 'Error processing Quote: ' + ex.getMessage();
        }
    }
}

Step 2: Create an LWC Quick Action (The Modern Button)

To trigger this code in Salesforce Lightning, you should create a Headless Lightning Web Component (LWC) Quick Action. It simply calls your Apex method and shows a success or error toast to the user.

import { LightningElement, api } from 'lwc';
import attachAndSendQuotePDF from '@salesforce/apex/QuotePDFController.attachAndSendQuotePDF';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { CloseActionScreenEvent } from 'lightning/actions';

export default class SendQuoteQuickAction extends LightningElement {
    @api recordId; // Automatically gets the Quote Id

    @api invoke() {
        attachAndSendQuotePDF({ quoteId: this.recordId })
            .then(result => {
                if(result.includes('Error')) {
                    this.showToast('Warning', result, 'warning');
                } else {
                    this.showToast('Success', result, 'success');
                }
                this.dispatchEvent(new CloseActionScreenEvent());
            })
            .catch(error => {
                this.showToast('Error', error.body.message, 'error');
                this.dispatchEvent(new CloseActionScreenEvent());
            });
    }

    showToast(title, message, variant) {
        this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
    }
}

Once deployed, you simply add this LWC as a Quick Action to your Quote Page Layout in the Lightning App Builder.

Common Developer Traps:
  • Heap Limits: The getContentAsPDF() method loads the entire rendered PDF into the Apex heap memory. If your PDF has massive high-res images, it will crash. Compress your logos!
  • Test Class Failures: As shown in the Apex code, calling getContentAsPDF() inside a test class throws a fatal error. You must bypass it using Test.isRunningTest().
  • Hardcoding URLs: Never hardcode your Salesforce instance URL when referencing the Visualforce page. Just use /apex/PageName.

Frequently Asked Questions

Can I use a Screen Flow instead of building an LWC?

Absolutely! If you prefer low-code, you can change the @AuraEnabled annotation to @InvocableMethod. This allows you to place the Apex class directly inside a Screen Flow action, making it easy for Admins to maintain the user interface.

Is it possible to preview the PDF before sending?

Yes. The simplest way to handle this in Lightning is to add a separate Quick Action that redirects the user to the Visualforce page URL (using target="_blank" to open in a new tab) so they can review it before hitting the "Save & Send" button.

Can I attach the PDF to standard Salesforce Files instead of QuoteDocument?

Yes. While QuoteDocument is the legacy standard for Quotes, modern orgs often prefer standard Files (ContentVersion). You can easily modify the Apex to insert a ContentVersion record and link it to the Quote using a ContentDocumentLink.

Always include business logic checks (like ensuring the Quote Status is 'Approved' or that line items exist) inside your Apex code before generating and sending the PDF to clients.
360 Summary Card
  • Target Objects: Quote, QuoteDocument, Contact
  • Core PDF Method: PageReference.getContentAsPDF()
  • Email Delivery: Messaging.SingleEmailMessage
  • Modern UI Standard: LWC Quick Actions or Screen Flows (Do NOT use Classic JavaScript buttons).