How to deploy Salesforce CPQ with Copado

Please note that assistance on CPQ migration is out of the scope of Copado Support. If you need help with such deployment, feel free to contact your Customer Success Manager to engage our Profession Services Organization to support you. 

You can use these sample queries to retrieve the records from your source organization and deploy them to the destination Org of the deployment. Each query must be added as a single Data Deployment Step. 

The Data Steps must be ordered with the highest parent object first and the child object last, as explained in the Data Deployment Step documentation. You can also add Apex Anonymous scripts as needed to perform custom operations with CPQ records (see image below).

User-added image

The Data Step requires you to create an External Id field in each of the CPQ objects. This is so that when the records get deployed to the destination environment, the existing records in the destination environment do not get overwritten but instead get updated with the latest changes. 

Once the external Id field is created, you will need to populate a value for this field. This can be achieved with a Workflow Rule that updates the field whenever a new record is created. The external Id value could simply be the Id of the record. Nonetheless, it is recommended to implement a custom formula for the External Id field so that the records can be better traceable wherever they're deployed to. 


Samples of queries

Deploy Service Discount Schedule
 
SELECT Id, Name, SBQQ__Type__c, SBQQ__DiscountUnit__c, SBQQ__AggregationScope__c, SBQQ__QuoteLineQuantityField__c, External_Id__c,SBQQ__PriceScale__c FROM SBQQ__DiscountSchedule__c WHERE External_Id__c = 'Default_ProductXXX'

Deploy Service Discount Tiers
 
SELECT Id, SBQQ__Schedule__r.External_Id__c, Name, SBQQ__Discount__c,External_Id__c, SBQQ__LowerBound__c, SBQQ__UpperBound__c FROM SBQQ__DiscountTier__c WHERE SBQQ__Schedule__r.External_Id__c = 'Default_ProductXXX'

Deploy Products
 
Select Id, Name, ProductCode, Family, IsActive,SBQQ__SortOrder__c, SBQQ__BlockPricingField__c, SBQQ__Specifications__c, SBQQ__AssetAmendmentBehavior__c, SBQQ__AssetConversion__c, SBQQ__QuantityEditable__c, SBQQ__DefaultQuantity__c, External_Id__c, SBQQ__ChargeType__c, SBQQ__OptionSelectionMethod__c, SBQQ__DiscountSchedule__r.External_Id__c, SBQQ__SubscriptionBase__c, SBQQ__SubscriptionType__c, SBQQ__BillingType__c FROM Product2 WHERE External_Id__c LIKE 'ProductXXX_OS%'
          

Service Product Pricebook Entry
 
SELECT Id, Product2.External_Id__c, Pricebook2.External_Id__c, UnitPrice, UseStandardPrice, External_Id__c, CurrencyIsoCode, IsActive FROM PricebookEntry WHERE Product2.External_Id__c LIKE 'ProductXXX_OS%'

Deploy Quote Term
 
SELECT Id, SBQQ__PrintOrder__c, SBQQ__Status__c, SBQQ__ConditionsMet__c, External_Id__c, SBQQ__Body__c, SBQQ__Type__c, SBQQ__Active__c FROM SBQQ__QuoteTerm__c WHERE External_Id__c = 'ProductXXXTerm1'

Deploy Summary Variable
 
SELECT Id, Name, SBQQ__TargetObject__c, External_Id__c, SBQQ__FilterField__c, SBQQ__Operator__c, SBQQ__FilterValue__c, SBQQ__AggregateFunction__c, SBQQ__AggregateField__c FROM SBQQ__SummaryVariable__c WHERE External_Id__c = 'SumProductXXX'
          

Deploy Term Conditions
 
SELECT Id, SBQQ__QuoteTerm__r.External_Id__c, SBQQ__TestedVariable__r.External_Id__c, SBQQ__Operator__c,SBQQ__Value__c, External_Id__c FROM SBQQ__TermCondition__c WHERE SBQQ__QuoteTerm__r.External_Id__c = 'ProductXXXTerm1'
          

Apex Anonymous

The Apex Anonymous script below is meant to handle Pricebook Entries. This is due to deployment errors appearing with standard prices and default Currency ISO Codes while performing upserts. In this example. you can solve it by using insert.

You can execute an Apex Anonymous in the destination Org as part of the deployment by using the Apex Deployment Step
 
