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
Traveller MadeTraveller Made 

How to insert a quote and a quoteLineItem when an opportunity is cloned with products ?

I would like to create a trigger which can insert a quote and a quotelineitem when I clone an opportunity with its product. I tried many Trigger.
If someone can help me understand the problem it would be awesome! I spent already a few days on it...

trigger FacturationCreator on Opportunity (after insert, after update) {
for (Opportunity o : Trigger.new) {
if (o.Type == 'Event') {

//Opportunity oppId=[select id from Opportunity where id=:q.OpportunityId];
List<OpportunityLineItem> opplines=[select id, quantity, PriceBookEntry.Product2Id, UnitPrice, PricebookentryId
from OpportunityLineItem where OpportunityId=:o.id];
 for(OpportunityLineItem oppline:opplines){ 
List <Quote> factList = new List<Quote>();

Quote q = new Quote();
q.name = 'Quote-' + o.name;
q.Montant_1_re_ch_ance__c = 1000;
q.Date_de_Facture__c = o.CloseDate;
q.Date_1_re_ch_ance__c = o.CloseDate;
q.opportunityId = o.id;
factList.add(q);
insert q;


List<Quote> qList=[select id from Quote where opportunityId=:o.id];
for(Quote q:qList){ 
QuoteLineItem qli = new QuoteLineItem();
qli.quoteId = q.Id;
qli.UnitPrice = oppline.UnitPrice;
qli.Product2Id = oppline.PriceBookEntry.Product2Id;
qli.Quantity = oppline.Quantity;
qli.PriceBookentryid = oppline.PriceBookentryId;
insert qli;}

}
        }
    }
}
Best Answer chosen by Traveller Made
bretondevbretondev
Trigger :
trigger FacturationCreator on Opportunity (after insert) {

		List<Id> oppIds = new List<Id>();
		for (Opportunity o : Trigger.new)
			oppIds.add(o.id);
			
		CreateQuote.createQuote(oppIds);	

}

Apex class :
 
public class CreateQuote {

