Quote Template – Custom Lines Section

Today, I realized that I’ve not done a custom lines section article! A travesty of justice! Or just an oversight. Either way, here it is! This includes a controller APEX class, a VisualForce page and a Template Content that is of type Custom. Have at it below!

This example is a lines section that groups lines by Product Family. It also rolls up lines by Product! So there will be a section of Hardware, and a rollup of hardware by product within that section (combining quantities of same products).

APEX Class (Controller)

// make sure to put /apex/c__CustomLinesSection in the Custom Source of the Template Content
public class CustomLinesSectionController {
    public Map<String, Map<String, SBQQ__QuoteLine__c>> linesByByProductCodeByProductFamily {get;set;}
    public Map<String, Decimal> netTotalByProductCode {get;set;}
    public Map<String, Decimal> netTotalByFamily {get;set;}
    public CustomLinesSectionController() {
        Id quoteId = (Id)ApexPages.currentPage().getParameters().get('qid');
        linesByByProductCodeByProductFamily = new Map<String, Map<String, SBQQ__QuoteLine__c>>();
        netTotalByProductCode = new Map<String, Decimal>();
        netTotalByFamily = new Map<String, Decimal>();
        for (SBQQ__QuoteLine__c ql : [SELECT Id, SBQQ__Number__c, SBQQ__ListPrice__c, SBQQ__Quantity__c, SBQQ__ProductName__c, SBQQ__ProductCode__c, SBQQ__NetTotal__c, SBQQ__ProductFamily__c FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__c = :quoteId AND SBQQ__Hidden__c = false ORDER BY SBQQ__Number__c]) {
            // by product group.
            String family = ql.SBQQ__ProductFamily__c == null ? 'General' : ql.SBQQ__ProductFamily__c;
            if (!linesByByProductCodeByProductFamily.containsKey(family)) {
                linesByByProductCodeByProductFamily.put(family, new Map<String, SBQQ__QuoteLine__c>());
            }
            
            // by product code.
            String key = (ql.SBQQ__ProductCode__c == null || ql.SBQQ__ProductCode__c == '' ? 'null' : ql.SBQQ__ProductCode__c) + ql.SBQQ__ProductName__c;
            if (!linesByByProductCodeByProductFamily.get(family).containsKey(key)) {
                linesByByProductCodeByProductFamily.get(family).put(key, ql);
            } else {
                linesByByProductCodeByProductFamily.get(family).get(key).SBQQ__Quantity__c += ql.SBQQ__Quantity__c;
            }
            
            // net total.
            if (!netTotalByProductCode.containsKey(key)) {
                netTotalByProductCode.put(key, 0);
            }
            if (ql.SBQQ__NetTotal__c > 0) {
                netTotalByProductCode.put(key, netTotalByProductCode.get(key) + ql.SBQQ__NetTotal__c);
            }
            
            // net total by group.
            if (!netTotalByFamily.containsKey(family)) {
                netTotalByFamily.put(family, ql.SBQQ__NetTotal__c);
            } else {
                netTotalByFamily.put(family, netTotalByFamily.get(family) + ql.SBQQ__NetTotal__c);
            }
        }
    }
}

APEX Unit Test Class

@isTest
public class CustomLinesSectionControllerTester {
    static testMethod void QuoteByLocation_PostInstall() {
        Product2 p = new Product2(Name='test', IsActive=True);
        insert p;
        
        Id pbid = Test.getStandardPricebookId();
        PricebookEntry pbe = new PricebookEntry(Pricebook2Id=pbid, Product2Id=p.id, UnitPrice=1, IsActive=True);
        insert pbe;
        
        SBQQ__Quote__c q = new SBQQ__Quote__c(SBQQ__Pricebook__c=pbid);
        insert q;
        
        SBQQ__QuoteLine__c ql1 = new SBQQ__QuoteLine__c(SBQQ__Quote__c=q.id, SBQQ__Product__c=p.id, SBQQ__Quantity__c=1);
        SBQQ__QuoteLine__c ql2 = new SBQQ__QuoteLine__c(SBQQ__Quote__c=q.id, SBQQ__Product__c=p.id, SBQQ__Quantity__c=1);
        insert new List<SBQQ__QuoteLine__c> {ql1, ql2};
        
        PageReference pageRef = Page.CustomLinesSection;
        Test.setCurrentPage(pageRef);
        ApexPages.currentPage().getParameters().put('qid', q.id);
        
        CustomLinesSectionController clsc = new CustomLinesSectionController();
    }
}