// The deletion of the original PricebookEntries must be done in two steps. This is an SFDC requirement.
          List<PricebookEntry> oldServiceEntries = [SELECT Id FROM PricebookEntry WHERE Product2.External_Id__c LIKE 'ProductXXX_O%' AND UseStandardPrice = TRUE ORDER BY UseStandardPrice DESC];
          if(oldServiceEntries.size()>0) delete oldServiceEntries;
          
          oldServiceEntries = [SELECT Id FROM PricebookEntry WHERE Product2.External_Id__c LIKE 'ProductXXX_O%' AND UseStandardPrice = FALSE ORDER BY UseStandardPrice DESC];
          if(oldServiceEntries.size()>0) delete oldServiceEntries;
          
          // CSV converted to Json - prepare the data set on a google spreadsheet or excel, then save it as a CSV and finally convert it into Json with a tool like https://www.csvjson.com/csv2json
          String mySon = '[{"id":"01u2500000623Z3AAI","Product2.External_Id__c":"ProductXXX_OS","Pricebook2.External_Id__c":"01sb0000000JB84AAG","UnitPrice":1450,"UseStandardPrice":"FALSE","External_Id__c":"01u2500000623Z3AAI","CurrencyIsoCode":"USD","IsActive":"TRUE"},{"id":"01u2500000623Z4AAI","Product2.External_Id__c":"ProductXXX_OS","Pricebook2.External_Id__c":"01sb0000000JB84AAG","UnitPrice":1200,"UseStandardPrice":"FALSE","External_Id__c":"01u2500000623Z4AAI","CurrencyIsoCode":"EUR","IsActive":"TRUE"},{"id":"01u2500000623TUAAY","Product2.External_Id__c":"ProductXXX_OS_2","Pricebook2.External_Id__c":"01sb0000000JB84AAG","UnitPrice":850,"UseStandardPrice":"FALSE","External_Id__c":"01u2500000623TUAAY","CurrencyIsoCode":"USD","IsActive":"TRUE"},{"id":"01u2500000623TVAAY","Product2.External_Id__c":"ProductXXX_OS_2","Pricebook2.External_Id__c":"01sb0000000JB84AAG","UnitPrice":750,"UseStandardPrice":"FALSE","External_Id__c":"01u2500000623TVAAY","CurrencyIsoCode":"EUR","IsActive":"TRUE"},{"id":"01u2500000623Z5AAI","Product2.External_Id__c":"ProductXXX_OS","Pricebook2.External_Id__c":"01sb0000002aM52AAE","UnitPrice":1450,"UseStandardPrice":"TRUE","External_Id__c":"01u2500000623Z5AAI","CurrencyIsoCode":"USD","IsActive":"TRUE"},{"id":"01u2500000623Z6AAI","Product2.External_Id__c":"ProductXXX_OS","Pricebook2.External_Id__c":"01sb0000002aM52AAE","UnitPrice":1200,"UseStandardPrice":"TRUE","External_Id__c":"01u2500000623Z6AAI","CurrencyIsoCode":"EUR","IsActive":"TRUE"},{"id":"01u2500000623TZAAY","Product2.External_Id__c":"ProductXXX_OS_2","Pricebook2.External_Id__c":"01sb0000002aM52AAE","UnitPrice":850,"UseStandardPrice":"TRUE","External_Id__c":"01u2500000623TZAAY","CurrencyIsoCode":"USD","IsActive":"TRUE"},{"id":"01u2500000623TaAAI","Product2.External_Id__c":"ProductXXX_OS_2","Pricebook2.External_Id__c":"01sb0000002aM52AAE","UnitPrice":750,"UseStandardPrice":"TRUE","External_Id__c":"01u2500000623TaAAI","CurrencyIsoCode":"EUR","IsActive":"TRUE"}]';
          
          // List of PricebookEntries to insert it later
          List<PricebookEntry> prices = new List<PricebookEntry>();
          
          // Get the list of products. Otherwise a nullpointer exception occurs on External ID
          List<Product2> products = new List<Product2>([SELECT Id, External_Id__c, ExternalId FROM Product2 WHERE IsActive = true]);
          
          // Convert Products to a map for referencing
          Map<String, Product2> product2Map = new Map<String, Product2>();
          for(Product2 prod : products){
          product2Map.put(prod.External_Id__c, prod);
          }
          
          // Convert Pricebooks to a map for referencing
          List<Pricebook2> pbs = new List<Pricebook2>([SELECT Id, External_Id__c FROM Pricebook2]);
          
          Map<String, Pricebook2> pricebookMap = new Map<String, Pricebook2>();
          for(Pricebook2 pb : pbs){
          pricebookMap.put(pb.External_Id__c, pb);
          }
          
          // Construct the PricebookEntries
          List<Object> items = (List<Object>)JSON.deserializeUntyped(mySon);
          for(Object item : items){
          System.debug(item);
          Map<String, Object> myMap = (Map<String, Object>) item;
          System.Debug(myMap.get('CurrencyIsoCode'));
          PricebookEntry pbe = new PricebookEntry();
          
          System.debug(myMap.get('Product2.External_Id__c'));
          
          pbe.Product2Id = product2Map.get((String) myMap.get('Product2.External_Id__c')).Id;
          pbe.Pricebook2Id = pricebookMap.get((String) myMap.get('Pricebook2.External_Id__c')).Id;
          
          pbe.UnitPrice = (Decimal) myMap.get('UnitPrice');
          pbe.UseStandardPrice = Boolean.valueOf(myMap.get('UseStandardPrice'));
          pbe.External_Id__c = (String) myMap.get('External_Id__c');
          pbe.CurrencyIsoCode = (String) myMap.get('CurrencyIsoCode');
          pbe.IsActive = Boolean.valueOf( myMap.get('IsActive'));
          prices.add(pbe);
                  }
          
          // insert the PricebookEntries. This only works on insert. The upsert command is limited since only 3 fields can be updated on the PriceBookEntry due to the master detail relationship between Product and Pricebook.
          insert prices;

 

How did we do?