Skip to main content

#Salesforce CPQ & Billing28 discussing

I have 4 Approval Rules that evaluate as 4 steps (using the Advanced Approvals package, not native Salesforce Approvals):

 

Discount 31%+ (step 1)

 

Discount 41%+ (step 2)

 

Discount 56%+ (step 3)

 

Discount 71%+ (step 4)

 

When I preview and submit a quote with a 71% discount or higher it correctly lists all 4 steps with the appropriate approvers. The first approver is the Sales Rep's Manager (dynamic approver using the Approver Field field). When the first person approves it should email the second approver and so on. However, when the first person approves it automatically deletes the remaining Approval records and marks the entire Quote as Approved. Any idea why it's doing this? I've made the rules as simple as possible (only 1 condition). 
11 answers
  1. Today, 2:22 PM

    I might be late as this post is from 2018 , I resolved this by removing the effective date field value and effective start date value from approval rules records

0/9000

I have two items related to reconfiguring bundles and how to handle existing subscriptions and assets. 

First Bundle

 

Currently we have a bundle configured as below:

  1. Bundle Header (Parent) - Product A (renewable subscription that has a price associated with it)
    1. Child Product (product option) - Product B (one-time asset that has a price associated with it).

When added to a quote, since the child is not flagged as Bundled, pricing sits at both lines. The business now wants to re-design the bundle. They want it to look like the following:

  1.  Bundle Header (Parent) - Product C (renewable subscription that has a $0 price)
    1. Child Product (product option) - Product A (renewable subscription that has a price associated with it).
    2. Child Product (product option) - Product B (one-time asset that has a price associated with it).

Pricing would sit at each child product line. I was thinking I could just reconfigure the bundle by creating the bundle header and adding Product A as a child and then Product B as a child. And remove the child option (product B) from from Product A (basically breaking the original bundle apart).  

But my concern is existing data that was sold under the current configuration. Do I create subscription bundle headers and then realign the current subscriptions and assets to the new bundle header (adding the Root ID, Required By ID, adjust the product option information on each subscription and asset)? If so, what are the possible risks to amendments and renewals? If this is not the best approach what would you suggest? I can’t create new product codes so I need to use the current products and product codes. 

 

Second Bundle

We have a one time subscription Service product that is part of two bundles. We want to break this apart from the bundles to make it standalone and have it only added on new sales quotes. This Service product is a one-time subscription product.  

For new sales, I need to hide the options within the two bundles and have the product auto added. For amendments, I need the options available for the products being revised but if we are adding one of the two bundles for the first time we need the option for the Service product hidden in both bundles.  

How would i build this? For new sales I am thinking a product selection rule to add the standalone product. For new sales a product selection rule to hide the options. For amendments, how would I approach this? 

 

Thank you. 

 

#Salesforce CPQ & Billing  #CPQ  #CPQAskAnExpert

1 answer
  1. Sep 27, 1:11 PM

    Hi Rachel,

    For the first bundle, I’d be cautious about manually changing the existing Subscription/Asset hierarchy (Root ID, Required By ID, Product Option, etc.). Those relationships are used by CPQ/Billing during amendments, renewals, and asset/subscription management, so manually realigning historical records could introduce issues later. 

     

    A safer approach is generally to keep the existing sold structure intact for historical subscriptions/assets and use the new bundle configuration for future sales. Since you need to retain the existing product codes, I’d validate the impact on amendments and renewals in a full sandbox before changing any existing subscription hierarchy. 

     

    For the second bundle, your approach of using Product Selection Rules for new sales makes sense. For amendments, I’d make the rule criteria aware of whether the quote is an amendment versus a new sale, and whether the bundle already exists on the customer's subscription/assets. That way, the Service product can remain available when amending an existing bundle while being excluded when the bundle is being added for the first time. 

     

    Because this involves existing subscription/asset relationships, I’d test several scenarios—new sale, renewal, amendment of the existing bundle, and adding the bundle for the first time—before deploying the configuration. If you can share the CPQ package version and how your amendment quotes are currently configured, it would help determine the exact rule conditions. 

0/9000

Hello, 

