Line Editor – Price Analyzer

This is a very cool custom visualforce page. The best way to describe it is to show it!

You can see that it has Price Metrics (Highest, Average, Lowest, and Median Prices), Pricing History which shows how the price of a product has changed over time, and a Volume Analysis which shows pricing by quantity and Opportunity stage. This was originally written by Matt Seeger and uploaded to his github.

The APEX Controller and Visualforce Page have been modified from their original forms to add the additional data point called “Median Price”. I added this to show how easy it is to modify this solution to accommodate requirements. The original code has been left there, commented out, in the event you want to revert and remove the additional data point.

Possible Additional Enhancements

Below is a list of possible additional enhancements that could be incorporated into this solution. I’ll be looking into adding some of these things.

  • Adding additional data points. This could be any of your custom data points per product.
    • Custom Price field.
    • Discounts
  • Adding filter criteria on the left to limit the pricing data by min/max price, by date range, or by Opportunity stage.
  • Other filtering/grouping such as by Sales Rep or Sales Region.
  • Incorporate MDQ functionality to show/filter data by segment.
  • Adding a configuration screen to allow for the ability to configure which data points to include as well as choose which price field to reference and other potential configurations.

APEX Controller

/*
 * Copyright (c) 2018 Matthew Seeger <mseeger@salesforce.com>

 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/**
 * Controller for CPQ Price Analyzer Visualforce Page
 * Parses URL parameters and can optionally fetch data for presentation in the Visualforce page
 * @author Matthew Seeger <mseeger@salesforce.com>
 */
public class PriceAnalyzerController{
    public Id productId {get; set;}
    public Product2 productObj {get; set;}
    public Decimal param1 {get; set;}
    public Decimal param2 {get; set;}
    public Decimal high {get; set;}
    public Decimal avg {get; set;}
    public Decimal low {get; set;} 
    public Decimal med {get; set;} 
    
    /**
     * Constructor for the Visualforce page
     * Simply parses data in URL and makes it available to the visualforce page
     */
    public PriceAnalyzerController(){
        this.param1 = Decimal.valueOf(ApexPages.currentPage().getParameters().get('SBQQ__Quantity__c'));
        this.param2 = Decimal.valueOf(ApexPages.currentPage().getParameters().get('SBQQ__NetPrice__c'));
        this.productId = ApexPages.currentPage().getParameters().get('SBQQ__Product__c');
        System.debug('param1: ');
        this.productObj = [SELECT Name FROM Product2 WHERE Id = :this.productId];
        getPricePoints();
    }
    
    /**
     * Fetches historical MAX, AVERAGE, and MIN Sales price of the item in scope.
     * Privided as an example of how to surface data in the visualforce page using SOQL in Apex looking at CPQ Quote Line Records
     */
    private void getPricePoints() {
        // Commented out to move the functions from SOQL to APEX.
        // AggregateResult[] res = [SELECT AVG(SBQQ__NetPrice__c), MAX(SBQQ__NetPrice__c), MIN(SBQQ__NetPrice__c) FROM SBQQ__QuoteLine__c WHERE SBQQ__Product__c = :this.productId AND SBQQ__Quote__r.SBQQ__Primary__c = true AND SBQQ__Quote__r.SBQQ__Opportunity2__r.CloseDate <= TODAY and SBQQ__Quote__r.SBQQ__Opportunity2__r.IsWon = TRUE];
        // this.avg = (Decimal)res[0].get('expr0');
        // this.high = (Decimal)res[0].get('expr1');
        // this.low = (Decimal)res[0].get('expr2');
        
        List<Decimal> testvalues = new List<Decimal>();
        Decimal sum = 0.0;
        
        SBQQ__QuoteLine__c[] qls = [SELECT SBQQ__NetPrice__c FROM SBQQ__QuoteLine__c WHERE SBQQ__Product__c = :this.productId AND SBQQ__Quote__r.SBQQ__Primary__c = true AND SBQQ__Quote__r.SBQQ__Opportunity2__r.CloseDate <= TODAY and SBQQ__Quote__r.SBQQ__Opportunity2__r.IsWon = TRUE];
        if (qls.size() > 0) {
            for (SBQQ__QuoteLine__c ql : qls) {
                testvalues.add(ql.SBQQ__NetPrice__c);
                if (this.high == null || this.high < ql.SBQQ__NetPrice__c) { this.high = ql.SBQQ__NetPrice__c; }
                if (this.low == null || this.low > ql.SBQQ__NetPrice__c) { this.low = ql.SBQQ__NetPrice__c; }
                sum += ql.SBQQ__NetPrice__c;
            }
            
            Integer sizeOfList = testvalues.size();
            Integer index = sizeOfList - 1;
            
            testvalues.sort();
            if (Math.mod(sizeOfList, 2) == 0) {
                this.med = (testValues[(index-1)/2] + testValues[(index/2)+1])/2;
            } else {
                this.med = testvalues[(index+1)/2];
            }
            
            this.avg = sum/qls.size();
        }
    }
    
