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

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 Line Trigger

trigger QuoteLineTriggers on SBQQ__QuoteLine__c (after insert) {

Quote Line Trigger Helper Class

public class QuoteLineTriggerHelper {
    public static void runTriggers(List<SBQQ__QuoteLine__c> newLines) {
        if (Trigger.isAfter) {
            if (Trigger.isInsert) {
    public static void insertMissingAmendmentRenewalLineGroups(List<SBQQ__QuoteLine__c> newLines) {
        List<Id> originalGroupIds = new List<Id>();
        Map<Id, SBQQ__QuoteLine__c> groupedLinesMap = new Map<Id, SBQQ__QuoteLine__c>();
        Map<Id, Id> newQuoteIdsMap = new Map<Id, Id>();
        Map<String,Schema.SObjectField> mfields = Schema.getGlobalDescribe().get('SBQQ__QuoteLineGroup__c').getDescribe().fields.getMap();
        // loop through lines.
        for (SBQQ__QuoteLine__c ql : newLines) {
            // if this line has a original group,
            if (ql.Original_Group_Id__c != null && !newQuoteIdsMap.containsKey(ql.SBQQ__Quote__c)) {
                newQuoteIdsMap.put(ql.SBQQ__Quote__c, ql.SBQQ__Quote__c);
        // query for new quotes list.
        List<SBQQ__Quote__c> newQuotes = [select id, SBQQ__LineItemsGrouped__c from SBQQ__Quote__c where id in :newQuoteIdsMap.values()];
        // loop through lines.
        for (SBQQ__QuoteLine__c ql : newLines) {
            // if this line has a original group,
            if (ql.Original_Group_Id__c != null) {
                // add to original group id list.
                // add to grouped lines list.
                groupedLinesMap.put(, ql);
        // 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');
        // contact Kamino government to commission the creation of a quote line group clone army.
        List<SBQQ__QuoteLineGroup__c> cloneArmy = new List<SBQQ__QuoteLineGroup__c>();
        // loop through quotes.
        for (SBQQ__Quote__c q : newQuotes) {
            // 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;
        // 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, CPQU__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();

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 *