	@future
	public static void createQuote(List<Id> oppIds) {
	
		List<Quote> lstQ = new List<Quote>();

		//Fetching all the OLIs belonging to the Opportunities of Trigger.new
		List<OpportunityLineItem> olis =[select id, OpportunityId, quantity, PriceBookEntry.Product2Id, UnitPrice, PricebookentryId from OpportunityLineItem where OpportunityId in :oppIds];
		
		
		//Preparing a Map of each Opportunity and their corrresponding LineItems  ( Id of the opportunity => List of the OLIs that belong to the opportunity)
		Map<Id,List<OpportunityLineItem>> mapOppIdOli = new Map<Id,List<OpportunityLineItem>>();
		for (OpportunityLineItem oli : olis) {
			if (mapOppIdOli.containsKey(oli.OpportunityId) {
				mapOppIdOli.get(oli.OpportunityId).add(oli);
			} else {
				List<OpportunityLineItem> lstOlis = new List<OpportunityLineItem>();
				lstOlis.add(oli);
				mapOppIdOli.put(oli.OpportunityId, lstOlis);
			}
		}
		
		
		//Preparing the new quotes - one for each Opportunity
		List<Opportunity> opps =[select id, name, CloseDate from Opportunity where Id in :oppIds];
		for (Opportunity o : opps) {
			if (o.Type == 'Event') {
				Quote q new Quote();
				q.name = 'Quote-' + o.name;
				q.Montant_1_re_ch_ance__c = 1000;
				q.Date_de_Facture__c = o.CloseDate;
				q.Date_1_re_ch_ance__c = o.CloseDate;
				q.opportunityId = o.id;
				lstQ.add(q);
			}	
		}

		
		//Inserting the new quotes
		insert lstQ;
		
		
		//Preparing the new QLIs - one for each OLI
		List<QuoteLineItem> lstQLI = new List<QuoteLineItem>();
		for (Quote q : lstQ) {
			List<OpportunityLineItem> lstOlis = mapOppIdOli.get(q.OpportunityId);
			if (lstOlis != null) {
				for (OpportunityLineItem oli : lstOlis) {
					QuoteLineItem qli = new QuoteLineItem();
					qli.quoteId = q.Id;
					qli.UnitPrice = oli.UnitPrice;
					qli.Product2Id = oli.PriceBookEntry.Product2Id;
					qli.Quantity = oli.Quantity;
					qli.PriceBookentryid = oli.PriceBookentryId;
					lstQLI.add(qli);
				}
			}
		}

		
		//Inserting the new QLIs
		insert lstQLI;

	}


}


 

All Answers

bretondevbretondev
Why do you activate your trigger for after update.
IMHO it should be only for after insert, because if you clone an opportunity you only insert records.

This is what I would do.
This code might not be 100% perfect, but close to it though.
 
trigger FacturationCreator on Opportunity (after insert) {


	List<Quote> lstQ = new List<Quote>();
	List<QuoteLineItem> lstQ = new List<QuoteLineItem>();
	
	
	//Fetching all the OLIs belonging to the Opportunities of Trigger.new
	List<Id> oppIds = new List<Id>();
	for (Opportunity o : Trigger.new)
		oppIds.add(o.id);
	List<OpportunityLineItem> olis =[select id, OpportunityId, quantity, PriceBookEntry.Product2Id, UnitPrice, PricebookentryId from OpportunityLineItem where OpportunityId in :oppIds];
	
	
	//Preparing a Map of each Opportunity and their corrresponding LineItems  ( Id of the opportunity => List of the OLIs that belong to the opportunity)
	Map<Id,List<OpportunityLineItem>> mapOppIdOli = new Map<Id,List<OpportunityLineItem>>();
	for (OpportunityLineItem oli : olis) {
		if (mapOppIdOli.containsKey(oli.OpportunityId) {
			mapOppIdOli.get(oli.OpportunityId).add(oli);
		} else {
			List<OpportunityLineItem> lstOlis = new List<OpportunityLineItem>();
			lstOlis.add(oli);
			mapOppIdOli.put(oli.OpportunityId, lstOlis);
		}
	}
	
	
	//Preparing the new quotes - one for each Opportunity
	for (Opportunity o : Trigger.new) {
		if (o.Type == 'Event') {
			Quote q new Quote();
			q.name = 'Quote-' + o.name;
			q.Montant_1_re_ch_ance__c = 1000;
			q.Date_de_Facture__c = o.CloseDate;
			q.Date_1_re_ch_ance__c = o.CloseDate;
			q.opportunityId = o.id;
			lstQ.add(q);
		}	
	}

	
	//Inserting the new quotes
	insert lstQ;
	
	
	//Preparing the new QLIs - one for each OLI
	List<QuoteLineItem> lstQLI = new List<QuoteLineItem>();
	for (Quote q : lstQ) {
		List<OpportunityLineItem> lstOlis = mapOppIdOli.get(q.OpportunityId);
		for (OpportunityLineItem oli : lstOlis) {
			QuoteLineItem qli = new QuoteLineItem();
			qli.quoteId = q.Id;
			qli.UnitPrice = oli.UnitPrice;
			qli.Product2Id = oli.PriceBookEntry.Product2Id;
			qli.Quantity = oli.Quantity;
			qli.PriceBookentryid = oli.PriceBookentryId;
			lstQLI.add(qli);
		}
	}

	
	//Inserting the new QLIs
	insert lstQLI;
				
}

 
bretondevbretondev
Actually line 5 can be cleared
Traveller MadeTraveller Made
Thank you a lot!
Actually, the 'after update' was a mistake and a consequence of many tries. Indeed, it shouldn't be there.

I tried what you posted which seems quite perfect, but unfortunately something is wrong... 

User-added image
bretondevbretondev
OK, so here we have a NullPointerExcpetion in line 50, which means lstOlis is null.
It is null because at least one opportunity does not have any OLIs.

I did not expect this situation. So here we just have to create QLIs only if there is at least one OLI (which means we just have to check that lstOlis is not null).

That would result the following :
 
//Preparing the new QLIs - one for each OLI
	List<QuoteLineItem> lstQLI = new List<QuoteLineItem>();
	for (Quote q : lstQ) {
		List<OpportunityLineItem> lstOlis = mapOppIdOli.get(q.OpportunityId);
             if (lstOlis != null) {
		for (OpportunityLineItem oli : lstOlis) {
			QuoteLineItem qli = new QuoteLineItem();
			qli.quoteId = q.Id;
			qli.UnitPrice = oli.UnitPrice;
			qli.Product2Id = oli.PriceBookEntry.Product2Id;
			qli.Quantity = oli.Quantity;
			qli.PriceBookentryid = oli.PriceBookentryId;
			lstQLI.add(qli);
		}
}
	}

 
Traveller MadeTraveller Made
Yes that is it ! So it now does work but it doesn't create the quotelineitem, just the quote...
bretondevbretondev
When you clone your Opportunity, do you clone it with its Products?
If you don't, it's not gonna work
Traveller MadeTraveller Made
Yes I do! The OpportunityLineItem is cloned too, the quote is created like I want, but no QuoteLineItem. 
bretondevbretondev
Do some debugging.
At line 13 put :
 
System.debug('### olis size IS : ' + olis.size());


And check in the Logs in the Developer Console.
Size should not be 0.
Traveller MadeTraveller Made
I have never done that before, that is clearly my limit. But maybe it is that ... ? 

User-added image
bretondevbretondev
yes
keep this window open
then clone a new opportunity
come back to this window and a new log will have appeared
double-click on the log
then search for ###
Traveller MadeTraveller Made
Ok, so it isn't a good news I guess ? 

User-added image
bretondevbretondev
No it's not.
Do the same for oppIds
System.debug('### oppIds contains : ' + oppIds);

This list should contain at least one id, the id of the new Opportunity
Traveller MadeTraveller Made
So good news this time ! 

User-added image
Traveller MadeTraveller Made
The problem should be in " List<OpportunityLineItem> olis =[select id, OpportunityId, quantity, PriceBookEntry.Product2Id, UnitPrice, PricebookentryId from OpportunityLineItem where OpportunityId=:oppIds];" right ?
bretondevbretondev
Yes.
Now I understand what is going on.
Your trigger is executed just after the new Opportunity is created, but just before the OLIs are created, so it doesn'find them.
My idea is that you will have to move all your code to a @future block so that it gets executed in the future.
By that time the OLIs should be available.
Traveller MadeTraveller Made
Oh ok I understand! You are awesome ! How can I do that ? 
bretondevbretondev
Trigger :
trigger FacturationCreator on Opportunity (after insert) {

		List<Id> oppIds = new List<Id>();
		for (Opportunity o : Trigger.new)
			oppIds.add(o.id);
			
		CreateQuote.createQuote(oppIds);	

}

Apex class :
 
public class CreateQuote {

	@future
	public static void createQuote(List<Id> oppIds) {
	
		List<Quote> lstQ = new List<Quote>();

		//Fetching all the OLIs belonging to the Opportunities of Trigger.new
		List<OpportunityLineItem> olis =[select id, OpportunityId, quantity, PriceBookEntry.Product2Id, UnitPrice, PricebookentryId from OpportunityLineItem where OpportunityId in :oppIds];
		
		
		//Preparing a Map of each Opportunity and their corrresponding LineItems  ( Id of the opportunity => List of the OLIs that belong to the opportunity)
		Map<Id,List<OpportunityLineItem>> mapOppIdOli = new Map<Id,List<OpportunityLineItem>>();
		for (OpportunityLineItem oli : olis) {
			if (mapOppIdOli.containsKey(oli.OpportunityId) {
				mapOppIdOli.get(oli.OpportunityId).add(oli);
			} else {
				List<OpportunityLineItem> lstOlis = new List<OpportunityLineItem>();
				lstOlis.add(oli);
				mapOppIdOli.put(oli.OpportunityId, lstOlis);
			}
		}
		
		
		//Preparing the new quotes - one for each Opportunity
		List<Opportunity> opps =[select id, name, CloseDate from Opportunity where Id in :oppIds];
		for (Opportunity o : opps) {
			if (o.Type == 'Event') {
				Quote q new Quote();
				q.name = 'Quote-' + o.name;
				q.Montant_1_re_ch_ance__c = 1000;
				q.Date_de_Facture__c = o.CloseDate;
				q.Date_1_re_ch_ance__c = o.CloseDate;
				q.opportunityId = o.id;
				lstQ.add(q);
			}	
		}

		
		//Inserting the new quotes
		insert lstQ;
		
		
		//Preparing the new QLIs - one for each OLI
		List<QuoteLineItem> lstQLI = new List<QuoteLineItem>();
		for (Quote q : lstQ) {
			List<OpportunityLineItem> lstOlis = mapOppIdOli.get(q.OpportunityId);
			if (lstOlis != null) {
				for (OpportunityLineItem oli : lstOlis) {
					QuoteLineItem qli = new QuoteLineItem();
					qli.quoteId = q.Id;
					qli.UnitPrice = oli.UnitPrice;
					qli.Product2Id = oli.PriceBookEntry.Product2Id;
					qli.Quantity = oli.Quantity;
					qli.PriceBookentryid = oli.PriceBookentryId;
					lstQLI.add(qli);
				}
			}
		}

		
		//Inserting the new QLIs
		insert lstQLI;

	}


}


 
This was selected as the best answer
Traveller MadeTraveller Made
Ok and when did the quote is supposed to be created among with its QuoteLineItem ? I tried but so far no quote here
bretondevbretondev
@future is asynchronous
You might have to wait a few seconds to see the quote and the QLI
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation_future.htm
Traveller MadeTraveller Made
I did wait but nothing happened... That is really weird ! 
bretondevbretondev
Redo the operation while keeping the Developer Console open.
Few seconds after you clone the Opportunity, a new log called FutureHandler or something like that shouls appear in the logs.
At that time Quote should be created.
If not, do some debugging.
Traveller MadeTraveller Made
So it seems like there is a problem 
User-added image
bretondevbretondev
Declare the Type field when fetching the opps :
List<Opportunity> opps =[select id, name, CloseDate, Type from Opportunity where Id in :oppIds];

 
Traveller MadeTraveller Made
Thank you very much but the big problem is the Price Book Entry, and with that you cannot help I think...User-added image
bretondevbretondev
I don't know if I can help.
I have never seen this message before.
Did you google the error message?
Many people have had the same issue as you.
At what line number does the issue appear? You didn't post the full stack trace of the message.
In the log you should see the line number.

In my opinion, it looks like the issue appears when you insert the QLI.
And for the QLI whe choose the same Pricebook Entry as the OLI :
qli.PriceBookentryid = oli.PriceBookentryId;

I think the PriceBookENtry specified here does not belong to Pricebook that is set at the level of Quote, hence the error.

So maybe you can try putting by default the same Pricebook for the Quote as the one that was assigned to the Opportunity.
Which means at line 34 you would add:
q.Pricebook2Id= o.Pricebook2Id;
Traveller MadeTraveller Made
THANK YOU VERY VERY MUCH ! IT DID WORK !!!!!!! YOU ARE AMAZING !