    public static void insertCustomAction() {
        SBQQ__CustomAction__c ca = new SBQQ__CustomAction__c();
        ca.Name = 'Price Analyzer';
        ca.SBQQ__DisplayOrder__c = 999;
        ca.SBQQ__Type__c = 'Button';
        ca.SBQQ__Active__c = true;
        ca.SBQQ__URLTarget__c = 'Popup';
        string orgId = System.URL.getSalesforceBaseURL().getHost();
        string ns = ([select NamespacePrefix from ApexClass where Name =:'PriceAnalyzerController']).NamespacePrefix;
        ca.SBQQ__URL__c = 'https://'+(ns != null ? ns : orgId)+'.lightning.force.com/apex/PriceAnalyzer?SBQQ__Product__c={!SBQQ__Product__c}&SBQQ__Quantity__c={!SBQQ__Quantity__c}&SBQQ__NetPrice__c={!SBQQ__NetPrice__c}';
        ca.SBQQ__Page__c = 'Quote Line Editor';
        ca.SBQQ__Location__c = 'Line';
        ca.SBQQ__Icon__c = 'Chart';
        ca.SBQQ__Label__c = 'Analyze Price';
        ca.SBQQ__Description__c = 'View Price Analyzer';
        
        insert ca;
    }
}

Visualforce Page

<!--
Copyright (c) 2018 Matthew Seeger <mseeger@salesforce.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->

