Wednesday, April 11, 2012

Generating Salesforce.com Quotes PDF with neat page breaks

I love Salesforce, but I fail to understand why the basic functionality like generating a presentable quotes PDF is not part of the standard out of the box functionality. Many of you may disagree because, salesforce.com does provide templates to modify the quotes. 

But if you look at it closely, the page breaks are not consistent, if my product description is a little too long, the quote extends to multiple pages, without proper page breaks. So, I searched the internet for few solutions and found 1 good solution on the developer force, to dynamically (almost ;-) ) populate pages with the number of quotes. However, again the drawback is that if the description is too long, the page breaks will not be consistent and the quote will look ugly. 

The final solution I came up with is an extension to an already existing solution on developer force. Though this is not a neat solution, but I guess we have to live with these kind of solutions until Salesforce comes up with a better out of the box quote generation functionality. 

As you would expect, there are 2 components - 
1. Visual Force Page to generate the pdf. 
2. An extension to return the quotes. 


Visual Force Page

<div style="page-break-after:always;"> 
     </div>
   <apex:repeat value="{!pageBrokenQuoteLines}" var="aPageOfQuotes" id="theList">
         <div style="page-break-after:always;"> 
<apex:dataTable value="{!aPageOfQuotes}" var="c" id="theTable">
<apex:column > 
<apex:facet name="header">Qty</apex:facet>
<apex:outputText value="{!c.Quantity}"/>
</apex:column>
<apex:column >
<apex:facet name="header" >Part Number</apex:facet>
<apex:outputText value="{!c.PricebookEntry.Product2.Name}"/>
</apex:column>
<apex:column ></apex:dataTable>

Extension

public with sharing SalesQuoteExample{
public List<QuoteLineItem[]> pageBrokenQuoteLines {get; private set; }
    
  // end new code
  
  public Quote quote { get; private set;} 
    public QuoteLineItem[] quoteLineItems { get; private set; }
    public Opportunity opp { get; private set; }  
    public Boolean hasContact { get { return (opp.opportunityContactRoles.size()  == 1);} }
    public Contact contact { get; private set; }  

    private ApexPages.StandardController controller;
    
      // constructor, loads the quote and any opportunity lines
    void queryQuoteLines(id id) { 
      quote = [  Select s.Contact.Name, Contact.Account.Name, s.Contact.MailingStreet, s.Contact.MailingState,
          s.Contact.MailingPostalCode, s.Contact.MailingCountry, 
        s.Contact.MailingCity , s.Contact.Phone, S.Email,
           (Select Quantity, PricebookEntry.Product2.Name, ListPrice, Adjusted_Price__c, 
           Product_Description__c, TotalPrice, LineNumber  From QuoteLineItems
           order by PricebookEntry.Product2.Name ), 
       s.Opportunity.HasOpportunityLineItem, s.Opportunity.Name, s.Name, s.QuoteNumber, 
       s.Opportunity.Id, s.OpportunityId
       
         From Quote s 
       where s.id = :quoteid limit 1]; 
      quoteLineItems = quote.QuoteLineItems; 
      prepareQuoteLinesForPrinting();
    }
    id quoteid; 
    /*public SalesQuotes() {
      quoteid = ApexPages.CurrentPage().getParameters().get('id');
      init(); 
    }*/
    public SalesQuotes(ApexPages.StandardController c) {
      controller = c;
      quoteid = c.getRecord().id;
      init();
    } 
    
    // load up quote lines, opportunity lines, opportunity details and contact info
    void init() { 
     queryQuoteLines(quoteid);
  }

    /* The action method that will generate a PDF document from the QuotePDF page and attach it to 
       the quote provided by the standard controller. Called by the action binding for the attachQuote
       page, this will do the work and take the user back to the quote detail page. */
    public PageReference attachQuote() {
        /* Get the page definition */
        PageReference pdfPage = new PageReference( '/apex/quotePDFAdvanced' );
        
        /* set the quote id on the page definition */
        pdfPage.getParameters().put('id',quote.id);
        
        /* generate the pdf blob */
        Blob pdfBlob = pdfPage.getContent();
        
        /* create the attachment against the quote */
        Attachment a = new Attachment(parentId = quote.id, name=quote.name + '.pdf', body = pdfBlob);
        
        /* insert the attachment */
        insert a;
        
        /* send the user back to the quote detail page */
        return controller.view();
    }
   
  PageReference opportunityPR() { return new pagereference('/'+quote.OpportunityId); }
  PageReference quotePR() { return new pagereference('/'+quote.id); }
  
   
  public PageReference reset() { 
    queryQuoteLines(quote.id);
    return null;
  }
    //splits the quote lines into an approximate number of rows that can be 
    //displayed per page
   private void prepareQuoteLinesForPrinting()
   {
      pageBrokenQuoteLines = new List<QuoteLineItem[]>();
      Integer linesAvailable = 60;
      
     
     QuoteLineItem[] pageOfQuotes = new QuoteLineItem[]{};
     Integer counter = 0;
     boolean firstBreakFound = false;
     
     //Additional variables
     String description;
     String productName;
     Integer descLines=0;    
     Integer lines; 
     
     for(QuoteLineItem q : quoteLineItems)
     {
       
       System.debug('Quote Line Item is ****'+q.LineNumber);
       description = q.Product_Description__c;
       if(description!=null)
       {
           descLines = description.length()/40;
           System.debug('Lines Available::::'+linesAvailable);
           System.debug('Description Length==='+description.length());
           System.debug('descLines+++'+descLines);
           if(linesAvailable > descLines)
           {
               System.debug('Lines Available');
               linesAvailable = linesAvailable - descLines;
               pageOfQuotes.add(q);
               descLines = 0;
           }
           else
           {
               System.debug('NO Lines Available::::'+linesAvailable);
               pageBrokenQuoteLines.add(pageOfQuotes);
               pageOfQuotes = new QuoteLineItem[]{};
               descLines = 0;
           }
           
       }
    }
     //if we have finished looping and have some quotes left lets assign them
     if(!pageOfQuotes.isEmpty())
     {
       pageBrokenQuoteLines.add(pageOfQuotes);
     }
   }  

}

The method  prepareQuoteLinesForPrinting  is based on the following assumptions
  1. The description will have approximately 40 characters per line.
  2. A page can accommodate 50 lines of quotes.   Can be changed by specifying a new value in the variable.
The logic of the for the method is 
  1. For each quote line item, calculate the description length.
  2. Divide this length by 40, to get an approximate number of lines this quote line item will take. 
  3. Subtract the value from above from the available lines. Available lines indicate the space left in the page to display quote line item.
  4. If the available lines is greater than the description lines(indicating space is available), add this quote on the current page.
  5. Else, add the quote to new page, reset the available lines to 50 (this is configurable).
Except for the logic I have mentioned above, this code is available on the following website, however, you will need to include the logic I have mentioned if you want to have cleaner page breaks


Credits
Authentic Blog, featured by BlogUpp

18 comments:

  1. Thanks for sharing fabulous information.It' s my pleasure to read it.I have also bookmarked you for checking out new posts.
    Digital Marketing Training in Hyderabad


    ReplyDelete