function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Paul Kim 24Paul Kim 24 

Clone Opportunity Line Item to Quote Line Items

Hey everyone,

New to APEX so I apologize if my question doesn't have enough information to start to accurately portray my question.

The problem I'm trying to solve is that on Opportunities, we're currently using Products. I'd like for the Opportunity Stage Name of "Closed Pending" to 1. create a Quote and then 2. then take all of the Opportunity Products and clone them onto the Quote as Quote Line Items - keeping in mind that the Products are the same for both the Quote and the Opportunity. Here's my code:

This is to create the Quote at the point of Stage = Closed Pending (isPending is a formula checkbox for when Stage is Closed Pending, so may be redundant?)
trigger CreateQuote on Opportunity (after update) {
    
    List<Opportunity> o = new List <Opportunity>();
    o = [SELECT ID FROM Opportunity WHERE StageName = 'Closed Pending'];
    
    for (Opportunity opp: Trigger.new) {
        if(opp.isPending__c ==true){
        Quote newQuote = new Quote();
        
        newQuote.Name = opp.Name + ' Quote';
        newQuote.OpportunityId = opp.Id;
        
        insert newQuote;
    }
    }   
}
Here's the second trigger where I attempt to clone the Opportunity Products:
trigger QuoteLineItem on Quote (after insert) {
    
    Quote q=trigger.new[0];
    
    Opportunity oppID=[SELECT id FROM Opportunity WHERE id=:q.OpportunityId /*AND StageName = 'Closed Pending'*/];
    
    List<OpportunityLineItem> opplines=[SELECT id, PricebookEntryId, Product2ID, Quantity, UnitPrice, ARR__c 
                                        FROM OpportunityLineItem 
                                        WHERE OpportunityId=:oppID.id];
    
    for(OpportunityLineItem oppline:opplines){
        
        QuoteLineItem qli = new QuoteLineItem();
        qli.QuoteId = q.id; 
        qli.Product2Id = oppline.PriceBookEntry.Product2Id;
        qli.Quantity = oppline.Quantity;
        qli.ARR__c = oppline.ARR__c;
        qli.UnitPrice = oppline.UnitPrice;
        insert qli;

    }

}

Any help would be greatly appreciated!
 
Nayana KNayana K
Deactivate second trigger and use this Opportunity trigger :

trigger CreateQuote on Opportunity (after update) 
{
    Map<Id,Quote> mapOppIdToQuote = new Map<Id,Quote>();
    List<QuoteLineItem> lstQLI = new List<QuoteLineItem>();
	
    for (Opportunity objOpp : Trigger.new) 
	{
        if(objOpp.StageName == 'Closed Pending')
		{
			mapOppIdToQuote.put(objOpp.Id, new Quote(Name = objOpp.Name + ' Quote', OpportunityId = objOpp.Id));
		}
    }
    
	// always DML statement should be outside for loop
	if(!mapOppIdToQuote.values().isEmpty())
		insert mapOppIdToQuote.values();
    
    for(OpportunityLineItem objOLI : [	SELECT Id, PricebookEntryId, PricebookEntry.Product2ID, Quantity, UnitPrice, ARR__c, OpportunityId 
                                        FROM OpportunityLineItem 
                                        WHERE OpportunityId =: mapOppIdToQuote.keySet()])
	{
        if(mapOppIdToQuote.containsKey(objOLI.OpportunityId))
		{
			lstQLI.add(new QuoteLineItem(QuoteId = mapOppIdToQuote.get(objOLI.OpportunityId).Id, Product2Id = objOLI.PricebookEntry.Product2ID,
										Quantity = objOLI.Quantity, ARR__c = objOLI.ARR__c, UnitPrice = objOLI.UnitPrice));
		}
    }
	
	if(!lstQLI.isEmpty())
		insert lstQLI;
}

Please mark this as best if this solution works.
Paul Kim 24Paul Kim 24
Hey Nayana,

Thank you so much for the help! This is immensely helpful for getting me on the right track, especially the use case of Map vs List.

After inserting your code and deactivating the "QuoteLineItem" trigger, when I change the Opportunity Stage to "Closed Pending", the following is the error:
Error:Apex trigger SyncQuoteOppTrigger caused an unexpected exception, contact your administrator: SyncQuoteOppTrigger: execution of AfterUpdate caused by: System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Price Book Entry]: [Price Book Entry]: Trigger.SyncQuoteOppTrigger: line 29, column 1

I can't seem to figure out the required field that I'm missing however.
Nayana KNayana K
if(mapOppIdToQuote.containsKey(objOLI.OpportunityId)) { lstQLI.add(new QuoteLineItem(QuoteId = mapOppIdToQuote.get(objOLI.OpportunityId).Id, Product2Id = objOLI.PricebookEntry.Product2ID, Quantity = objOLI.Quantity, ARR__c = objOLI.ARR__c, UnitPrice = objOLI.UnitPrice, PriceBookEntryId = objOLI.PricebookEntryId)); }