function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Sammy7Sammy7 

Steps to generate a custom quote/invoice pdf

Hi guys, the quote template has lots of limitations when it comes to styling; I would like to do something custom and need a step-by-step guide. Basically, I would like a "create quote" button on quote pagelayout that references my visualforce page and creates the pdf. 

So I think I need to follow the steps below:
1. Create a custom controller class. Can I do this with standard List controller? Basically, I have to query all the quote and quotelineitems
2. Create a new visual force page using controller from step1, add the proper styling and render as pdf.
3. Create the button on quote with a link to visual force page
Any tips and guides are appreciated. 
Thanks.

Did I miss anything? 
 
NagendraNagendra (Salesforce Developers) 
Hi Sammy,

Quotes have a powerful built-in tool in Salesforce to generate PDF files based on templates. In this post I will explain how you can take advantage of this feature to generate Invoices. In most cases, the template used for a Quote is different from the one used for an Invoice. Salesforce only brings you the option to create Quote PDFs within the Quote object and it doesn’t allow you to change the name of the file. Below I will explain how you can generate Invoice PDFs files based on Quote templates from any Object in Salesforce by just clicking a custom button.

Requirements: Features:
  • Hack Quote Templates to dynamically generate PDFs.
  • Easily generate Invoice PDF files based on the synced or latest created Quote.
  • Save PDF as an Attachment, allowing you to change the name of the file.
  • No need to use third party libraries.
How it works:
  • I’m using PageReference.getContent() method to retrieve the PDF blob. Ideally, this logic would happen within a trigger context or in a background process. However, getContent() method cannot be used in Triggers, Scheduled Apex or Batch jobs. This only gives us the option of using an Apex Controller class.
The Quote Template

Create a new Quote Template:
  • Go to App Setup > Customize > Quotes > Templates.
  • Click New.
  • For “Template Name”, use “Invoice”.
  • Click Save and customize your template.
  • Take a look at the URL and copy the template Id. The parameter name is named “summlid”.
  • Once you are done, click Save and in the next screen remember to Activate the new template.
The Custom Setting:

We will need a Custom Setting to manage the Invoice PDF generation. I named it “Application Properties” and it has one single field named “value” (String [255]). These are the required records:
  • Invoice_Footer_Height: 100 by default. You will have to play with this value as it depends on how long is the header of your template.
  • Invoice_Header_Height: 100 by default. You will have to play with this value as it depends on how long is the footer of your template.
  • Invoice_Template_Id: Salesforce Id of the template. Is the same value as the “summlid” parameter from the previous step.
  • Quote_Template_Data_Viewer_URL: URL that Salesforce uses to generate Quote PDF files. The value is:
  • /quote/quoteTemplateDataViewer.apexp?id={!QuoteId}&headerHeight={!InvoiceHeaderHeight}&footerHeight={!InvoiceFooterHeight}&summlid={!InvoiceTemplateId}
  • In the Code Resources section you will find a sample CSV file.
The Controller:
  • I’ve named the controller ‘InvoicePdfWsSample’. Is a global class because it has a webService method. This method receives a list of Opportunities Ids and generates an Attachment with the Invoice PDF file for each one.
/**
 * @author      Valnavjo <valnavjo_at_gmail.com>
 * @version     1.0.0
 * @since       10/08/2014
 */
