Custom Visualforce Page for CPQ Quote

Business Use Case

Salesforce CPQ makes it easy to create discount schedules for products to determine tiers for volume prices or discounts. Administrators can even provide sales users with the ability to customize the discount tiers for specific users in the Line Editor.
For some businesses, it is important to display rate tables on the quote output documents. This is fairly common for companies that sell consumable services, like voice and data or bandwidth.
The challenge is that for data to be displayed on the Salesforce CPQ output document, the information needs to reside on the Quote or the Quote Line Item object. There is an out-of-the-box feature that allows an admin to display the discount tiers associated with a line, but the formatting is generally unacceptable to most customers and it cannot be reformatted.
However, when creating Template Content, Salesforce CPQ does provide the Custom Content option which enables you to pull in Visualforce content.

Business Solution

Custom Visualforce Page for Steelbrick Quote ApexThe ability to add Visualforce content to a CPQ Quote significantly enhances the Quote Template feature. Here is a walkthrough of what is required along with some common stumbling stones.
For this solution, you will need to complete the following tasks.
  1. Configure the CPQ Quote Template to refer to your Visualforce page.
  2. Write a Visualforce page that renders your custom content.
  3. Write an Apex Controller for your Visualforce page.
  4. Write Custom Metadata to provide formatting details (optional).

CPQ Configuration

According to the CPQ Knowledgebase, the following reference is made to creating Custom content for use in Quote Templates:

Custom: Select this option to use a Visualforce component in the Custom Source field that you want to display in this section of your quote template. You’ll need to enter the full URL for the Visualforce page that generates this content, using the following format (page name should start with the “c__” prefix): https://c.<instance>.force.com/apex/c__OptivTemplateSectionComponent. The VisualForce component needs to be compatible with XML (specifically XSL-FO) to be compatible with CPQ document output.

The  CPQ article can be found here.

  1. A little more detail and tips about this statement.
  2. XSL-FO stands for Extensible Stylesheet Language Formatting Objects. It is a markup language for XML document formatting that is most often used to generate PDF files as is the case here. By writing our Page in XML, it is easily transformed and formatted by the XSL-FO engine into a PDF object.
  3. You need a Visualforce page, not a Visualforce component.
  4. WARNING: The naming for the page is very important and the reference in the documentation above is actually incorrect. The page name must follow this naming convention as outlined in this document.

https://c..visual.force.com/apex/DiscountScheduleView

  • Replace <instance> with your Salesforce instance name.
  • Replace DiscountScheduleView with the name of your Visualforce page

For our business use case, we need a Quote so we can retrieve the related Quote Line Items which are attached to the Discount Schedules which in turn are the parent record of the Tiers.

Visualforce Page

Let’s build out our page. First, start with the Page tag itself as there are special requirements for it. It needs to have the following properties.

  1. content/type=”text/xml” required to include in CPQ Quote Template
  2. showHeader=”false” since there Quotes have no headers.
  3. sidebar=”false” since there Quotes have no sidebars.

Then we add our controller and action method that performs page initialization for us.

<apex:page showHeader="false" sidebar="false" cache="false" 
 contentType="text/xml" controller="DiscountTierCtrl" 
 action="{!init}">

Now we need to add the body of the page.

<apex:page showHeader="false" sidebar="false" cache="false" 
 contentType="text/xml" controller="DiscountTierCtrl" 
 action="{!init}">
 <block>
 <table table-layout="fixed" 
 width="40%" border-bottom-style="solid">
 <table-body>
 <table-row border="{!formatDetails.TableBorder__c}">
 <table-cell display-align="center" padding="5">
 <block text-align="left" 
 font-family="{!formatDetails.TableFontFamily__c}" 
 font-size="{!formatDetails.TableFontSize__c}" font-weight="bold" 
 color="{!formatDetails.TableTextColor__c}" >
 <apex:outputText value="{!formatDetails.TierNameColumHeading__c}">
 </apex:outputText>
 </block>
 </table-cell>
 <table-cell display-align="center" padding="5" 
 border="{!formatDetails.TableBorder__c}">
 <block text-align="left" font-family="{!formatDetails.TableFontFamily__c}" 
 font-size="{!formatDetails.TableFontSize__c}" 
 font-weight="bold" color="{!formatDetails.TableTextColor__c}" >
 <apex:outputText 
 value="{!formatDetails.TierPriceColumnHeading__c}"></apex:outputText>
 </block>
 </table-cell>
 </table-row>
 <apex:repeat var="tier" value="{!discountTiers}">
 <table-row>
 <table-cell display-align="center" padding="5"
 border="{!formatDetails.TableBorder__c}">
 <block font-family="{!formatDetails.TableFontFamily__c}" 
 font-size="{!formatDetails.TableFontSize__c}" 
 color="{!formatDetails.TableTextColor__c}" text-align="left">
 <apex:outputText >{!tier.Name}</apex:outputText>
 </block>
 </table-cell>
 <table-cell display-align="center" 
 border="{!formatDetails.TableBorder__c}" padding="5">
 <block font-family="{!formatDetails.TableFontFamily__c}" 
 font-size="{!formatDetails.TableFontSize__c}" 
 olor="{!formatDetails.TableTextColor__c}" text-align="right">
 <apex:outputText 
 value="{0, number, $###,##0.00}">
 <apex:param value="{!tier.calcUnitPrice}"/>
 </apex:outputText>
 </block>
 </table-cell>
 </table-row>
 </apex:repeat>
 </table-body>
 </table>
 </block>