VisualForce Page
This is the VisualForce Page that renders the custom lines section. Notice! It doesn’t look like HTML! That is correct. The CPQ document generator requires custom content to be in a form of XML called XSL:FO. Here’s some documentation on it!

<apex:page showHeader="false" sidebar="false" cache="false" contentType="text/xml" controller="CustomLinesSectionController" >
    <!-- make sure to put /apex/c__CustomLinesSection in the Custom Source of the Template Content -->
    <apex:repeat var="family" value="{!linesByByProductCodeByProductFamily}">
        <table table-layout="fixed" width="100%">
            <table-column column-width="100%" />
            <table-body>
                <table-row>
                    <table-cell padding="2pt" background-color="#2B3263" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                        <block color="#FFFFFF" font-weight="bold" font-size="9pt" padding="2pt">{!HTMLENCODE(family)}</block>
                    </table-cell>
                </table-row>
            </table-body>
        </table>
        <table table-layout="fixed" width="100%">
            <table-column column-width="50%" />
            <table-column column-width="5%" />
            <table-column column-width="20%" />
            <table-column column-width="25%" />
            <table-body>
                <table-row>
                    <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                        <block color="#000000" font-weight="bold">
                            Product
                        </block>
                    </table-cell>
                    <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                        <block color="#000000" font-weight="bold">
                            Qty 
                        </block>
                    </table-cell>
                    <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                        <block color="#000000" font-weight="bold">
                            Item Price 
                        </block>
                    </table-cell>
                    <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                        <block color="#000000" font-weight="bold">
                            Extended Price 
                        </block>
                    </table-cell>
                </table-row>
                <apex:repeat var="code" value="{!linesByByProductCodeByProductFamily[family]}">
                    <table-row>
                        <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                            <block >
                                {!HTMLENCODE(linesByByProductCodeByProductFamily[family][code].SBQQ__ProductName__c)}
                            </block>
                        </table-cell>
                        <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                            <block >
                                <apex:outputText value="{0, number, ###,###,###,###}">
                                    <apex:param value="{!linesByByProductCodeByProductFamily[family][code].SBQQ__Quantity__c}"/>
                                </apex:outputText>
                            </block>
                        </table-cell>
                        <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                            <block text-align="right">
                                <apex:outputText value="{0, number, $###,###,###,##0.00}">
                                    <apex:param value="{!linesByByProductCodeByProductFamily[family][code].SBQQ__ListPrice__c}"/>
                                </apex:outputText>
                            </block>
                        </table-cell>
                        <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                            <block text-align="right">
                                <apex:outputText value="{0, number, $###,###,###,##0.00}">
                                    <apex:param value="{!netTotalByProductCode[code]}"/>
                                </apex:outputText>
                            </block>
                        </table-cell>
                    </table-row>
                </apex:repeat>
                <table-row>
                    <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="0px" border-color="#000000" border-style="solid">
                        <block >
                            &nbsp;
                        </block>
                    </table-cell>
                    <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="0px" border-color="#000000" border-style="solid">
                        <block >
                            &nbsp;
                        </block>
                    </table-cell>
                    <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                        <block text-align="right" font-weight="bold">
                            Total
                        </block>
                    </table-cell>
                    <table-cell padding="2pt" background-color="#FFFFFF" font-size="9pt" border-width="1px" border-color="#000000" border-style="solid">
                        <block text-align="right">
                            <apex:outputText value="{0, number, $###,###,###,##0.00}">
                                <apex:param value="{!netTotalByFamily[family]}"/>
                            </apex:outputText>
                        </block>
                    </table-cell>
                </table-row>
            </table-body>
        </table>
        <block>&nbsp;</block>
    </apex:repeat>
</apex:page>

Template Content (Custom)

Content Name: Lines (Custom) Content
Custom Source: /apex/c__CustomLinesSection

Install Links
Here’s some links for you to install the above things!
Sandbox | Production

4 thoughts on “Quote Template – Custom Lines Section

    1. Do you mean implement an automated way to hide/show a specific column? You’d have to implement that yourself. This is merely an example to get you started!

  1. In my document one rows content splitted between two page,half content on first page and half on second page.
    How can I avoid page break within a table row.

    1. This happens because your lines table is larger than 100% of the page. You need to make the widths of your columns smaller to add up to 100% of the width.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.