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
ColaCola 

Trigger on Task to Update Opportunity and Account

I've created a trigger on a Task to update a custom Opportunity field (Last_Contact_Date__c). The field gets updated based on the most recent task Activity Date. The trigger is seen below and works as desired: 
trigger UpdateOppLastContactDate on Task (after insert, after update) {
  
  Set<String> whatIDs = new Set<String>();
  
    for (Task t : Trigger.new) {
      whatIDs.add(t.whatID);
  }
  
     List<Opportunity> opps = [SELECT Id, Last_Contact_Date__c FROM Opportunity WHERE Id =: whatIDs];
    
     Map<String, Task> taskMap = new Map<String, Task>();
  
    for (Task t : Trigger.new){
      if (!(t.Type.equals('Eloqua Activity'))) {
        if (t.Status.equals('Completed')) {
          taskMap.put(t.whatID, t);
        }
      }
      
  }
         
    for (Opportunity o : opps) {
      if (taskMap.containsKey(o.Id)) {
        o.Last_Contact_Date__c = taskMap.get(o.Id).ActivityDate;
      }
  }
  update opps;
}

However, then I tried to modify this trigger to also update a custom Accounts variable (In_Touch_Date__c) but I cannot get it to function properly. 

Here is the modified code: 

trigger UpdateOppLastContactDate on Task (after insert, after update) {
	
	Set<String> whatIDs = new Set<String>();
	
    for (Task t : Trigger.new) {
  		whatIDs.add(t.whatID);
	}
	
   	List<Opportunity> opps = [SELECT Id, Last_Contact_Date__c FROM Opportunity WHERE Id =: whatIDs];
   	
   	Map<Id, Account> accts = new Map<Id, Account>();
   	   
   	Map<String, Task> taskMap = new Map<String, Task>();

    for (Task t : Trigger.new){
    	if (!(t.Type.equals('Eloqua Activity'))) {
    		if (t.Status.equals('Completed')) {
    			taskMap.put(t.whatID, t);
    		}
    	}
  		
	}
    
    for (Opportunity o : opps) {
  		if (taskMap.containsKey(o.Id)) {
    		o.Last_Contact_Date__c = taskMap.get(o.Id).ActivityDate;
    		Account a = accts.get(o.AccountId);
    		if (a.In_Touch_Date__c != null) {
    			if (taskMap.get(o.Id).ActivityDate.daysBetween(a.In_Touch_Date__c) < 0) {
    				a.In_Touch_Date__c = taskMap.get(o.Id).ActivityDate;
    			}
    		}
  		}
	}
	update opps;
}
I've bolded the updated code, any advice on why this doesn't work is much apprecaited. 
Best Answer chosen by Cola
Always ThinkinAlways Thinkin
Hi Marc,
The exception is getting thrown because the equals() String method does not like null values; that's the Null in NullPointerException, so you have to test for null first before you can even evaluate the Type field. If it's null, you don't want the equals() method to get called at all.

I also noted that the Account__r is unnecessary, we can simply use Account.In_Touch_Date__c

Although it's not a problem, the trigger does a lot of unnecessary processing when the Task is not related to an Opportunity. A few if(opps.size() > 0){} conditions around anything checking the opps List will keep things running efficiently.

I commented the few changes I made below.

trigger UpdateOppLastContactDate on Task (after insert, after update) {
	
	Set<String> whatIDs = new Set<String>();
	
    for (Task t : Trigger.new) {
  		whatIDs.add(t.whatID);
	}
	
   	List<Opportunity> opps = [SELECT Id, 
                              Last_Contact_Date__c, 
                              AccountId, 
                              Account.In_Touch_Date__c //corrected Account__r to Account
                              FROM Opportunity 
                              WHERE Id =: whatIDs];
    
   	List<Account> filteredAccts = new List<Account>(); 
   	Map<Id, Account> accts = new Map<Id, Account>();
   	
   	for(Opportunity o : opps){
		accts.put(o.AccountId, o.Account);//corrected Account__r to Account
	}

   	Map<String, Task> taskMap = new Map<String, Task>();
	
	/* This puts all of the tasks that are not type "Eloqua Activity" and that have 
	*  a status that is "Completed" into a list.
	*/
    for (Task t : Trigger.new){
        if (t.Type != null){//Check for Null first because string.equals() throws exceptions for null values
            if (!(t.Type.equals('Eloqua Activity'))) {
                if (t.Status.equals('Completed')){
                    taskMap.put(t.whatID, t);
                }
            }
        }
	}
    
    /*  This takes all the opportunities that have been created or updated and 
    *	updates their contact date to the most recent Activity Date of the
    *	opportunity. 
    */     
    for (Opportunity o : opps) {
        System.debug('TaskMap Keys' + TaskMap.keySet());
  		if (taskMap.containsKey(o.Id)) {
            System.debug('Due Date from taskMap: ' + taskMap.get(o.Id).ActivityDate);
    		o.Last_Contact_Date__c = taskMap.get(o.Id).ActivityDate;
    		Account a = accts.get(o.AccountId);
    		if (a.In_Touch_Date__c != null) {
    			if (taskMap.get(o.Id).ActivityDate.daysBetween(a.In_Touch_Date__c) < 0) {
    				a.In_Touch_Date__c = taskMap.get(o.Id).ActivityDate;
       			}
    		}
  		}
	}
	update opps;
	update filteredAccts; 
	update accts.values(); 
}

