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 checkbox field, a trigger and a 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.
Please NOTE: The Custom Checkbox field and Price Rule are to get around a “Legacy Renewal Service” issue. If you have the Legacy Renewal Service turned on, CPQ will not let the trigger set the Group Line Items field on the Quote object. So we set a custom version of this field (Has_Groups__c) to TRUE and then the Price Rule conditions on this custom field and checks the Group Line Items CPQ field on the Quote. If this Price Rule does not run, you will get a forever spinning “waiting” dots image because of a javascript error. Essentially, there are groups on the quote but the line editor doesn’t think there is because the checkbox is unchecked so it kabooms and waits forever.
Checkbox Field
Quote -> Has Groups (Has_Groups__c)

Quote Trigger Helper Class
Name: QuoteTriggerHelper
public class QuoteTriggerHelper {
public static boolean insertMissingAmendmentRenewalLineGroupsRun = false;
public static void runTriggers(Map<Id, SBQQ__Quote__c> newQuotesMap) {
if (Trigger.isAfter) {
if (Trigger.isUpdate) {
QuoteTriggerHelper.doInsertMissingAmendmentRenewalLineGroups(newQuotesMap);
}
}
}
public static void doInsertMissingAmendmentRenewalLineGroups(Map<Id, SBQQ__Quote__c> newQuotesMap) {
if (!QuoteTriggerHelper.insertMissingAmendmentRenewalLineGroupsRun) {
QuoteTriggerHelper.insertMissingAmendmentRenewalLineGroupsRun = true;
QuoteTriggerHelper.insertMissingAmendmentRenewalLineGroups(newQuotesMap, null);
}
}
public static void insertMissingAmendmentRenewalLineGroups(Map<Id, SBQQ__Quote__c> newQuotesMap, Map<Id, SBQQ__QuoteLine__c> groupedLinesMap) {
Map<Id, Id> newQuoteIdsMap = new Map<Id, Id>();
Map<Id, Id> originalGroupIdsMap = new Map<Id, Id>();
if (groupedLinesMap == null) {
groupedLinesMap = new Map<Id, SBQQ__QuoteLine__c>([select id, SBQQ__Group__c, SBQQ__Quote__c,
SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c,
SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c,
SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c,
SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c,
SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c,
SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c,
SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c,
SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c
from SBQQ__QuoteLine__c
where
SBQQ__Quote__c in :newQuotesMap.keySet() and
SBQQ__Quote__r.SBQQ__Type__c != 'Quote' and
SBQQ__Group__c = null and (
SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null or
SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null or
SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null or
SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null or
SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c != null or
SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c != null or
SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c != null or
SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c != null
)]);
}
// loop through lines.
for (SBQQ__QuoteLine__c ql : groupedLinesMap.values()) {
// create original group id.
Id originalGroupId = ql.SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null ? (
ql.SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c
) : ql.SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null ? (
ql.SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c
) : ql.SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null ? (
ql.SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c
) : ql.SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null ? (
ql.SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c
) : null;
// if this line has a original group and does not have an actual group,
if (originalGroupId != 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);
}
// if we don't already have this group id in the list,
if (!originalGroupIdsMap.containsKey(originalGroupId)) {
// add to original group id list.
originalGroupIdsMap.put(originalGroupId, originalGroupId);
}
// 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() and Has_Groups__c = false and (SBQQ__Type__c = 'Renewal' or SBQQ__Type__c = 'Amendment')]);
// if we have quotes with original group ids but no group set,
if (newQuotesMap.size() > 0) {
// survey the republic for the existence of a sufficient army.
List<SBQQ__QuoteLineGroup__c> cloneArmy = [select id, SBQQ__Source__c from SBQQ__QuoteLineGroup__c where SBQQ__Quote__c in :newQuoteIdsMap.values() and SBQQ__Source__c != null];
// make a list of the missing original group ids.
for (Id origGpId : originalGroupIdsMap.values()) {
for (SBQQ__QuoteLineGroup__c clone : cloneArmy) {
if (origGpId == clone.SBQQ__Source__c) {
originalGroupIdsMap.remove(origGpId);
}
}
}
// if the republic requires a clone army,
if (originalGroupIdsMap.size() > 0) {
// CLONE QUOTE LINE GROUPS
// get all fields of quote line group.
Map<String,Schema.SObjectField> qlgfields = Schema.getGlobalDescribe().get('SBQQ__QuoteLineGroup__c').getDescribe().fields.getMap();
// query original groups.
List<Id> originalGroupIds = originalGroupIdsMap.values();
List<SBQQ__QuoteLineGroup__c> originalGroups = Database.query('select '+string.join(new List<String>(qlgfields.keySet()), ',')+' from SBQQ__QuoteLineGroup__c where id in :originalGroupIds');
// contact Kamino government to commission the creation of a quote line group clone army.
cloneArmy = new List<SBQQ__QuoteLineGroup__c>();
// loop through quotes.
for (SBQQ__Quote__c q : newQuotesMap.values()) {
if (!q.SBQQ__LineItemsGrouped__c) {
// set this quote to have grouped lines.
q.SBQQ__LineItemsGrouped__c = true;
q.Has_Groups__c = true;
}
// if we have original group ids,
if (originalGroups.size() > 0) {
// loop through clone army.
for (SBQQ__QuoteLineGroup__c og : originalGroups) {
// create clone marking it with the source.
SBQQ__QuoteLineGroup__c ng = new SBQQ__QuoteLineGroup__c(SBQQ__Quote__c = q.id, SBQQ__Source__c = og.id);
// loop through quote line group fields. put all values into writeable fields.
for (String fieldName : qlgfields.keySet()) {
Schema.DescribeFieldResult field = qlgfields.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);
}
}
}
update newQuotesMap.values();
// 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) {
// loop through grouped lines.
List<SBQQ__QuoteLine__c> qlToUpdate = new List<SBQQ__QuoteLine__c>();
for (SBQQ__QuoteLine__c ql : groupedLinesMap.values()) {
// update to new group.
Id originalGroupId = ql.SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null ? (
ql.SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c
) : ql.SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null ? (
ql.SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c
) : ql.SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null ? (
ql.SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c
) : ql.SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c != null ? (
ql.SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c
) : null;
if (originalGroupId != null) {
ql.SBQQ__Group__c = newGroupIdByOldGroupIdMap.get(originalGroupId);
qlToUpdate.add(ql);
}
}
// update grouped lines.
if (qlToUpdate.size() > 0) {
update qlToUpdate;
}
// after updated lines, query again to see if any were missed.
groupedLinesMap = new Map<Id, SBQQ__QuoteLine__c>([select id, SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c, SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__c, SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c, SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__c, SBQQ__RenewedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c, SBQQ__UpgradedSubscription__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c, SBQQ__RenewedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c, SBQQ__UpgradedAsset__r.SBQQ__QuoteLine__r.SBQQ__Group__r.SBQQ__Source__c, SBQQ__Group__c, SBQQ__Quote__c from SBQQ__QuoteLine__c where id in :groupedLinesMap.keySet() and SBQQ__Group__c = null]);
}
// if any were missed, do it again.
if (groupedLinesMap.size() > 0) {
QuoteTriggerHelper.insertMissingAmendmentRenewalLineGroups(newQuotesMap, groupedLinesMap);
}
}
}
}
Quote Trigger Helper Tester Class
Name: QuoteTriggerHelperTester
@isTest (seealldata=false)
public class QuoteTriggerHelperTester {
@testSetup static void methodName() {
// set up foundation.
Product2 sp = new Product2(Name='Test Subscription', IsActive=true, SBQQ__PricingMethod__c='List', SBQQ__SubscriptionPricing__c='Fixed Price', SBQQ__SubscriptionType__c='Renewable', SBQQ__SubscriptionTerm__c=12);
Product2 ap = new Product2(Name='Test Asset', IsActive=true, SBQQ__PricingMethod__c='List', SBQQ__AssetConversion__c='One per unit');
insert new List<Product2>{sp, ap};
Id pbid = Test.getStandardPricebookId();
PricebookEntry spbe = new PricebookEntry(Product2Id=sp.id, Pricebook2Id=pbid, UnitPrice=1, IsActive=true);
PricebookEntry apbe = new PricebookEntry(Product2Id=ap.id, Pricebook2Id=pbid, UnitPrice=1, IsActive=true);
insert new List<PricebookEntry>{spbe, apbe};
Account a = new Account(Name='Test');
insert a;
// set up first opp and quote scenario.
Opportunity o = new Opportunity(AccountId=a.id, Name='Test', StageName='Prospecting', CloseDate=Date.today(), Pricebook2Id=pbid);
insert o;
SBQQ__Quote__c q = new SBQQ__Quote__c(SBQQ__Account__c=a.id, SBQQ__Opportunity2__c=o.id, SBQQ__Type__c='Quote', SBQQ__Primary__c=true, SBQQ__Pricebook__c=pbid, SBQQ__StartDate__c=Date.today(), SBQQ__SubscriptionTerm__c=12);
insert q;
SBQQ__QuoteLineGroup__c qlg = new SBQQ__QuoteLineGroup__c(SBQQ__Account__c=a.id, SBQQ__Quote__c=q.id, Name='Test');
insert qlg;
SBQQ__QuoteLine__c sql = new SBQQ__QuoteLine__c(SBQQ__Quote__c=q.id, SBQQ__Group__c=qlg.id, SBQQ__Product__c=sp.id, SBQQ__Quantity__c=1, SBQQ__SubscriptionPricing__c='Fixed Price', SBQQ__ProductSubscriptionType__c='Renewable', SBQQ__SubscriptionType__c='Renewable', SBQQ__DefaultSubscriptionTerm__c=12, SBQQ__SubscriptionTerm__c=12, SBQQ__StartDate__c=Date.today());
SBQQ__QuoteLine__c aql = new SBQQ__QuoteLine__c(SBQQ__Quote__c=q.id, SBQQ__Group__c=qlg.id, SBQQ__Product__c=ap.id, SBQQ__Quantity__c=1);
insert new List<SBQQ__QuoteLine__c>{sql, aql};
// set up contract and subscriptions.
Contract c = new Contract(AccountId=a.id, SBQQ__Opportunity__c=o.id, SBQQ__Quote__c=q.id, Status='Draft', StartDate=Date.today(), ContractTerm=12, SBQQ__PreserveBundleStructureUponRenewals__c=true);
insert c;
SBQQ__Subscription__c s = new SBQQ__Subscription__c(SBQQ__Account__c=a.id, SBQQ__Contract__c=c.id, SBQQ__Product__c=sp.id, SBQQ__QuoteLine__c=sql.id, SBQQ__Quantity__c=1, SBQQ__ListPrice__c=1, SBQQ__NetPrice__c=1, SBQQ__CustomerPrice__c=1, SBQQ__RenewalQuantity__c=1);
insert s;
Asset asset = new Asset(Name='Test Asset', AccountId=a.id, Product2Id=ap.id, SBQQ__QuoteLine__c=aql.id, Quantity=1);
insert asset;
}
static testMethod void doInsertMissingRenewalLineGroups() {
List<Product2> ps = [select id from Product2 order by Name];
Id pbid = Test.getStandardPricebookId();
Account a = [select id from Account where Name='Test'];
Contract c = [select id from Contract where AccountId = :a.id];
SBQQ__Subscription__c s = [select id from SBQQ__Subscription__c where SBQQ__Account__c = :a.id];
Asset asset = [select id from Asset where AccountId = :a.id];
// ********* Starting Test **********
Test.startTest();
Opportunity ro = new Opportunity(AccountId=a.id, Name='Renewal Test', StageName='Prospecting', CloseDate=Date.today(), SBQQ__RenewedContract__c=c.id, Pricebook2Id=pbid);
insert ro;
SBQQ__Quote__c rq = new SBQQ__Quote__c(SBQQ__Account__c=a.id, SBQQ__Opportunity2__c=ro.id, SBQQ__Type__c='Renewal', SBQQ__Primary__c=true, SBQQ__Pricebook__c=pbid, SBQQ__StartDate__c=Date.today(), SBQQ__SubscriptionTerm__c=12);
insert rq;
SBQQ__QuoteLine__c srql = new SBQQ__QuoteLine__c(SBQQ__Quote__c=rq.id, SBQQ__Group__c=null, SBQQ__Product__c=ps[1].id, SBQQ__Quantity__c=1, SBQQ__SubscriptionPricing__c='Fixed Price', SBQQ__ProductSubscriptionType__c='Renewable', SBQQ__SubscriptionType__c='Renewable', SBQQ__DefaultSubscriptionTerm__c=12, SBQQ__SubscriptionTerm__c=12, SBQQ__StartDate__c=Date.today(),
SBQQ__RenewedSubscription__c=s.id);
SBQQ__QuoteLine__c arql = new SBQQ__QuoteLine__c(SBQQ__Quote__c=rq.id, SBQQ__Group__c=null, SBQQ__Product__c=ps[0].id, SBQQ__Quantity__c=1,
SBQQ__RenewedAsset__c=asset.id);
insert new List<SBQQ__QuoteLine__c>{srql, arql};
QuoteTriggerHelper.insertMissingAmendmentRenewalLineGroupsRun = false;
update rq;
Test.stopTest();
}
static testMethod void doInsertMissingAmendmentLineGroups() {
List<Product2> ps = [select id from Product2 order by Name];
Id pbid = Test.getStandardPricebookId();
Account a = [select id from Account where Name='Test'];
SBQQ__Quote__c q = [select id from SBQQ__Quote__c where SBQQ__Opportunity2__r.Name='Test'];
Contract c = [select id from Contract where AccountId = :a.id];
SBQQ__Subscription__c s = [select id from SBQQ__Subscription__c where SBQQ__Account__c = :a.id];
Asset asset = [select id from Asset where AccountId = :a.id];
// ********* Starting Test **********
Test.startTest();
Opportunity ao = new Opportunity(AccountId=a.id, Name='Amendment Test', StageName='Prospecting', CloseDate=Date.today(), SBQQ__AmendedContract__c=c.id, Pricebook2Id=pbid);
insert ao;
SBQQ__Quote__c aq = new SBQQ__Quote__c(SBQQ__Account__c=a.id, SBQQ__Opportunity2__c=ao.id, SBQQ__Type__c='Amendment', SBQQ__Primary__c=true, SBQQ__Pricebook__c=pbid, SBQQ__StartDate__c=Date.today(), SBQQ__SubscriptionTerm__c=12);
insert aq;
SBQQ__QuoteLine__c srql = new SBQQ__QuoteLine__c(SBQQ__Quote__c=aq.id, SBQQ__Group__c=null, SBQQ__Product__c=ps[1].id, SBQQ__Quantity__c=1, SBQQ__SubscriptionPricing__c='Fixed Price', SBQQ__ProductSubscriptionType__c='Renewable', SBQQ__SubscriptionType__c='Renewable', SBQQ__DefaultSubscriptionTerm__c=12, SBQQ__SubscriptionTerm__c=12, SBQQ__StartDate__c=Date.today(),
SBQQ__UpgradedSubscription__c=s.id);
SBQQ__QuoteLine__c arql = new SBQQ__QuoteLine__c(SBQQ__Quote__c=aq.id, SBQQ__Group__c=null, SBQQ__Product__c=ps[0].id, SBQQ__Quantity__c=1,
SBQQ__UpgradedAsset__c=asset.id);
insert new List<SBQQ__QuoteLine__c>{srql, arql};
QuoteTriggerHelper.insertMissingAmendmentRenewalLineGroupsRun = false;
update aq;
Test.stopTest();
}
}
Quote Trigger
Name: QuoteTriggers
Object: SBQQ__Quote__c
trigger QuoteTriggers on SBQQ__Quote__c (after update) {
QuoteTriggerHelper.runTriggers(Trigger.newMap);
}
Price Rule
This Price Rule is only for the legacy Renewal service (see the second note at the top). The new Renewal service does not have an issue with setting the Grouped Line Items checkbox on Quote but the legacy service does. We have no idea which service you’re using so this solution accommodates both!
Price Rule Name: Quote โ Inject TRUE to Group Line Items