</apex:page>

Take note of a few things.

  1. FormatDetails is a Custom Metadata Type with the following fields
    1. TableFontFamily – name of default Font to use. For instance – Helvetica, Arial, etc.
    2. TableFontSize – size of default Font in pixels. For instance – 9px
    3. TableTextColor – default color of text in quote template. This can either be the hexidecimal (#808080) or text (black) value.
    4. TableBorder – formatting for Borders. For example:  1pt solid #1073B9
  2. Use XML table and block component rather than Apex tags for it to render properly as text/xml.
  3. IMPORTANT: All content must be enclosed in a block element for it to render correctly.
  4. TIP: Make a single change to your page at a time and preview the Quote Document to ensure you understand the implications of your change and to be able to troubleshoot more effectively.

Apex Controller

Let’s start with the constructor and the initialization method.

public class DiscountTierCtrl {
 public List<DiscountTierWrapper> discountTiers {get; private set;}
 public DiscountTierTableFormatDetails__mdt formatDetails {get; private set;}
 
 protected Id quoteId;

 public DiscountTierCtrl() {
 discountTiers = new List<DiscountTierWrapper>();
 quoteId = (Id)ApexPages.currentPage().getParameters().get('qid');
 }

A few notes about the constructor.

  1. It is not necessary to extend the Standard Controller
  2. The CPQ Quote Template sends the quote Id to us as parameter qid.

Now let’s take a look at the initialization method.

public PageReference init() {
 // Step #1
 formatDetails = [select TableBorder__c, TableFontFamily__c, TableFontSize__c, 
 TableTextColor__c, TierNameColumHeading__c, TierPriceColumnHeading__c 
 from DiscountTierTableFormatDetails__mdt limit 1];

 // Step #2
 try {
 Map<Id, SBQQ__QuoteLine__c> mapQlinesByDiscountSchedule = new Map<Id, SBQQ__QuoteLine__c>();
 for (SBQQ__QuoteLine__c qline :[select Id, 
 SBQQ__DiscountSchedule__c from SBQQ__QuoteLine__c 
 where SBQQ__Quote__c =:quoteId]) {
 mapQlinesByDiscountSchedule.put(qline.SBQQ__DiscountSchedule__c, qline); 
 }
 
 // now lets get our discount tiers
 // Step #3
 Map<Id, List<SBQQ__DiscountTier__c>> mapDiscountTiersByQli = new Map<Id, List<SBQQ__DiscountTier__c>>();
 for (SBQQ__DiscountTier__c discountTier : [select Name, Id, SBQQ__Price__c
 from SBQQ__DiscountTier__c 
 where SBQQ__Schedule__c in :mapQlinesByDiscountSchedule.keyset()]) {
// Step #4
 discountTiers.add(new DiscountTierWrapper(discountTier));
 } 
 return null;
 } catch (Exception exc) {
 String errorMsg = 'There was an error getting Discount Schedules for our Quote. Exception Cause = ' + 
 exc.getCause() + ', Exception Message = ' + exc.getMessage();
 System.debug('=====> ' + errorMsg);
 ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, errorMsg));
 }
 return null;
 }

In the initialization method, we execute the following actions.

  1. Get the Format Details Metadata that specifies Table Column Headings, Table Font Family, Size and Colors and Border Values for use in the page.
  2. Get the QuoteLineItems and create a Map keyed off Discount Schedule Ids.
  3. Then we use the keys from that Map to get the Discount Schedule records with the Tier Data values.
  4. Create a Wrapper to hold the value(s) to be displayed on the page.

Finally, our wrapper class passed to the Visualforce page.

 public class DiscountTierWrapper {
 public String name {get; set;}
 public Decimal calcUnitPrice {get; set;}
 
 public DiscountTierWrapper(SBQQ__DiscountTier__c tier) {
 this.name = tier.Name;
 this.calcUnitPrice = tier.SBQQ__Price__c;
 }
 }

Custom Metadata

To provide the greatest amount of customization in your Visualforce page, it is recommended that you create a custom metadata type to hold formatting details. This will enable Administrators to change the look&feel with point&click precision.

  • Tier Name Column Heading
  • Tier Price Column Heading
  • Table Font Size
  • Table Border
  • Table Font Size
  • Table Text Color

That’s it! Hopefully, this step by step guide will help you avoid the gotchas I hit when building out a custom Visualforce Page to embed in CPQ Quote Templates. Need more custom development with Visualforce and Apex? We can help!

View Our Other Helpful Salesforce Guides