Custom Visualforce Page for CPQ Quote
Business Use Case
Business Solution
- Configure the CPQ Quote Template to refer to your Visualforce page.
- Write a Visualforce page that renders your custom content.
- Write an Apex Controller for your Visualforce page.
- 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.
- A little more detail and tips about this statement.
- 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.
- You need a Visualforce page, not a Visualforce component.
- 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.
- content/type=”text/xml” required to include in CPQ Quote Template
- showHeader=”false” since there Quotes have no headers.
- 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.
- FormatDetails is a Custom Metadata Type with the following fields
- TableFontFamily – name of default Font to use. For instance – Helvetica, Arial, etc.
- TableFontSize – size of default Font in pixels. For instance – 9px
- TableTextColor – default color of text in quote template. This can either be the hexidecimal (#808080) or text (black) value.
- TableBorder – formatting for Borders. For example: 1pt solid #1073B9
- Use XML table and block component rather than Apex tags for it to render properly as text/xml.
- IMPORTANT: All content must be enclosed in a block element for it to render correctly.
- 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.
- It is not necessary to extend the Standard Controller
- 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.
- Get the Format Details Metadata that specifies Table Column Headings, Table Font Family, Size and Colors and Border Values for use in the page.
- Get the QuoteLineItems and create a Map keyed off Discount Schedule Ids.
- Then we use the keys from that Map to get the Discount Schedule records with the Tier Data values.
- 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
- Configuring the “Assign using active assignment rules” checkbox
- How to Convert a Lead In Use By a Time-Based Workflow in Salesforce
- Tracking Stage or Status Changes in Salesforce
- Understanding “Cannot Specify Id” on Insert Exception
- Aggregate Query has too many rows for direct assignment, use FOR loop