<apex:page title="Price Analyzer" applyBodyTag="False" applyHtmlTag="False" showHeader="false" standardStylesheets="false" sidebar="false" docType="html-5.0" controller="PriceAnalyzerController">
<html xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <head>
        <apex:slds />
        <apex:includeScript value="https://cdnjs.cloudflare.com/ajax/libs/jsforce/1.7.0/jsforce.min.js" />
        <apex:includeScript value="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.0/Chart.bundle.min.js"/>
    </head>
    <body>
        <div class="slds-scope">
            <div class="slds-page-header">
                <div class="slds-media">
                    <div class="slds-media__figure">
                        <span class="slds-icon_container slds-icon-standard-poll">
                            <svg class="slds-icon slds-page-header__icon" aria-hidden="true">
                                <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Asset.SLDS, '/assets/icons/standard-sprite/svg/symbols.svg#poll')}" />
                            </svg>
                        </span>
                    </div>
                    <div class="slds-media__body">
                        <h1 class="slds-page-header__title slds-truncate slds-align-middle" title="Price Analyzer">Price Analyzer</h1>
                        <p class="slds-text-body_small slds-line-height_reset">{!productObj.Name}</p>
                    </div>
                </div>
            </div>

            <div class="demo-only demo-only--sizing slds-grid slds-wrap">
                <div class="slds-size_1-of-7">
                    <div class="slds-section slds-is-open">
                        <h3 class="slds-section__title slds-theme_shade">
                            <span class="slds-truncate slds-p-horizontal_small" title="Price Metrics">Price Metrics</span>
                        </h3>
                        <div aria-hidden="false" class="slds-section__content">
                            <div class="slds-box slds-m-vertical_xx-small">
                                <div class="slds-text-title_caps slds-m-bottom_xx-small">Highest Price</div>
                                <div class="slds-text-heading_medium">
                                    <apex:outputText value="{0, number, currency}">
                                        <apex:param value="{!high}" />
                                    </apex:outputText>
                                </div>
                            </div>
                            <div class="slds-box slds-m-vertical_xx-small">
                                <div class="slds-text-title_caps slds-m-bottom_xx-small">Average Price</div>
                                <div class="slds-text-heading_medium">
                                    <apex:outputText value="{0, number, currency}">
                                        <apex:param value="{!avg}" />
                                    </apex:outputText>
                                </div>
                            </div>
                            <div class="slds-box slds-m-vertical_xx-small">
                                <div class="slds-text-title_caps slds-m-bottom_xx-small">Lowest Price</div>
                                <div class="slds-text-heading_medium">
                                    <apex:outputText value="{0, number, currency}">
                                        <apex:param value="{!low}" />
                                    </apex:outputText>
                                </div>
                            </div>
                            <div class="slds-box slds-m-vertical_xx-small">
                                <div class="slds-text-title_caps slds-m-bottom_xx-small">Median Price</div>
                                <div class="slds-text-heading_medium">
                                    <apex:outputText value="{0, number, currency}">
                                        <apex:param value="{!med}" />
                                    </apex:outputText>
                                </div>
                            </div>
                        </div>
                    </div>

                </div>

                <!-- History Chart -->
                <div class="slds-size_4-of-7">
                    <div class="slds-section slds-is-open slds-p-horizontal_xx-small">
                        <h3 class="slds-section__title slds-theme_shade">
                            <span class="slds-truncate slds-p-horizontal_small" title="Pricing History">Pricing History</span>
                        </h3>
                        <div aria-hidden="false" class="slds-section__content">
                            <div class="chart-container" style="height:350px; width:99%">
                                <canvas id="timechart"></canvas>
                            </div>
                        </div>
                    </div>
                </div>
                
                <!-- Scatter Chart -->
                <div class="slds-size_2-of-7">
                    <div class="slds-section slds-is-open">
                        <h3 class="slds-section__title slds-theme_shade">
                            <span class="slds-truncate slds-p-horizontal_small" title="Volume Analysis">Volume Analysis</span>
                        </h3>
                        <div aria-hidden="false" class="slds-section__content">
                            <div class="chart-container" style="height:350px; width:99%">
                                <canvas id="chart"></canvas>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            
            <script>
                /**
                Scatter plot
                */
                function processChartData(err, res){
                    console.debug('*CPQ_PA Processing Scatter Plot');
                    if(err){
                        console.error(err);
                        return;
                    }
                    console.debug('*CPQ_PA Processing Results:');
                    console.debug(res);
                    var thisData = [{x:{!param1},y:{!param2}}]
                    var wonData = [];
                    var activeData = [];
                    var lostData = [];
                    for(var i = 0; i< res.records.length; i++){
                        if(res.records[i].SBQQ__Quote__r.SBQQ__Opportunity2__r.IsClosed){
                            if(res.records[i].SBQQ__Quote__r.SBQQ__Opportunity2__r.IsWon){
                                wonData.push({x:res.records[i].SBQQ__Quantity__c,y:res.records[i].SBQQ__NetPrice__c});
                            }
                            else{
                                lostData.push({x:res.records[i].SBQQ__Quantity__c,y:res.records[i].SBQQ__NetPrice__c});
                            }
                        }
                        else{
                            activeData.push({x:res.records[i].SBQQ__Quantity__c,y:res.records[i].SBQQ__NetPrice__c});
                        }
                    }
                    var chartParams = {
                    type: 'scatter',
                    data: {
                        datasets: [
                            {label: 'Won',data: wonData, backgroundColor: '#04844b', radius:5},
                            {label: 'Active',data: activeData, backgroundColor: '#ff9a3c', radius:5},
                            {label: 'Lost',data: lostData, backgroundColor: '#c23934', radius:5},
                            {label: 'This Deal',data: thisData, backgroundColor: '#0070d2', radius:7}
                        ]
                    },
                    options: {
                        scales: {
                            xAxes: [{
                                type: 'linear',
                                position: 'bottom',
                                scaleLabel: {display:true, labelString: 'Quantity'}
                            }],
                            yAxes: [{
                                type: 'linear',
                                position: 'left',
                                scaleLabel: {display:true, labelString: 'Price'}
                            }]
                        },
                        legend:{
                            position:'bottom'
                        }
                    }
                    };
                    var ctx = document.getElementById('chart');
                    ctx.getContext('2d').canvas.height = ctx.parentElement.offsetHeight;
                    ctx.getContext('2d').canvas.width = ctx.parentElement.offsetWidth;
                    var scatterChart = new Chart(ctx, chartParams);
                }
                /**
                Time series as line chart
                */
                function processTimeChartData(err, res){
                console.debug('*CPQ_PA Processing Time Plot');
                if(err){
                    console.error(err);
                    return;
                }
                console.debug('*CPQ_PA Processing Results:');
                console.debug(res);
                var priceData = [];
                for(var i = 0; i< res.records.length; i++){
                    if(res.records[i].CloseDate){
                        priceData.push({t: new Date(res.records[i].CloseDate),y:res.records[i].expr0});
                    }
                }
                var chartParams = {
                    type: 'line',
                    data: {
                        datasets: [
                            {label: 'Closed', pointStyle:'circle', data: priceData, borderColor: '#ff9a3c', pointRadius:5, showLine:true}
                        ]
                    },
                    options: {
                        legend:{
                            position:'bottom'
                        },
                        scales: {
                            xAxes: [
                                {
                                    type: 'time',
                                    position: 'bottom',
                                    time: {unit: 'month'},
                                    scaleLabel: {display:true, labelString: 'Close Date'}
                                }
                            ],
                            yAxes: [
                                {
                                    type: 'linear',
                                    position: 'left',
                                    scaleLabel: {display:true, labelString: 'Average Selling Price'}
                                }
                            ]
                        }
                    }
                };
                var ctx = document.getElementById('timechart');
                ctx.getContext('2d').canvas.height = ctx.parentElement.offsetHeight;
                ctx.getContext('2d').canvas.width = ctx.parentElement.offsetWidth;
                var scatterChart = new Chart(ctx, chartParams);
                }


                var conn = new jsforce.Connection({ accessToken: '{!$API.Session_Id}' });
                /**
                Examples of accessing historical data client side using jsforce
                */
                conn.query('SELECT Id, SBQQ__Quote__r.SBQQ__Opportunity2__r.IsWon, SBQQ__Quote__r.SBQQ__Opportunity2__r.IsClosed, SBQQ__NetPrice__c, SBQQ__ListPrice__c, SBQQ__Quantity__c FROM SBQQ__QuoteLine__c WHERE SBQQ__Product__c = \'{!productId}\' AND SBQQ__Quote__r.SBQQ__Opportunity2__c !=null AND SBQQ__NetPrice__c != null AND SBQQ__Quantity__c != null AND SBQQ__Quote__r.SBQQ__Primary__c = TRUE AND SBQQ__Quote__r.SBQQ__Opportunity2__r.CloseDate <= TODAY ORDER BY SBQQ__Quote__r.SBQQ__Opportunity2__r.CloseDate DESC LIMIT 200', processChartData);

                conn.query('SELECT SBQQ__Quote__r.SBQQ__Opportunity2__r.CloseDate, AVG(SBQQ__NetPrice__c) FROM SBQQ__QuoteLine__c WHERE SBQQ__Product__c = \'{!productId}\' AND SBQQ__Quote__r.SBQQ__Primary__c = TRUE AND SBQQ__Quote__r.SBQQ__Opportunity2__r.CloseDate <= TODAY AND SBQQ__Quote__r.SBQQ__Opportunity2__r.IsWon = TRUE GROUP BY SBQQ__Quote__r.SBQQ__Opportunity2__r.CloseDate ORDER BY SBQQ__Quote__r.SBQQ__Opportunity2__r.CloseDate', processTimeChartData);

            </script>
        </div>
    </body>
