While performing regression tests on a Summer'21 sandbox, I noticed that custom fields were not filled in newly created QuoteLineItems.

For many years, I have used the trick as described here, and upon inspection, I found out that OpportunityLineItemId is no longer present in the JSON representation of a new QuoteLineItem.

After some experimenting, I executed this little piece of code in the Summer'21 sandbox:

QuoteLineItem qli1 = new QuoteLineItem(QuoteId = '0Q03M0000004XBUSA2',
                                       Quantity = 1,
                                       HoeveelheidBayreuth__c = 1,
                                       Line_number__c = 1,
                                       UnitPrice = 99.00,
                                       PricebookEntryId = '01u58000005wszNAAQ',
                                       OpportunityLineItemId = '00k3M000003TIOKQA4');
insert qli1;
System.debug('qli1.Id = ' + qli1.Id);
QuoteLineItem qli2 = [SELECT Id, PricebookEntryId, OpportunityLineItemId, OpportunityLineItem.Id FROM QuoteLineItem WHERE Id = :qli1.Id];
System.debug('qli2.Id = ' + qli2.Id);
System.debug('qli2.PricebookEntryId = ' + qli2.PricebookEntryId);
System.debug('qli2.OpportunityLineItemId = ' + qli2.OpportunityLineItemId);
System.debug('qli2.OpportunityLineItem.Id = ' + qli2.OpportunityLineItem.Id);

And, lo and behold, the last two lines give null results. In a Spring'21 sandbox, both last two lines give the Id of the corresponding OpportunityLineItem (of course I used the appropriate hard-coded ids for the sandboxes).

You can also see this behaviour without using Apex: create an Opportunity, an OpportunityLineItem, and a Quote, all via the common user interface. Then inspect the created QuoteLineItem (OK, you have to use something like a SOQL query in Workbench for that). And even though OpportunityLineItemId is one of the QuoteLineItem's fields, it is not filled.

So, for some reason Salesforce has disabled the link between OpportunityLineItems and QuoteLineItems.

Update: sfdcfox's remark got me thinking: previously, the link between OpportunityLineItems and QuoteLineItems was always filled, even when opportunity/quote syncing was disabled. Which could be considered a bit strange. But now with Summer'21, the link is not available anymore when opportunity/quote syncing is disabled. Which makes sense, but it breaks the code that copied custom fields. I do not like to enable opportunity/quote syncing, it has too many disadvantages.

So, what other way is there to have Salesforce copy custom fields from OpportunityLineItems to QuoteLineItems when creating a Quote from an Opportunity?

  • OpportunityLineItemId is a managed field, meaning it is set by the system, used by the Opportunity Quote Sync process. If you check this field in an after insert trigger, it should work as you expect.
    – sfdcfox
    May 11, 2021 at 14:10
  • In a Spring'21 org/sandbox, it is even set if you don't use quote syncing. But in a Summer'21 sandbox, it ain't so anymore. May 11, 2021 at 14:19
  • 1
    I have been checking out about this with our team internally and will share more inputs as I have. Thank you May 19, 2021 at 9:52

3 Answers 3


Here is the trigger that solves the problem for our org. Note: our org has the special property that all Opportunity Line Items have unique ProductCodes within one Opportunity (we have Apex code that guarantees that).