(and I just noticed that Safari doesn't seem to generate that nice Code block! Back to Firefox for Forum work I guess!)

All Answers

Always ThinkinAlways Thinkin
Hi Marc,
There's a couple things missing here. The Account map is not populated with any records, so you will need to add a query to do that, or modify your existing Opportunity query to populate it with the Accounts for the Opportunities you're referencing. After you have the Account records in the map, your for loop should put the update values back into the accts map. And finally, don't forget the all-important update accts DML call.

ColaCola
I'm not sure how to go about populating the Account map with the records, could you be more specific? And yes, the latter two things make sense. 
Always ThinkinAlways Thinkin

I'm winging this while sprint-planning but will check it out later if it doesn't get you further:

add AccountId and Account__r.In_Touch_Date__c into your query and then after declaring your Account map populate it:

for(Opportunity o : opps){
accts.put(o.AccountId, o.Account__r.In_Touch_Date__c);
}

that would allow your accts.get() to acquire the related Account, check its In_Touch_Date__c field value and update it. 

From there you'll need to update the map again: accts.put(o.AccountId, o.Account__r.In_Touch_Date__c);

Finally, you'll need to do the DML update:

update accts;

There are definitely more efficient ways to organize & error-proof all this (and probably a few typos in my psuedo code above), so let me know how far that gets you and I'll check back in a few hours for further improvements.


 

ColaCola
Hi again,

So I've updated the code: 

trigger UpdateOppLastContactDate on Task (after insert, after update) {
	
	Set<String> whatIDs = new Set<String>();
	
    for (Task t : Trigger.new) {
  		whatIDs.add(t.whatID);
	}
	
   	List<Opportunity> opps = [SELECT Id, Last_Contact_Date__c, AccountId, Account__r.In_Touch_Date__c  FROM Opportunity WHERE Id =: whatIDs];
   	
   	Map<Id, Account> accts = new Map<Id, Account>();
   	
   	for(Opportunity o : opps){
		accts.put(o.AccountId, o.Account__r);
	}

   	Map<String, Task> taskMap = new Map<String, Task>();
	
	/* This puts all of the tasks that are not type "Eloqua Activity" and that have 
	*  a status that is "Completed" into a list.
	*/
    for (Task t : Trigger.new){
    	if (!(t.Type.equals('Eloqua Activity'))) {
    		if (t.Status.equals('Completed')) {
    			taskMap.put(t.whatID, t);
    		}
    	}
  		
	}
    
    /*  This takes all the opportunities that have been created or updated and 
    *	updates their contact date to the most recent Activity Date of the
    *	opportunity. 
    */     
    for (Opportunity o : opps) {
  		if (taskMap.containsKey(o.Id)) {
    		o.Last_Contact_Date__c = taskMap.get(o.Id).ActivityDate;
    		Account a = accts.get(o.AccountId);
    		if (a.In_Touch_Date__c != null) {
    			if (taskMap.get(o.Id).ActivityDate.daysBetween(a.In_Touch_Date__c) < 0) {
    				a.In_Touch_Date__c = taskMap.get(o.Id).ActivityDate;
    				accts.put(o.AccountId, a);
    			}
    		}
  		}
	}
	update opps;
	update accts; 
}

But now I receive the following error: 

DML requires SObject or SObject list type: MAP<Id, Account> 

Any suggestions?

Cheers
Always ThinkinAlways Thinkin
Hi Marc,
You can "cast" the Map to a List by using update accts.values(); which will give the update DML the list of sObjects it wants. 

One hazard to consider is that the accts Map has all of the Accounts for all the Opportunities returned by your query, not just the ones that you filtered in line 39. You could create a second Map to hold the ones you actually want to update. You could also just use an Account List, but if you ever need to expand the trigger in the future to handle other updates to Accounts, the Map allows you to do that more easily.

Nice work accounting for my haphazard advice and getting the put() correct with the inclusion of the sObject itself and not just the field.
ColaCola
Okay awesome. So now I have a trigger that will compile and deploy to the server. But then when I actually go and try and update a Task Activity Date for an Opportunity in Sandbox I receive the following error: 

Apex trigger UpdateOppLastContactDate caused an unexpected exception, contact your administrator: UpdateOppLastContactDate: execution of AfterInsert caused by: System.NullPointerException: Attempt to de-reference a null object: Trigger.UpdateOppLastContactDate: line 23, column 1

Here is the updated code: 

trigger UpdateOppLastContactDate on Task (after insert, after update) {
	
	Set<String> whatIDs = new Set<String>();
	
    for (Task t : Trigger.new) {
  		whatIDs.add(t.whatID);
	}
	
   	List<Opportunity> opps = [SELECT Id, Last_Contact_Date__c, AccountId, Account__r.In_Touch_Date__c  FROM Opportunity WHERE Id =: whatIDs];
   	List<Account> filteredAccts = new List<Account>(); 
   	Map<Id, Account> accts = new Map<Id, Account>();
   	
   	for(Opportunity o : opps){
		accts.put(o.AccountId, o.Account__r);
	}

   	Map<String, Task> taskMap = new Map<String, Task>();
	
	/* This puts all of the tasks that are not type "Eloqua Activity" and that have 
	*  a status that is "Completed" into a list.
	*/
    for (Task t : Trigger.new){
    	if (!(t.Type.equals('Eloqua Activity'))) {
    		if (t.Status.equals('Completed')) {
    			taskMap.put(t.whatID, t);
    		}
    	}
  		
	}
    
    /*  This takes all the opportunities that have been created or updated and 
    *	updates their contact date to the most recent Activity Date of the
    *	opportunity. 
    */     
    for (Opportunity o : opps) {
  		if (taskMap.containsKey(o.Id)) {
    		o.Last_Contact_Date__c = taskMap.get(o.Id).ActivityDate;
    		Account a = accts.get(o.AccountId);
    		if (a.In_Touch_Date__c != null) {
    			if (taskMap.get(o.Id).ActivityDate.daysBetween(a.In_Touch_Date__c) < 0) {
    				a.In_Touch_Date__c = taskMap.get(o.Id).ActivityDate;
       			}
    		}
  		}
	}
	update opps;
	update filteredAccts; 
	update accts.values(); 
}

And thanks for all the help! Very helpful. 



Always ThinkinAlways Thinkin
Hi Marc,
The exception is getting thrown because the equals() String method does not like null values; that's the Null in NullPointerException, so you have to test for null first before you can even evaluate the Type field. If it's null, you don't want the equals() method to get called at all.

I also noted that the Account__r is unnecessary, we can simply use Account.In_Touch_Date__c

Although it's not a problem, the trigger does a lot of unnecessary processing when the Task is not related to an Opportunity. A few if(opps.size() > 0){} conditions around anything checking the opps List will keep things running efficiently.

I commented the few changes I made below.

trigger UpdateOppLastContactDate on Task (after insert, after update) {
	
	Set<String> whatIDs = new Set<String>();
	
    for (Task t : Trigger.new) {
  		whatIDs.add(t.whatID);
	}
	
   	List<Opportunity> opps = [SELECT Id, 
                              Last_Contact_Date__c, 
                              AccountId, 
                              Account.In_Touch_Date__c //corrected Account__r to Account
                              FROM Opportunity 
                              WHERE Id =: whatIDs];
    
   	List<Account> filteredAccts = new List<Account>(); 
   	Map<Id, Account> accts = new Map<Id, Account>();
   	
   	for(Opportunity o : opps){
		accts.put(o.AccountId, o.Account);//corrected Account__r to Account
	}

   	Map<String, Task> taskMap = new Map<String, Task>();
	
	/* This puts all of the tasks that are not type "Eloqua Activity" and that have 
	*  a status that is "Completed" into a list.
	*/
    for (Task t : Trigger.new){
        if (t.Type != null){//Check for Null first because string.equals() throws exceptions for null values
            if (!(t.Type.equals('Eloqua Activity'))) {
                if (t.Status.equals('Completed')){
                    taskMap.put(t.whatID, t);
                }
            }
        }
	}
    
    /*  This takes all the opportunities that have been created or updated and 
    *	updates their contact date to the most recent Activity Date of the
    *	opportunity. 
    */     
    for (Opportunity o : opps) {
        System.debug('TaskMap Keys' + TaskMap.keySet());
  		if (taskMap.containsKey(o.Id)) {
            System.debug('Due Date from taskMap: ' + taskMap.get(o.Id).ActivityDate);
    		o.Last_Contact_Date__c = taskMap.get(o.Id).ActivityDate;
    		Account a = accts.get(o.AccountId);
    		if (a.In_Touch_Date__c != null) {
    			if (taskMap.get(o.Id).ActivityDate.daysBetween(a.In_Touch_Date__c) < 0) {
    				a.In_Touch_Date__c = taskMap.get(o.Id).ActivityDate;
       			}
    		}
  		}
	}
	update opps;
	update filteredAccts; 
	update accts.values(); 
}

(and I just noticed that Safari doesn't seem to generate that nice Code block! Back to Firefox for Forum work I guess!)
This was selected as the best answer
ColaCola
Okay so I was able to successfully get the trigger deploy to the server and run but it doesn't seem to do anything. Now, neither the Opportunity's Last_Contact_Date__c or the Account's In_Touch_Date__c are being updated. 

Any ideas? 

Cheers
Always ThinkinAlways Thinkin
When I set up the code in a DE Org, I had to remember to populate the Task.Type field before marking Task.Status Completed to trigger the updates. Other than that I was getting the upstream values on Opp and Acct populating. For the sake of eliminating other conflicting logic in your company Org, you might want to try your Trigger in a DE Org - the 10 minutes of set up time could be a good investment in confirming whether the problem is in your logic or in some other configuration that gets in the way. You could also start adding lots of System.Debugs at each step to confirm that the values you think the logic is receiving are actually correct.
ColaCola
That worked, thanks!