Currently our system is built that upon the creation of a quote for a new sale they set a custom Contract Term field on the quote to the length they want the term to be (for example, 36) and then on the quote they set the Subscription Term to 12. They have automation that then creates a Master Agreement that is a high level agreement that contains the full length of the contract (for example, start date of 1/1/2026 through end date 12/31/2029) and on the master agreement the contract term says 36. And then there is a child agreement that is related to the master agreement (a child to this agreement) that has a contract term of 12 and a start date of 1/1/2026 through 12/31/2026. Then on 12/30/20206 they create a new Subscription Agreement via an auto renewal process for Year 2 with dates of 1/1/2027 through 12/31/2027. Then do the same thing at Year 3 for dates of 1/1/2028 through 12/31/2028. Then on 1/1/2029 they complete what is called a contract renewal where the above starts over again with a new master agreement then child subscription agreements. 

 

Now with a new sales they want to move to a quote with the custom contract term of 36 months and subscription term of 36 months but within the quote are quote line groups for Year 1, Year 2, Year 3 of 12 months. When the contract creates they want one contract containing the whole contracts information. Then at the end of the 3 years the "contract renewal" occurs for the 36 months for the contract term and Subscription Term. 

 

The issue is existing contracts on the old structure. How do we handle the interim subscription agreements before the contract renewal if we remove the master agreement. And then how do we move to the new contract renewal process at the end of the compete term. I am assuming some sort of data migration but not sure what that would be or would look like. 

 

Has anyone else had to go through this? And nope they do not want MDQ. 

Thank you. 

 

#Salesforce CPQ & Billing  #CPQ  #CPQGurus  #CPQAskAnExpert  #Salesforce Developer  #Architects

2 answers
  1. Sep 20, 2:32 PM

    @Sai T

     

    Thank you for your response. This is the recommendation I gave as how to move forward with this change. 

    If you had to have some mechanism to determine the existing current structure versus the new what would you use? For example we have a Year in Contract field for the current structure which is a formula field but that doesn't say whether a contract is new or existing. I wonder if we should create a field on the backend that we update via an upload that flags a contract as "Legacy"/"Existing" new. Just trying to think of the best approach so that the automation will know that a contract is existing or new so we don't accidentally take the interim renewal and try to convert it to the new structure. We do have a field called Upcoming renewal type that says Subscription Renewal or Contract Renewal. I think we can use that to say basically exclude subscription renewal and look at the ones that are contract renewal.

0/9000

Hi All,

Currently, I'm updating our Oracle CPQ manage package to 8.6 version from 7.0 version.

The new version requires install two manage package. This process is already done and now i'm migrating some apex classes and test classes.

When I'm running my test classes, I'm facing next issue:

 

caused by: cafsl.DataSetService.DataSetException: Could not load the transaction; integration settings are missing.

 

I'm already creating a record of cpq site settings and embedded cpq site setting in my test classes and it's still failing. Does anyone know what I need to create?

 

Thanks

 

@Salesforce CPQ 

@* Salesforce Revenue Cloud * 

5 answers
  1. Sep 16, 1:01 PM

    Have you created the cafsl__Embedded_CPQ_Settings__c record in the test class? If not, please try adding it.

0/9000

🚨 HIRING | SALESFORCE CPQ DEVELOPER 🚨

🚀 Looking for a Salesforce CPQ Developer to join a Leading Global Consulting & Professional Services Organization.

📍 Hyderabad | Gurgaon

 

🏠

Hybrid Work Model

 

💼

Experience:

4+ Years 

⚡

Immediate Joiners / Serving Notice Period

🔥 Key Requirements

✅ Strong Salesforce & Salesforce CPQ

experience 

✅ Hands-on

CPQ implementation

experience 

✅ Experience delivering end-to-end CPQ projects 

✅ Strong CPQ configuration & development skills

🌟 Exciting opportunity to work on impactful Salesforce CPQ projects with a global organization!

📩 Share your CV: supriyah@wen-jobs.co.in

 

📞

Contact:

7892999581 

 

#Job Postings  #Salesforce CPQ & Billing  #CPQ  #Salesforce

