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.
Key Points for Modern Implementation
- No More JavaScript Buttons: In Salesforce Classic, developers used
OnClick JavaScriptbuttons. 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
QuoteDocumentobject and send it viaSingleEmailMessagein 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.
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.
- 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 usingTest.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.
- 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).