And now here’s a couple install links for the things above.
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.
MoreCPQPostInstall.PullQuoteLineGroupsIntoRenewalsandAmendments_PostInstall();


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 ,
”
)
)
This is great thanks! I’ve updated the article to incorporate this. Good catch!
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?
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.
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
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.
Have you implemented the Price Rule? This will happen if the Price Rule isn’t firing or isn’t there.
Ah! This is the thing that is missing! In the package settings, set “Calculate Immediately” to true.
I am also facing the same issue
Have you implemented and activated the price rule?
I was experiencing the same issue and setting Filter Value explicitely to NULL for the summary variable instead of leaving it blank did the trick for me!
I’m also encountering the same issue. I have to hit calculate first before the group line items to be set to TRUE.
There is a solution! In CPQ Package Settings, set Calculate Immediately to true!
Yes, I have implemented Price rule and activated.
I noticed that ‘Group Line Items’ in Quote is not updating to TRUE. This is the reason it just keeps loading on Quote Line Editor. When I manually make it to TRUE on the amendment Quote, the line editor loads and retains the Groups.
Thank you Yamin S. Your solution works!. I have explicitly given NULL in the Filter Value of a summary variable. Thank you D P ๐
This is not working as expected when there is a package (product with product options)
Hey Azar. I’ve updated the code to accommodate. Let me know if this works. If not, send me a screenshot of what you’re seeing to dennis@morepalmer.org.
Dennis, I have activated the Price Rule but it will not fire automatiaclly after a renewal or amendment causing the QLE to be blank. I need to edit and save the QL to force a recalulation then it works great. From the other notes it appears this has happened to others. Was a solution ever found for this? Thanks Greg
Yes! The solution is to set “Calculate Immediately” to true in the CPQ Package Settings.
Any thoughts of modifying the class to use @InvocableMethod so it can be dropped into a flow or process builder instead of a trigger?
This is a great idea. Will update the article Monday morning to incorporate this. Thanks for the feedback!
I have not yet updated to enable this yet! This shall go on the list for next week.
Done! Updated to include this.
Adding on to Chrisโ thoughts, is there any way to use twin fields and/or QCP to simplify or eliminate the use of code, and to minimize performance impacte?
Yes! QCP is currently in development.
Awesome. Will that eliminate the need for a trigger, or just reduce its performance impact?
Yes, QCP will eliminate the need for a trigger.
Hi, did you manage to convert this to run as a QCP rather than with triggers?
I have not! Even if it were a QCP, it would involve an APEX web service that does the group creating. The next evolution of this solution will be a callable method that is called by a scheduled process builder action so the code is executed outside of the standard CPQ processing.
I am still not able to get this to work. One of the 2 issues happens:
1) It just spins when I do the amendment (in that case, everything gets created as expected, just have to click out and back in like others have mentioned). I have Calculate Immediately on.
2) When I tried to do the ‘NULL’ instead of leaving filter value blank in the summary variable (as some mentioned resolved their issue), it doesn’t spin anymore but it just groups all of my lines into 1 big group rather than splitting them out as expected. I’m not overly shocked by this considering every line is going to have SBQQ__Group__c != ‘NULL’ since its either an ID or blank and would recommend anyone else who tried this method to re-look and make sure it is functioning correctly.
Any help would be greatly appreciated.
1) The spinning happens because the Group Line Items field is not checked. The explicit NULL resolves this.
2) I’ve updated the trigger code. Try it and let me know if it’s still is grouping in one big group.
The reason 2 was happening is because it is not creating the groups (bug in code) and then the price rule flags the quote as grouped so the line editor makes a group and assigns it to every line.
Hello, this is great! Thank you for sharing this information.
I am trying this out in the Sandbox and sometimes I am getting an error of ‘No metadata was retrieved for field null.Name’ from the Quote Line Editor screen. Is that related to this? I updated the code per your note, but still seem to be seeing the error.
Thanks!
Hey Sean, I have not seen this error implementing this solution. Do you have other price/product rules or a QCP?
Updated QuoteTriggerHelper! Update yours and try again!
Explicit NULL resolved the issue – What exactly you changed? I am also facing the spinning issue in the QLE on amendment.
Hi DP, do you have a test class for the helper class? I am not a developer and need some help in doing this one.
Added to the post. Thanks for the reminder!
Hi Denise, thank you a bunch for writing this article. I have followed the instructions that you provided, however when I go to test this, I created a quote with 2 groups, contracted it, and amended it, however, my amendment quote only contains one group always called “Group 1”; I am not getting any error messages or hangups. Any ideas what the problem could be? Thank you in advance.
Hi Brian,
For some reason, your groups are not being created by the trigger. Then the price rule is setting it as grouped. I am going to re-implement this in a fresh org to see if I can reproduce this issue as it is happening to a few of you.
Updated QuoteTriggerHelper! Update yours and try again!
Hi Dennis, thanks for the help on this, however, I am still seeing issues.
– When I try to amend an existing contract with groups, CPQ tries to load the line editor page but instead, the page just tries to load forever.
– I am still seeing odd formatting loading on the line editor page for segmented products
– Creating a new quote always creates a group; I think you said in another post that for now it is required, however our business case doesn’t always require groups on quotes, and grouping does affect the quote line section on quote templates, so if something could be done about that, that would be great.
Thanks!
-Brian
Yes I am looking for solutions to the “groups are required” part of this. For now there is no solution as CPQ unchecks that box if I check it directly in the code.
I’ll be adding segmented products to my testing agenda for the next release of this solution.
Hi Dennis,
Even after adding the NULL in the filter value of the summary variable and updating to your most recent Apex class and trigger code, my groups are not persisting and are being combined into one group for both Renewal and Amendments. I have checked to ensure Calculate Immediately is checked in the CPQ settings, the price rule is active and the trigger is active. I there anything else I could check?
Thanks so much.
Hi Dennis,
After going through the latest here in terms of the Apex class, trigger and adding NULL in the summary variable. We are experiencing the issue where the groups are combined in to one group. Calculate Immediately is check on as well. Is there anything else we can check or that might cause this? Note, posting again as I am not sure my original post went through.
Thanks so much.
Updated QuoteTriggerHelper! Update yours and try again!
Hi Dennis,
Another thing I am now noticing is when I go to add products in the QLE, when I add a product it is automatically put into a group. Hitting ungroup does not take the product out of the group. It’s as if I am forced to use groups.
Thanks for any thoughts here. Matt
Hey Matt,
A couple things about all the comments!
– For some reason, your groups are not being created by the trigger. Then the price rule is setting it as grouped. I am going to re-implement this in a fresh org to see if I can reproduce this issue as it is happening to a few of you.
– Yes the price rule makes it so that all quotes have groups. You can put in a price condition so only renewal/amendment quotes with original lines that have groups fire this rule.
Thanks, Dennis, I really appreciate the help! Some additional details- it seems like whenever any quote is created, there is always a “Group 1” present that cannot be removed. Also, the formatting for our segmented products seems to get wacky.
FYI: This solution requires groups. The Price Rule forces it. I am actively looking for a way around this but for now, you’re forced into groups.
Hi DP,
We used this and working as expected but after deployment instead of just recreating the QL groups, it is also creating extra groups. For example, 4 QL groups on the Original Quote, on Amendment/Renew it is now 8.
Hi Justin, this is great feedback. I’ll be reviewing the solution to solve for this.
Hello itโs been a while. This has been updated to eliminate a situation where duplicate groups are created. ๐๐ป
Hi DP,
Thanks for the fix. We used this and working as expected but after re-amendment, grouping not working as expected, it is also creating extra groups. For example, 2 QL groups on the Original Quote, on Amendment/Renew it is now 4 and if it has 4 QL groups its creating 8 new grouping.
Yes! This is a known bug. I’ll be reviewing the solution to accommodate this. Thanks for the feedback!
Hi Dennis,
This is such an important functionality, unfortunate that it is not out of the box. Thank you for posting this solution here. Just wanted to ask if you are aware of any other workarounds to the endless spinning upon entering the Quote Line Editor and once in the QLE, the Quote Groups not persisting. I see that this is an issue that other users have faced, I have tried turning on Calculate Immediately and explicitly using NULL in the Summary Variable but to no avail.
Thank you for any thoughts,
Erik
Hi Erik!
The endless spinning is because of the “Group Line Items” checkbox not being checked. It turns out, currently, to have this solution, you must always check this box on all Quotes for it to work properly at all times.
Hello! when trying to Amend a Contract (the original Quote has Subscription Lines and Asset Lines), the Assets Quote Lines on the Amended Quote do not have a value on the SBQQ__Group__c field. This resulted in the endless spinning because some of the Quote Lines have blank Groups.
Hi Armand!
The class has been updated to include Assets! Good catch!
Hello! Does this also preserve group for renew subscriptions on the quote line editor? Is there a way to achieve that? Thanks!
Yes! This will pull all your original groups from your original quote into the renewal quote and assign the new group records to all of the appropriate quote lines.
Hello! So having issues with Renewed Quotes…
My company does Subscriptions per Year in Quote Line Groups. So we use the Subscription Start Date and Subscription End Date on QLGs heavily as that becomes the New Effective Subscription Date.
That means a bundle for 3 years starting 5/25/2022 – 5/24/2025 would be created across each Group with its individual line such that:
Year 1 QLG: Bundle A – Start Date 5/25/2022 End Date 5/24/2023
Year 2 QLG: Bundle A – Start Date 5/25/2023 End Date 5/24/2024
Year 3 QLG: Bundle A – Start Date 5/25/2024 End Date 5/24/2025
That being said, when a Contract is Activated and the Opportunity is Closed Won – the Renewal Opp is created and for testing, I check the Renewal Quote checkbox.
The problem on the Renewed Quote I’m seeing is :
A. The QLE Screen is stuck on Loading
B. Looking at the Quote Lines on the Created Quote all have incorrect Effective Start Dates / End Date
For Example
Year 1 QLG: Bundle A – Start Date 5/25/2022 End Date 5/24/2023 – Original Quote Line
Year 1 QLG: Bundle A – Start Date 5/25/2023 End Date 5/24/2023 – Renewed Quote Line
So all my Dates are screwed up – wondering if there’s a solution to this or if I should just clear this out? Or not sure tbh, because if the Renewal Dates don’t continue on from the Original Quote where
Renewed Start Date for Year 1 QLG Bundle A would be: Effective Start (Original Quote Line) + 36 or at least follow the Subscription Term in Months for the QLG?
I am stuck at the exactly same scenario as well where we use QLGs as subscription years.
Hey Ann Marie Jo! The stuck on loading is usually the Group Line Items checkbox not being checked. But it could also be because one or more of your lines don’t have a QLG? Check to see if that’s the case. If so, I can create this scenario in my org and debug through to see what the deal is. Sorry for the late response!
Hiya –
So for some reason, the QLG Groups are carrying over for most QLI but not some components. Is there a reason why this is happening? This is tied to the same issue I posted.
Would love to talk to you about this – maybe I could book your services for an hour or two’s worth of consult?
Yes! You can definitely book some time! Shoot me an email at dennis@morecpq.com and we can set something up!
I have used the code provided and created the price rule and ran the script too. On renewal quote, I can see the Group Line Items field is checked as well. However, I am still seeing a forever waiting dots on quote line editor. Can you help me fix this?
Hey Vaibhav! The stuck on loading is usually the Group Line Items checkbox not being checked. But it could also be because one or more of your lines donโt have a QLG? Check to see if thatโs the case. If so, I can create this scenario in my org and debug through to see what the deal is. Sorry for the late response!
Heya,
The issue I’m seeing is that we’re when an Amendment is created on a Renewal Opp; the groups are inheriting from the Original Quote and not the renewal.
e.g. Quote has 3 lines running from April 1st – June 31st this is then contracted
A Renewal opp is generated from the original contract and the Quote is updated to have 3 lines running July 1st – September 30th this in turn is also contracted.
When an amendment is made on the renewal opp the Quote Line groups are showing dates for April 1st – June 31st and not July 1st – September 30th
This is resolved. Thanks for the tip and testing! There’s an updated installer link in this article. Let me know if you still have issues.
Hello!
I am running into an issue when running a second amendment on the Contract.
Error Message: Error:Error while saving Quote Lines:SBQQ.ValidationException: [“QuoteTrigger: execution of AfterUpdate caused by: System.DmlException: Update failed. First exception on row 8 with id a8L5b000003wSsOEAU; first error: FIELD_CUSTOM_VALIDATION_EXCEPTION, Line 9: Select a start date on or after ’11/15/2022′.: [] Class.QuoteTriggerHelper.insertMissingAmendmentRenewalLineGroups: line 142, column 1 Class.QuoteTriggerHelper.doInsertMissingAmendmentRenewalLineGroups: line 14, column 1 Class.QuoteTriggerHelper.runTriggers: line 6, column 1 Trigger.QuoteTrigger: line 3, column 1”] (System Code)
The original quote has 3 QLG divided into Year 1, Year 2, Year 3 spanning 4/30/2021-4/29/2024
The amendment was done 11/15/2022 to the end of the contract term.
We are trying to do another amendment, but are running into that error message.
It seems to be related to that amended quote that’s been contracted into the original contract.
I checked that Quote and it does have Groups as well called Year 2, Year 3.
What is the fix for this sort of situation? Thank you!
Oh no! Let me see if I can resolve this error. I must be doing something that assumes the start/end dates are the same across groups.