Background:
Quotes in Salesforce represent the proposed prices of your company’s products and services. You create a quote from an opportunity and its products. 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, any change to line items in the quote syncs with products on the opportunity, and vice versa.
But this sync is happening for all standard fields not for custom fields.
Lets see things in real:
I have opportunity record with no product and quotes initially:
Add quote under this opportunity record:
Add some products under this quote (i.e Quote Line Items):
Now when we click start sync button on quote, it populate all products under this quote to the associated opportunity (i.e Opportunity Line Item).
So far so good.
If you change any value on quote line item, it will immediately reflect value on respective opportunity line item.
Problem: Custom fields of opportunity line item and quote line item does not sync. Which means if any value of custom field for quote line item record changes it will not reflect under opportunity line item.
There is no point and click approach where we can map opportunity line items field with quote line item field.
Solution:
Step 1) Create custom formula field on Quote line item (I call it "Opportunity Line Item ID" )
This will give you synced opportunity line item id.
Note: This field is hidden and you will not find it in formula field picker. You have to use as it is OpportunityLineItem.Id. If you click on check syntax, this will give you success message as shown above.
Step 2) Handler class for QuoteLineItem trigger
public class QuoteLineItemTriggerHandler
{
public static void syncQuotes(List<QuoteLineItem> newLineItems)
{
// get quote ids we need to query for
Set<Id> quoteIds = new Set<Id>();
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<OpportunityLineItem> lstOppotunityToUpdate = new List<OpportunityLineItem>();
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<ID> 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<Quote> 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) QuoteLineItem Trigger
trigger QuoteLineItemTrigger on QuoteLineItem ( after update) {
QuoteLineItemTriggerHandler.syncQuotes(Trigger.new);
}
Step 4) Handler for OpportunityLineItem Trigger
public class OpportunityLineItemTriggerHandler{
public static Boolean isTriggerFire = true;
public static void sync(Set<Id> oliIds ){
List<OpportunityLineItem> lstQLIUpdate = new List<OpportunityLineItem>();
for(QuoteLineItem qli: [Select id, Renewal__c, Opportunity_Line_Item_ID__c from QuoteLineItem WHERE pp_dev3__Opportunity_Line_Item_ID__c= :oliIds]){
lstQLIUpdate.add(new OpportunityLineItem(Id=qli.Opportunity_Line_Item_ID__c, Renewal__c=qli.Renewal__c));
}
if(!lstQLIUpdate.isEmpty()){
isTriggerFire = false;
update lstQLIUpdate;
isTriggerFire = true;
}
}
}
Step 5) OpportunityLineItem Trigger
trigger OpportunityLineItemTrigger on OpportunityLineItem (after insert) {
if(Trigger.isInsert && Trigger.isAfter && OpportunityLineItemTriggerHandler.isTriggerFire){
Set<Id> qliIds = Trigger.newMap.keyset();
OpportunityLineItemTriggerHandler.sync(qliIds);
}
}
Custom field data is also get sync now!!