Salesforce CPQ – Pull Quote Line Groups into Renewals and Amendments

Hello! If you’re here, you’re looking for how to pull groups into your amendments and/or renewals! You might have come from the Salesforce Idea page or perhaps google search…

There is a formula field, a trigger, and a small price rule involved. Fear not! All of it is right here.

Please NOTE: This code is provided as is. It might not work in your org. Creating these things requires a system administrator profile and access to all objects/fields involved. The trigger code may fail based on your org’s validation rules or other automation. It also might cause an APEX timeout because of this processing time + existing automation processing time. The price rule may fail because of other automation or price/product rules/validations you have in place. So, put these things in place, test it out, modify where necessary to fit your org.

Formula field on Quote Line

Object: SBQQ__QuoteLine__c
Label: Original Group Id
Field Name: Original_Group_Id
Type: Text
Formula (For Sales Cloud Users):

IF( NOT( ISBLANK( SBQQ__UpgradedSubscription__c ) ),
    SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c ,
    IF( NOT( ISBLANK( SBQQ__RenewedSubscription__c ) ),
        SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c ,
        ''
    )
)

Formula (For Service Cloud Users):

IF( NOT( ISBLANK( SBQQSC__UpgradedContractLine__c ) ),
    SBQQSC__UpgradedContractLine__r.SBQQSC__QuoteLine__r.SBQQ__Group__c ,
    IF( NOT( ISBLANK( SBQQSC__RenewedContractLine__c ) ),
        SBQQSC__RenewedContractLine__r.SBQQSC__QuoteLine__r.SBQQ__Group__c ,
        ''
    )
)

Quote Trigger Helper Class
Name: QuoteTriggerHelper

public class QuoteTriggerHelper {
    public static void runTriggers(Map<Id,SBQQ__Quote__c> newQuotesMap) {
        if (Trigger.isAfter) {
            if (Trigger.isUpdate) {
                QuoteTriggerHelper.insertMissingAmendmentRenewalLineGroups(newQuotesMap);
            }
        }
    }
	
    public static void insertMissingAmendmentRenewalLineGroups(Map<Id, SBQQ__Quote__c> newQuotesMap) {
        // query new lines.
        List<SBQQ__QuoteLine__c> newLines = [select id, Original_Group_Id__c, SBQQ__Group__c, SBQQ__Quote__c from SBQQ__QuoteLine__c where SBQQ__Quote__c in :newQuotesMap.keySet()];
        
        // if we have new lines,
        if (newLines.size() > 0) {
            // loop through new lines.
            Map<Id, Id> newQuoteIdsMap = new Map<Id, Id>();
            List<Id> originalGroupIds = new List<Id>();
            Map<Id, SBQQ__QuoteLine__c> groupedLinesMap = new Map<Id, SBQQ__QuoteLine__c>();
            for (SBQQ__QuoteLine__c ql : newLines) {
                // if this line has a original group and does not have an actual group,
                if (ql.Original_Group_Id__c != null && ql.SBQQ__Group__c == null) {
                    if (!newQuoteIdsMap.containsKey(ql.SBQQ__Quote__c)) {
                        // add to new quote ids map.
                        newQuoteIdsMap.put(ql.SBQQ__Quote__c, ql.SBQQ__Quote__c);
                    }
                    
                    // add to original group id list.
                    originalGroupIds.add(ql.Original_Group_Id__c);
                    
                    // add to grouped lines list.
                    groupedLinesMap.put(ql.id, ql);
                }
            }
            
            // query for new quotes list.
            newQuotesMap = new Map<id, SBQQ__Quote__c>([select id, SBQQ__LineItemsGrouped__c from SBQQ__Quote__c where id in :newQuoteIdsMap.values()]);
            
            // if we have quotes with original group ids but no group set,
            if (newQuotesMap.size() > 0) {
                // get all fields of quote line group.
                Map<String,Schema.SObjectField> mfields = Schema.getGlobalDescribe().get('SBQQ__QuoteLineGroup__c').getDescribe().fields.getMap();
                
                // query original groups.
                List<SBQQ__QuoteLineGroup__c> originalGroups = Database.query('select '+string.join(new List<String>(mfields.keySet()), ',')+' from SBQQ__QuoteLineGroup__c where id in :originalGroupIds');
                
                // if we have original groups,
                if (originalGroups.size() > 0) {
                    // contact Kamino government to commission the creation of a quote line group clone army.
                    List<SBQQ__QuoteLineGroup__c> cloneArmy = new List<SBQQ__QuoteLineGroup__c>();
                    
                    // CLONE QUOTE LINE GROUPS
                    // loop through quotes.
                    for (SBQQ__Quote__c q : newQuotesMap.values()) {
                        // set this quote to have grouped lines.
                        q.SBQQ__LineItemsGrouped__c = true;
                        
                        // if we have original group ids,
                        if (originalGroupIds.size() > 0) {
                            // loop through clone army.
                            for (SBQQ__QuoteLineGroup__c og : originalGroups) {
                                SBQQ__QuoteLineGroup__c ng = new SBQQ__QuoteLineGroup__c(SBQQ__Quote__c = q.id, SBQQ__Source__c = og.id);
                                for (String fieldName : mfields.keySet()) {
                                    Schema.DescribeFieldResult field = mfields.get(fieldName).getDescribe();
                                    if (field.isUpdateable() && fieldName != 'Id' && fieldName != 'SBQQ__Quote__c' && fieldName != 'SBQQ__Source__c' && og.get(fieldName) != null) {
                                        try {
                                            ng.put(fieldName, og.get(fieldName));
                                        } catch (Exception e) { }
                                    }
                                }
                                
                                cloneArmy.add(ng);
                            }
                        }
                    }
                    
                    // if we have a clone army,
                    if (cloneArmy.size() > 0) {
                        // insert clone army.
                        insert cloneArmy;
                    }
                    
                    // UPDATE QUOTE LINES
                    
                    // make old group id -> new group id map.
                    Map<Id, Id> newGroupIdByOldGroupIdMap = new Map<Id, Id>();
                    
                    // loop through clone army.
                    for (SBQQ__QuoteLineGroup__c g : cloneArmy) {
                        // add source -> id to the old group id -> new group id map.
                        newGroupIdByOldGroupIdMap.put(g.SBQQ__Source__c, g.id);
                    }
                    
                    // if we have grouped lines,
                    if (groupedLinesMap.size() > 0) {
                        // requery grouped lines.
                        groupedLinesMap = new Map<Id, SBQQ__QuoteLine__c>([select id, SBQQ__Group__c, Original_Group_Id__c from SBQQ__QuoteLine__c where id in :groupedLinesMap.keySet()]);
                        
                        // loop through grouped lines.
                        for (SBQQ__QuoteLine__c ql : groupedLinesMap.values()) {
                            // update to new group.
                            ql.SBQQ__Group__c = newGroupIdByOldGroupIdMap.get(ql.Original_Group_Id__c);
                        }
                        
                        // update grouped lines.
                        update groupedLinesMap.values();
                    }
                }
            }
        }
    }
}

Quote Trigger
Name: QuoteTriggers
Object: SBQQ__Quote__c

trigger QuoteTriggers on SBQQ__Quote__c (after update) {
	QuoteTriggerHelper.runTriggers(Trigger.newMap);
}

Summary Variable

Variable Name: Quote Line – Count – Grouped Lines
Target Object: Quote Line
Aggregate Function: Count
Aggregate Field: Quantity
Scope: Quote
Filter Field: SBQQ__Group__c
Operator: not equals
Filter Value: <Leave this blank>

Price Rule to Inject TRUE into the SBQQ__LineItemsGrouped__c Field

Price Rule
Price Rule Name
: Quote – Inject TRUE to Group Line Items
Evaluation Scope: Calculator
Calculator Evaluation Event: Before Calculate
Active: TRUE

Price Conditions
Object: Summary Variable
Summary Variable: Quote Line – Count – Grouped Lines
Operator: greater than
Filter Type: Value
Value: 0

Price Actions
Target Object: Quote
Target Field: SBQQ__LineItemsGrouped__c
Formula: TRUE

D P

9 thoughts on “Salesforce CPQ – Pull Quote Line Groups into Renewals and Amendments

  1. This is great, thanks. For users out there who use Service Cloud for CPQ, you can use the exact same code and rules, but the formula field needs to change a little bit. Here’s that formula:

    IF( NOT( ISBLANK( SBQQSC__UpgradedContractLine__c ) ),
    SBQQSC__UpgradedContractLine__r.SBQQSC__QuoteLine__r.SBQQ__Group__c ,
    IF( NOT( ISBLANK( SBQQSC__RenewedContractLine__c ) ),
    SBQQSC__RenewedContractLine__r.SBQQSC__QuoteLine__r.SBQQ__Group__c ,

    )
    )

  2. Question on the price rule – in your experience do you not need a “false” pairing to the “true” you created? In other words, if I want to un-group a renewal quote, is it enough to delete the groups and save, or do I need a price rule that sets grouping to false based on a sumvar count of 0?

    1. Ah yes, this is a good thought. How to “undo” this thing… Will the rule kill my ungroupings or set something wonky… The short answer is no. The long answer is: CPQ usually manages this checkbox for you as you group/ungroup in the line editor. But, for some reason, it does not do this when groups get inserted via trigger like the above. It omits the evaluation of grouped lines and setting of this field, hence the price rule is needed. In the trigger, it always thinks there are no grouped lines.

  3. Hi Dennis,

    It’s great to be able to pull the groups into the amendment and renewal quotes! I am testing the code, but when I tried to open the quotes updated by the code, the quote line editor failed to load (just hang there), did you experience this before?

    Thanks.

    Jacky

    1. I had to hit the calculate button manually on the quote page, after the calculation completes, then get into the QLE. Not sure if it is something from our sandbox.

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.