trigger QuoteLineItemTrigger on QuoteLineItem(after insert) {

    // This trigger is necessary because Salesforce does not copy custom fields
    // from OpportunityLineItems to QuoteLineItem when creating Quotes.
    // This trigger only works because OpportunityLineItems within an
    // Opportunity always have different Product codes.

    Set<Id> qliIds = new Set<Id>();
    for (QuoteLineItem qli : Trigger.New) {
    Map<Id, Id> qliId2OppId = new Map<Id, Id>();
    Set<Id> oppIds = new Set<Id>();
    for (QuoteLineItem qli : [SELECT Id, Quote.OpportunityId FROM QuoteLineItem WHERE Id IN :qliIds]) {
        qliId2OppId.put(qli.Id, qli.Quote.OpportunityId);
    Map<String, Id> oppIdAndProdCode2OLIId = new Map<String, Id>();
    Set<Id> oliIds = new Set<Id>();
    for (OpportunityLineItem oli : [SELECT Id, OpportunityId, ProductCode FROM OpportunityLineItem WHERE OpportunityId IN :oppIds]) {
        oppIdAndProdCode2OLIId.put('' + oli.OpportunityId + oli.ProductCode, oli.Id);
    Map<Id, OpportunityLineItem> oliId2OLI = new Map<Id, OpportunityLineItem>(
        [SELECT Id,
         FROM OpportunityLineItem
         WHERE Id IN :oliIds]
    List<QuoteLineItem> qlis2Update = new List<QuoteLineItem>();
    for (QuoteLineItem qli : [SELECT Id, Product2.ProductCode FROM QuoteLineItem WHERE Id IN :qliIds]) {
        Id oppId = qliId2OppId.get(qli.Id);
        String key = '' + oppId + qli.Product2.ProductCode;
        Id oliId = oppIdAndProdCode2OLIId.get(key);
        OpportunityLineItem oli = oliId2OLI.get(oliId);

        if (oli != null) {
            qLi.Custom_Field_1__c = oli.Custom_Field_1__c;

    update qlis2Update;

The scenario where value of OpportunityLineItemId can be set in the apex code without problems, but it looks like it is silently ignored and not saved. While debugging and querying the field separately the value of OpportunityLineItemId is null on the QuoteLineItem Object. This happens only in the Summer '21 Org.

Update: The new functionality was introduced in 232 (Summer'21) release as part of existing Bug fix.

Here is the statement: "We explicitly set OpportunityLineItem as null for all newly created quotelinesItem. This will prevent breaking any existing client implementations. However, as part of Sync, QuotelinesItems can also be created when a new Opportunity line item is created on Opportunity, so in cases when the sync is "true" on Quote then we do not clear the opportunitylineItem on newly create quotelineItem.Even though we pass an Opp. Line item while creating a new Quote Line, we nullify the value on Opp. Line Item."

A doc bug (W-9286341) has been created internally to update the documentation for QuoteLineItem object reference guide. This doc would be updated with this change in Summer'21 release.However, there is no ETA info yet.

  • 1
    Yes, this confirms my suspicions. The statement "This will prevent breaking any existing client implementations." is pretty ironic I think. Each time you change something, you risk you break something. It broke our implementation. Thanks for the official update. May 19, 2021 at 14:11

Simple way: Make your own - OpportunityLineItemId.
Create a formula field in the Opportunity Product(opportunitylineitem) that points to the Id field. Next, create a lookup field in the QuoteLineItem that references the lookup field.

   OppLineId    OppLineId__c    Formula (Text)
   OpportunityProduct   OppLineId__c    Lookup(Opportunity Product)

In your Quote Trigger (or handler class) you can just copy the Id to the quotelineitem and you are done. You now have a pointer to the opportunitylineitem. If you want to copy more fields you can continue as follows:

Use getGlobalDescribe, to obtain all of the fields from the opportunitylineitem and quotelineitem objects, for the copy operation. You'll need to weed out the fields that you cannot/should not copy. As you are copying the fields the new OppLineId field will be copied too. Heres the method that gets the field names/fields.

public static Map<String,Schema.SObjectField> getObjectFields(String objStr){  
   SObjectType objectType = Schema.getGlobalDescribe().get(objStr);
   Map<String,Schema.SObjectField> mfields = 
   return mfields;

I use the above method to copy opportunitylineitem fields to quotelineitem fields when creating quotes in Lightning. For whatever reason Salesforce doesn't copy all of the fields in Lightning.

Limitations: It works on new quotes created after you implement this. It wont work on existing quotes unless you prime the field.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .