Blog

Quotes and Opportunity Line Items – Add a Trigger to Sync Custom Fields

Background:

Quotes in Salesforce represent a proposal of prices for your company’s products and services to a particular prospect. If the Quote object is enabled on your page layout, a Sales Rep can generate a Quote from an Opportunity and its Line Items (OLIs). Each Opportunity can have multiple associated Quotes, and any one of them can be synced with the Opportunity. When a Quote and an Opportunity are synced together, any change to product line items on the Quote syncs with Products on the Opportunity, and vice versa. All this is standard Salesforce functionality.

Problem:

However, this sync only happens for standard fields on the line items, not for custom fields. Since product pricing tends to be complex and requires flexibility for most businesses, this means that many OLIs have custom calculation fields that are central to a business’s pricing offers. Some examples where custom fields might be lurking are annualized pricing calculations for products sold at a monthly unit price, custom descriptions or names to display to customers on the Quote document, or custom date ranges for ongoing services.

Let’s take a look at an example of this issue in action.

Here’s an Opportunity record with no products or quotes initially:

Let’s add a Quote to this Opportunity record by clicking “New Quote”:

And add some products to the Quote (i.e Quote Line Items):

 Now when we click the “Start Sync” button on the Quote, it populates all Quote line items to the associated Opportunity (i.e it creates matching Opportunity Line Items).
 
So far so good.
If you change any value on a Quote line item, it will immediately reflect that value on the respective OLI.  But what about that cool custom field I made to tag whether this product is a renewal of a previous contract?? There’s no point-and-click approach to map Opportunity Line Item fields with Quote line item fields.
Here’s a description of how we’ve solved this quandary with some custom fields and a little Apex.
Step 1) Create a custom formula field on Quote line item (I call it “Opportunity Line Item ID”).

This will return the synced Opportunity Line Item ID on the Quote Line Item and let our quote find and link the objects.

Note: This field is hidden in the formula picker interface, so you have to enter it just as shown. The QLI to OLI relationship can’t be referenced in a formula, and that’s why you can’t just add custom formula reference fields onto the Quote Line Item to look up to their OLI counterparts; you have to use Apex to keep the fields in sync.
Step 2) Create a handler class for QuoteLineItem trigger.
public class QuoteLineItemTriggerHandler
{
    public static void syncQuotes(List newLineItems)
    {
        // get quote ids we need to query for
        Set quoteIds = new Set();
        for (QuoteLineItem qli : newLineItems)
        {
            if (qli.QuoteId != null)
            {
                quoteIds.add(qli.QuoteId);
            }
        }
 
        // Linking quote line item with Opportunity Line Items
        Map<ID,ID> mapQuoteLineItemSortOrder= returnDefaultLinking(quoteIds);
 
        //Fetch opportunity line item for sync
        Map<ID,OpportunityLineItem> mapOppLineItems=new Map<ID,OpportunityLineItem>();
        for(OpportunityLineItem oli:[select id, Renewal__c from OpportunityLineItem where Opportunity.SyncedQuoteId in :quoteIds])
        {
            mapOppLineItems.put(oli.id,oli);
        }
 
        List lstOppotunityToUpdate = new List();
        for (QuoteLineItem qli : newLineItems) {
            OpportunityLineItem oli = mapOppLineItems.get(mapQuoteLineItemSortOrder.get(qli.Id));
            if (oli != null ) {
                oli.Renewal__c=qli.Renewal__c;
                //update more fields....
 
                lstOppotunityToUpdate.add(oli);
            }
        }
        update lstOppotunityToUpdate;
    }
    private static Map<ID,ID> returnDefaultLinking(Set poIds)
    {
        Map<ID,ID> mapSortOrder= new Map<ID,ID>();
        String query='select id, name,(select id, Opportunity_Line_Item_ID__c from QuoteLineItems  ) from Quote where id in :poIds';
        List lstQuotesWithLineItems=Database.query(query);
        for(Quote q: lstQuotesWithLineItems)
        {
            if(q.QuoteLineItems !=null)
            {
                for(QuoteLineitem qli : q.QuoteLineItems)
                {
                    if(qli.Opportunity_Line_Item_ID__c!=null)
                    {
                        //map quote line item id with respective opportunity line item id
                         mapSortOrder.put(qli.Id,ID.valueOF(qli.Opportunity_Line_Item_ID__c));
                    }
                }
            }
        }
        return mapSortOrder;
    }
}
Step 3)  Create the QuoteLineItem Trigger
trigger QuoteLineItemTrigger on QuoteLineItem (after insert, after update) 
{
    QuoteLineItemTriggerHandler.syncQuotes(Trigger.new);
}
 Now, return to your Quote Line Item and make a change, you will see the custom field data is also getting synced to the Opportunity Line Item now:
 Happy Coding!
SFDX Debug

Stream logs to CLI with apex:log:tail

Line Item Content

CPQ Custom Quote Lines

View File Document

Modify CPQ PDF Header and Footer

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Think we can help you? Reach out