</html>
</apex:page>

Test Class

<!--
Copyright (c) 2022 Dennis Palmer <dennis@morecpq.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->

@isTest
public class PriceAnalyzerControllerTester {
    static testMethod void PriceAnalyzerControllerTester() {
        Product2 p = new Product2(Name='PriceAnalyzer', IsActive=True);
        insert p;
        
        Id pbid = Test.getStandardPricebookId();
        PricebookEntry pbe = new PricebookEntry(Pricebook2Id=pbid, Product2Id=p.id, UnitPrice=1, IsActive=True);
        insert pbe;
        
        Account a = new Account(Name='PriceAnalyzer');
        insert a;
        
        Opportunity o = new Opportunity(AccountId=a.id, Name='PriceAnalyzer', StageName='Closed Won', CloseDate=Date.today());
        insert o;
        
        SBQQ__Quote__c q = new SBQQ__Quote__c(SBQQ__Pricebook__c=pbid, SBQQ__Opportunity2__c=o.id, SBQQ__Primary__c=True);
        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__NetPrice__c=1);
        insert new List<SBQQ__QuoteLine__c> {ql1};
        
        Opportunity o2 = new Opportunity(AccountId=a.id, Name='PriceAnalyzer', StageName='Closed Won', CloseDate=Date.today());
        insert o2;
        
        SBQQ__Quote__c q2 = new SBQQ__Quote__c(SBQQ__Pricebook__c=pbid, SBQQ__Opportunity2__c=o2.id, SBQQ__Primary__c=True);
        insert q2;
        
        SBQQ__QuoteLine__c ql12 = new SBQQ__QuoteLine__c(SBQQ__Quote__c=q2.id, SBQQ__Product__c=p.id, SBQQ__Quantity__c=1, SBQQ__NetPrice__c=1);
        insert new List<SBQQ__QuoteLine__c> {ql12};
        
