Salesforce CPQ – Pull Quote Line Groups into Renewals and Amendments

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

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 ,

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) {
    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.
                    // add to grouped lines list.
                    groupedLinesMap.put(, 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 =, SBQQ__Source__c =;
                                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) { }
                    // 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.
                    // 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) {

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


Leave a Reply

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