0/9000
Has anybody heard of a way segment (or desegment) all line items on a quote? 
12 answers
  1. Sep 9, 6:57 PM

    @Rohit paruchuri

    - 

    I'm open to meet with you if it helps to understand your use case.  

     

    You'll need to add a Screen before the action to warn the rep to save their quote before clicking the button, OR you can add an apex action to save the Quote.  

     

    To refresh the Quote after the action, use this invocable CalculateQuote action I built: 

     

    public with sharing class CPQCalculateQuoteAction {

    public class Input {

    @InvocableVariable(required=true label='CPQ Quote ID')

    public Id quoteId;

    }

    public class Result {

    @InvocableVariable(label='Calculation Request ID')

    public Id requestId;

    @InvocableVariable(label='CPQ Quote ID')

    public Id quoteId;

    }

    public class Status {

    @AuraEnabled

    public String state;

    @AuraEnabled

    public String message;

    @AuraEnabled

    public Id quoteId;

    }

    public class CalculationException extends Exception {

    }

    // Injectable boundary: unit tests never run the managed calculator or create quotes.

    public virtual class Gateway {

    public virtual void checkAccess(Set<Id> quoteIds) {

    if (

    !SBQQ__Quote__c.SObjectType.getDescribe().isAccessible() ||

    !SBQQ__Quote__c.SObjectType.getDescribe().isUpdateable()

    ) {

    throw new CalculationException(

    'Read and edit access to CPQ Quotes is required.'

    );

    }

    Set<Id> editable = new Set<Id>();

    for (UserRecordAccess access : [

    SELECT RecordId, HasEditAccess

    FROM UserRecordAccess

    WHERE UserId = :UserInfo.getUserId() AND RecordId IN :quoteIds

    ]) {

    if (access.HasEditAccess)

    editable.add(access.RecordId);

    }

    if (!editable.containsAll(quoteIds)) {

    throw new CalculationException(

    'A quote does not exist or you cannot edit it.'

    );

    }

    }

    public virtual String readQuote(Id quoteId) {

    return SBQQ.ServiceRouter.read(

    'SBQQ.QuoteAPI.QuoteReader',

    String.valueOf(quoteId)

    );

    }

    public virtual void calculate(String quoteJSON) {

    Map<String, Object> context = new Map<String, Object>{

    'quote' => JSON.deserializeUntyped(quoteJSON),

    'callbackClass' => 'CPQCalculateQuoteCallback'

    };

    SBQQ.ServiceRouter.load(

    'SBQQ.QuoteAPI.QuoteCalculator',

    null,

    JSON.serialize(context)

    );

    }

    public virtual void saveQuote(String quoteJSON) {

    SBQQ.ServiceRouter.save('SBQQ.QuoteAPI.QuoteSaver', quoteJSON);

    }

    public virtual Id launch(Set<Id> requestIds) {

    return Database.executeBatch(new CPQCalculateQuoteBatch(requestIds), 1);

    }

    }

    @TestVisible

    private static Gateway gateway = new Gateway();

    @InvocableMethod(

    label='Calculate CPQ Quote'

    description='Queues CPQ calculation and save. Pass the request ID to the Wait for CPQ Calculation Flow screen component. Completion is asynchronous.'

    )

    public static List<Result> calculateQuotes(List<Input> inputs) {

    if (inputs == null || inputs.isEmpty() || inputs.size() > 200) {

    throw new CalculationException('Supply between 1 and 200 CPQ Quote IDs.');

    }

    Set<Id> quoteIds = new Set<Id>();

    for (Input input : inputs) {

    if (

    input == null ||

    input.quoteId == null ||

    input.quoteId.getSObjectType() != SBQQ__Quote__c.SObjectType

    ) {

    throw new CalculationException(

    'Each input must be an SBQQ__Quote__c record ID.'

    );

    }

    quoteIds.add(input.quoteId);

    }

    gateway.checkAccess(quoteIds);

    List<CPQ_Calculation_Request__c> requests = new List<CPQ_Calculation_Request__c>();

    for (Id quoteId : quoteIds) {

    requests.add(

    new CPQ_Calculation_Request__c(

    Quote_Id__c = String.valueOf(quoteId),

    Active_Quote_Key__c = String.valueOf(quoteId),

    Status__c = 'Queued'

    )

    );

    }

    // The unique active key prevents races across users, not just duplicates in this list.

    Savepoint sp = Database.setSavepoint();

    try {

    insert requests;

    Set<Id> requestIds = new Map<Id, CPQ_Calculation_Request__c>(requests)

    .keySet();

    Id batchId = gateway.launch(requestIds);

    Map<Id, Id> requestByQuote = new Map<Id, Id>();

    for (CPQ_Calculation_Request__c request : requests) {

    request.Batch_Job_Id__c = String.valueOf(batchId);

    requestByQuote.put(Id.valueOf(request.Quote_Id__c), request.Id);

    }

    update requests;

    List<Result> results = new List<Result>();

    for (Input input : inputs) {

    Result result = new Result();

    result.quoteId = input.quoteId;

    result.requestId = requestByQuote.get(input.quoteId);

    results.add(result);

    }

    return results;

    } catch (Exception ex) {

    Database.rollback(sp);

    throw new CalculationException(

    'Could not queue calculation. A quote may already have an active or unresolved request. ' +

    ex.getMessage()

    );

    }

    }

    @AuraEnabled

    public static Status getStatus(Id requestId) {

    if (requestId == null)

    throw new AuraHandledException('Calculation request ID is required.');

    List<CPQ_Calculation_Request__c> requests = [

    SELECT Quote_Id__c, Status__c, Error__c, Batch_Job_Id__c

    FROM CPQ_Calculation_Request__c

    WHERE Id = :requestId

    LIMIT 1

    ];

    if (requests.isEmpty())

    throw new AuraHandledException(

    'Calculation request not found or not accessible.'

    );

    CPQ_Calculation_Request__c request = requests[0];

    Id quoteId = Id.valueOf(request.Quote_Id__c);

    gateway.checkAccess(new Set<Id>{ quoteId });

    Status result = new Status();

    result.quoteId = quoteId;

    result.state = request.Status__c;

    result.message = request.Error__c;

    if (

    result.state != 'Completed' &&

    result.state != 'Failed' &&

    String.isNotBlank(request.Batch_Job_Id__c)

    ) {

    Id batchId = Id.valueOf(request.Batch_Job_Id__c);

    for (AsyncApexJob job : [

    SELECT Status, NumberOfErrors

    FROM AsyncApexJob

    WHERE Id = :batchId

    ]) {

    if (

    job.Status == 'Failed' ||

    job.Status == 'Aborted' ||

    (job.Status == 'Completed' &&

    job.NumberOfErrors > 0)

    ) {

    result.state = 'Failed';

    result.message = 'The calculation batch stopped or reported an unhandled error. An administrator must inspect Apex Jobs before retrying.';

    }

    }

    }

    return result;

    }

    public static void processRequest(Id requestId) {

    CPQ_Calculation_Request__c request = [

    SELECT Quote_Id__c, Status__c

    FROM CPQ_Calculation_Request__c

    WHERE Id = :requestId

    ];

    if (request.Status__c != 'Queued')

    return;

    try {

    Id quoteId = Id.valueOf(request.Quote_Id__c);

    gateway.checkAccess(new Set<Id>{ quoteId });

    String quoteJSON = gateway.readQuote(quoteId);

    // No DML before CPQ: calculation can make callouts or schedule a callback.

    gateway.calculate(quoteJSON);

    request = [

    SELECT Status__c

    FROM CPQ_Calculation_Request__c

    WHERE Id = :requestId

    FOR UPDATE

    ];

    // A synchronous callback may already have saved the quote and completed the request.

    if (request.Status__c == 'Queued') {

    request.Status__c = 'Calculating';

    update request;

    }

    } catch (Exception ex) {

    failRequest(requestId, ex.getMessage());

    }

    }

    public static void saveCalculatedQuote(String quoteJSON) {

    Map<String, Object> model = (Map<String, Object>) JSON.deserializeUntyped(

    quoteJSON

    );

    Map<String, Object> record = (Map<String, Object>) model.get('record');

    Id quoteId = Id.valueOf((String) record.get('Id'));

    String activeKey = String.valueOf(quoteId);

    List<CPQ_Calculation_Request__c> requests = [

    SELECT Status__c

    FROM CPQ_Calculation_Request__c

    WHERE Active_Quote_Key__c = :activeKey

    FOR UPDATE

    ];

    if (requests.isEmpty() || requests[0].Status__c == 'Failed') {

    throw new CalculationException(

    'No active calculation request; the callback was not saved.'

    );

    }

    CPQ_Calculation_Request__c request = requests[0];

    Savepoint sp = Database.setSavepoint();

    try {

    gateway.checkAccess(new Set<Id>{ quoteId });

    gateway.saveQuote(quoteJSON);

    request.Status__c = 'Completed';

    request.Active_Quote_Key__c = null;

    request.Error__c = null;

    update request;

    } catch (Exception ex) {

    Database.rollback(sp);

    failRequest(request.Id, ex.getMessage());

    }

    }

    private static void failRequest(Id requestId, String message) {

    // Keep the active key on errors: an unobserved CPQ callback may still be in flight.

    update new CPQ_Calculation_Request__c(

    Id = requestId,

    Status__c = 'Failed',

    Error__c = message.left(32000)

    );

    }

    }

    ```

0/9000

Once invoices are generated, how are you managing payment reminders, dunning, and collections follow-up? Custom Flow/Apex, a third-party AR tool, or still manual chasing in Service Cloud or spreadsheets?  

 

Disclosure: I work on Quick Receivable, an AR automation app for Salesforce. Genuinely curious what teams here are doing post-invoice, not trying to pitch anything. 

 

#Salesforce CPQ & Billing

1 answer
  1. Sep 7, 2:44 PM

    We’ve found Flow is fine for basic reminders, but collections gets messy pretty quickly once invoices go overdue in different ways. At that point I’d rather have a proper AR tool handling the follow ups than keep adding more Flow rules or tracking things in spreadsheets.

0/9000

We have a product rule set up as follows in Sandbox and Production. Below is the Current Product Rule Configuration.  

 

Product Rule Header: 

  • Type=Selection 
  • Conditions Met = All 
  • Scope = Product 
  • Evaluation Event = Always 
  • Evaluation Order = 10 

Error Condition: 

  • Tested Object = Quote 
  • Tested Field = Boost = True
  • Operator = equals 
  • Filter Type = Value
  •  Filter Value = TRUE 

Price Action 1: 

  • Product = Product A 
  • Type = Enable 

Price Action 2: 

  • Product = Product B 
  • Type: Enable & Add 

Price Action 3: 

  • Product = Product C 
  • Type = Enable & Add 
  • Required = True 

Price Action 4: 

  • Product = Product C 
  • Type = Hide 

Configuration Rule

:  

Product = Bundle 1  

 

Results in the Configurator

 

In Sandbox the following is occurring in the Configurator:

  • Product A is not appearing
  • Product B is not appearing
  • Product C is appearing and checked

In Production the following is occurring in the Configurator:

  • Product A is appearing
  • Product B is appearing and checked
  • Product C is not appearing

What the Sales team would like to have happen is:

  • Product A is appearing
  • Product B is appearing and checked
  • Product C is appearing and checked

What correction needs to happen to get this to occur? At a minimum I would think I would delete Product Action 4. 

 

Thank you. 

Rachel 

 

#Salesforce CPQ & Billing  #CPQ @* Salesforce Revenue Cloud *

1 answer
  1. Sep 6, 2:36 AM

    Your instinct to delete Price Action 4 is right, and here's why it's likely the actual root cause of the environment difference, not just a cleanup step. 

    You have two Product Actions targeting the same product (Product C) within one rule, with directly contradictory outcomes: Action 3 says Enable & Add + Required, Action 4 says Hide. Unlike Product Rules themselves (which have an explicit Evaluation Order field to control which rule "wins" when multiple rules affect the same product), individual Product Actions within a single rule don't have a documented, configurable sequence field in the UI. When two actions in the same rule conflict like this, which one is applied can come down to internal record creation order - and if this rule was built independently in Sandbox and in Production (rather than deployed as a single unit via change set/package that preserves record order), it's entirely plausible the two environments evaluate Action 3 vs. Action 4 in a different order, which would produce exactly the opposite behavior you're seeing on Product C between the two orgs. 

    Given the outcome the Sales team actually wants — Product C appearing and checked — the fix is straightforward: delete Price Action 4 (Hide on Product C) entirely, and keep only Price Action 3 (Enable & Add, Required) for Product C. That removes the ambiguity rather than relying on action-order behavior that isn't guaranteed to be consistent across orgs. 

    One more thing worth confirming before you conclude it's purely a config issue: since Product A and B's behavior also flipped between environments (not just C), double-check that the Quote.Boost__c field is actually set to TRUE in both orgs during your test — if the condition itself isn't evaluating the same way in both places (e.g., a default value difference on that field between Sandbox and Production data), that alone could explain the whole rule appearing to "not fire" in one environment before you even get to the Product C ordering issue.

0/9000

Hello!

Any idea why search filters are not visible for specific users? I see it in Quote Line Editor but user with the same permission sets does not. We have different profiles but I can not guess which option is resposible for that. Modify all and view all permission do not change anything

6 answers
  1. Aug 10, 2023, 7:34 AM

    @Pawel Wiktorek Please check-

    1. If User has access to Search Filter records.

    2. If there are multiple "Add Products" custom actions and the other user is looking at a different Add Products custom action.

    3. Try executing scripts under CPQ Package Setting -> Additional Settings

    4. Try creating another Search Filter with Product Code-contains-blank. If it works, change the Field to HierarchyLocation__c

    5. If nothing helps, please create a case with SF CPQ Support.

0/9000

Hello, 

I am trying to understand best practice for discount schedules when applying to a brand new sale and when applying to an amendment. Here are the scenarios:

  1. New Sale - pulls in current discount schedule. The discount schedule is using either the SBQQ__Quantity__c field or a custom quantity field. Cross Orders is checked true. Some discount schedules have aggregation scope blank and some have aggregation scope set to Product.
  2. Amendments - subscriptions lines being pulled in through the amendment process apply the original discount schedule as expected. Any new lines added are applying old discount schedules without a tier. But we want the new discount schedule to apply to new lines. Things to note are in number 1 above the SBQQ__QuoteLineQuantityField__c and the fact that Cross Order is checked as true. I have written a price rule that does the following and is working as expected on the new lines but I am not getting the Discount Tiers. Do I need an additional price rule to apply the tier or do I need to create duplicate Discount Schedules that look at the SBQQ__EffectiveQuantity__c field for amendments?
    1. Price Rule Header
      1. Evaluation Scope: Calculator
      2. Lookup Object: SBQQ__DiscountSchedule__c
      3. Calculator Evaluation Event: Before Calculate
    2. Price Conditions
      1. SBQQ__Existing__c equals false
      2. SBQQ__Product__c not equal to blank
    3. Lookup Query
      1. Query 1
        1. Tested Object: Quote Line
        2. Match Type: Field Value
        3. Tested Field: SBQQ__Product__c
        4. Operator: equals
        5. Lookup Field: SBQQ__Product__c
      2. Query 2
        1. Tested Object: Quote
        2. Match Type: Field Value
        3. Tested Field: SBQQ__PricebookId__c
        4. Operator: equals
        5. Lookup Field: SBQQ__Pricebook__c
      3.   Query 3
        1. Match Type: Static Value
        2. Tested Value: true
        3. Operator: equals
        4. Lookup Field: Active_for_New_Quote_Lines__c
      4.  Query 4
        1. Match Type: Static Value
        2. Tested Value: 2026
        3. Operator: equals
        4. Lookup Field: APricing_Year__c
    4.  Price Actions
      1. Price Action 1
        1. Target Object: Quote Line
        2. Target Field: SBQQ__DiscountSchedule__c
      2. Price Action 2
        1. Target Object: Quote Line
        2. Target Field: SBQQ__DiscountSchedule__c
        3. Source Lookup Field: Id

Thank you. 

 

#Salesforce CPQ & Billing  #CPQ

1 answer
0/9000