        PageReference pageRef = Page.PriceAnalyzer;
        Test.setCurrentPage(pageRef);
        ApexPages.currentPage().getParameters().put('SBQQ__Product__c', ql1.SBQQ__Product__c);
        ApexPages.currentPage().getParameters().put('SBQQ__Quantity__c', ql1.SBQQ__Quantity__c + '');
        ApexPages.currentPage().getParameters().put('SBQQ__NetPrice__c', ql1.SBQQ__NetPrice__c + '');
        
        PriceAnalyzerController pac = new PriceAnalyzerController();
    }
    
    static testMethod void PriceAnalyzerControllerTester2() {
        Product2 p = new Product2(Name='PriceAnalyzer', IsActive=True);
        insert p;
        
        Id pbid = Test.getStandardPricebookId();
        PricebookEntry pbe = new PricebookEntry(Pricebook2Id=pbid, Product2Id=p.id, UnitPrice=1, IsActive=True);
        insert pbe;
        
        Account a = new Account(Name='PriceAnalyzer');
        insert a;
        
        Opportunity o = new Opportunity(AccountId=a.id, Name='PriceAnalyzer', StageName='Closed Won', CloseDate=Date.today());
        insert o;
        
        SBQQ__Quote__c q = new SBQQ__Quote__c(SBQQ__Pricebook__c=pbid, SBQQ__Opportunity2__c=o.id, SBQQ__Primary__c=True);
        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__NetPrice__c=1);
        insert new List<SBQQ__QuoteLine__c> {ql1};
        
        Opportunity o2 = new Opportunity(AccountId=a.id, Name='PriceAnalyzer', StageName='Closed Won', CloseDate=Date.today());
        insert o2;
        
        SBQQ__Quote__c q2 = new SBQQ__Quote__c(SBQQ__Pricebook__c=pbid, SBQQ__Opportunity2__c=o2.id, SBQQ__Primary__c=True);
        insert q2;
        
        SBQQ__QuoteLine__c ql12 = new SBQQ__QuoteLine__c(SBQQ__Quote__c=q2.id, SBQQ__Product__c=p.id, SBQQ__Quantity__c=1, SBQQ__NetPrice__c=1);
        insert new List<SBQQ__QuoteLine__c> {ql12};
        
        Opportunity o3 = new Opportunity(AccountId=a.id, Name='PriceAnalyzer', StageName='Closed Won', CloseDate=Date.today());
        insert o3;
        
        SBQQ__Quote__c q3 = new SBQQ__Quote__c(SBQQ__Pricebook__c=pbid, SBQQ__Opportunity2__c=o3.id, SBQQ__Primary__c=True);
        insert q3;
        
        SBQQ__QuoteLine__c ql13 = new SBQQ__QuoteLine__c(SBQQ__Quote__c=q3.id, SBQQ__Product__c=p.id, SBQQ__Quantity__c=1, SBQQ__NetPrice__c=1);
        insert new List<SBQQ__QuoteLine__c> {ql13};
        
        PageReference pageRef = Page.PriceAnalyzer;
        Test.setCurrentPage(pageRef);
        ApexPages.currentPage().getParameters().put('SBQQ__Product__c', ql1.SBQQ__Product__c);
        ApexPages.currentPage().getParameters().put('SBQQ__Quantity__c', ql1.SBQQ__Quantity__c + '');
        ApexPages.currentPage().getParameters().put('SBQQ__NetPrice__c', ql1.SBQQ__NetPrice__c + '');
        
        PriceAnalyzerController pac = new PriceAnalyzerController();
    }
    
    static testmethod void PriceAnalyzer_PostInstall() {
        PriceAnalyzerController.PriceAnalyzer_PostInstall();
    }
}

Custom Action

The Custom Action for this is to make the little chart icon next to each line. It’s configured as a button that pops up a dialog that displays the contents of a url that points to the Visualforce Page above, passing SBQQ__Product__c, SBQQ__Quantity__c and SBQQ__NetPrice__c.

IMPORTANT: If you do this record manually, there is a “morecpq” in the url. You want to replace this with either your org namespace or your org instance. The installer below is smart and will insert the correct value there.

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

After you install, be sure to run the below script in the execute anonymous popup in the developer console. Click here to open the developer console in production and here to open it in sandbox.

PriceAnalyzerController.PriceAnalyzer_PostInstall();

2 thoughts on “Line Editor – Price Analyzer

  1. I am having issues getting this to work. I keep getting the “Server IP Address could not be found” error, my domain looks correct. Any ideas?

    1. Hi Richard. I have not seen this error before in this tool. There aren’t any IP Addresses in the solution that I can find.

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.