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.
https://c..visual.force.com/apex/DiscountScheduleView
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.
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.
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.
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.
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.
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;
}
}
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.
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!