global with sharing class InvoicePdfWsSample {
    /**
     * Default header height for invoice
     */
    private static final String DEFAULT_INVOICE_HEADER_HEIGHT = '100';
     
    /**
     * Default footer height for invoice
     */
    private static final String DEFAULT_INVOICE_FOOTER_HEIGHT = '100';
     
    /**
     * Webservice method that is called from a custom button to generate
     * an invoice PDF file using quote templates feature.
     * It generates the invoice based on:
     *      - The synced Quote, or
     *      - The latest Quote
     * If the Opportunity doesn't have any Quotes, this method doesn't do
     * anything.
     * 
     * This method uses PageReference.getContent().
     *
     * @param oppsIdList {List<Id>} list of Opportunity Ids from where the method
     *                   will generate the Invoice PDF.
     * @return {String} with an error message, if any. Blank otherwise.
     */
    webService static String generateInvoicePdf(List<Id> oppsIdList) {
        try {
            //From list to set
            final Set<Id> oppsId = new Set<Id>(oppsIdList);
 
            //Get template Id for Invoice and url to hack pdf generation
            final String invoiceTemplateId = Application_Properties__c.getAll().get('Invoice_Template_Id').value__c;
            String invoiceHeaderHeight = Application_Properties__c.getAll().get('Invoice_Header_Height').value__c;
            String invoiceFooterHeight = Application_Properties__c.getAll().get('Invoice_Footer_Height').value__c;
            final String quoteTemplateDataViewerUrl = Application_Properties__c.getAll().get('Quote_Template_Data_Viewer_URL').value__c;
             
            //Pre-validations
            //Invoice_Template_Id and Quote_Template_Data_Viewer_URL are mandatory 
            if (String.isBlank(invoiceTemplateId) || String.isBlank(quoteTemplateDataViewerUrl)) {
                String errorMsg = 'Invoice Template Id or Quote Template Data Viewer URL are blank, please review their values in Application Properties custom setting.';
 
                return errorMsg;
            }
             
            //Default values for invoice header/footer height
            if (String.isBlank(invoiceHeaderHeight)) invoiceHeaderHeight = DEFAULT_INVOICE_HEADER_HEIGHT;
            if (String.isBlank(invoiceFooterHeight)) invoiceFooterHeight = DEFAULT_INVOICE_FOOTER_HEIGHT; 
 
            //Iterate over Opps and generate Attachments list
            final List<Attachment> attList = new List<Attachment>();
            for (Opportunity opp : [select Id,
                                           (select Id, Invoice_number__c, IsSyncing, CreatedDate
                                            from Quotes
                                            order by CreatedDate DESC)
                                    from Opportunity
                                    where Id IN :oppsId]) {
                //No Quotes, no party
                if (opp.Quotes.isEmpty()) continue;
 
                //Synced quote
                Quote theQuote = null;
 
                //Try to get the synced one
                for (Quote quoteAux : opp.Quotes) {
                    if (quoteAux.IsSyncing) {
                        theQuote = quoteAux;
                        break;
                    }
                }
 
                //No synced Quote, get the last one
                if (theQuote == null) theQuote = opp.Quotes.get(0);
 
                PageReference pageRef = new PageReference(
                    quoteTemplateDataViewerUrl.replace('{!QuoteId}', theQuote.Id)
                                              .replace('{!InvoiceHeaderHeight}', invoiceHeaderHeight)
                                              .replace('{!InvoiceFooterHeight}', invoiceFooterHeight)
                                              .replace('{!InvoiceTemplateId}', invoiceTemplateId)
                );
 
                attList.add(
                    new Attachment(
                        Name = 'Invoice #' + theQuote.Invoice_number__c + '.pdf',
                        Body = pageRef.getContent(),
                        ParentId = opp.Id
                    )
                );
            }
 
            //Create Attachments
            if (!attList.isEmpty()) insert attList;
             
            return '';
        } catch (Exception e) {
            System.debug(LoggingLevel.ERROR, e.getMessage());
 
            final String errorMsg = 'An error has occured while generating the invoice. Details:\n\n' +
                                     e.getMessage() + '\n\n' +
                                     e.getStackTraceString();
             
            return errorMsg;
        }
    }
}
The Custom Button:
  • Create a new custom button on Opportunity:
  • Go to App Setup > Customize > Opportunities > Buttons, Links, and Actions.
  • Click New Button or Link.
  • For “Label”, use “Generate Invoice”. Leave “Name” as default.
  • For “Display Type”, select “Detail Page Button”.
  • For “Behavior”, select “Execute JavaScript”.
  • For “Content Source”, select “OnClick JavaScript”.
  • Use the following JavaScript code and click “Save”.
{!REQUIRESCRIPT("/soap/ajax/29.0/connection.js")}
{!REQUIRESCRIPT("/soap/ajax/29.0/apex.js")}
 
var oppId = '{!Opportunity.Id}';
var errorMsg = sforce.apex.execute("InvoicePdfWsSample", "generateInvoicePdf", {oppsIdList:oppId});
 
if (errorMsg != '') alert(errorMsg);
else window.location.reload();
The code is simply getting the Opportunity Id and calling the webService method named “generateInvoicePdf” using the AJAX Toolkit.

User-added imageUser-added imageUser-added image

Kindly mark this post as solved if the information help's so that it gets removed from the unanswered queue and becomes a proper solution which results in helping others who are really in need of it.

Best Regards,
Nagendra.P


 
Sammy7Sammy7
Hi Nagendra,
 I am well aware of this work around of using the quote template, but I need a better solution as I cannot change the font size and formatting in the out of box quote template in salesforce and I don't want to pay for third party app for a one time thing. 
 
Kushal DoshiKushal Doshi
Hi Nagendra & Sammy,

I have the same issue.

Could we do this on the Order object by tweaking the code?

Waiting in anticipation.

Regards,

Kushal
Jos Vervoorn 2Jos Vervoorn 2
Would be great to have this working on Lightning ...
Chetan KapaniaChetan Kapania
Hi Nagendra, 

I tried the steps mentioned but getting confused with the coding part. I have created Application Properties under Custom Settings but while saving the code it says Variable does not exist: Application_Properties__c. Any reason this might be happening. 

Regards
Chetan
uuuu
Hii Nagendra,
Is this useful for custom object?
If yes then how i suppose to do this.
Please give step by step guidance.
Thanks
Anita
V TV T
I also had a similar requirement. I found solution here...Save, send Preview PDF (https://salesforcetrail.blogspot.com/2016/12/how-to-save-quote-pdf-send-pdf-preview.html)
V TV T
link has been updated. Best Regards, [image: 珞] *Vinod Tiwari*Mobile:- +91-9719764989 Skype:- vtfriday