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
Jonathan WallaceJonathan Wallace 

Populate a lookup field using the current user, when an opportunity is closed

Hello, 

new to Triggers, we want to populate a custom lookup field with the current user when an opportunity is closed. 

For example, we have a custom field called closed_by__c which is a lookup to the user object. When a user selects "Closed" from the standard stagename field we want to have the users name populated in the closed_by__c 

I've spent a ton of time looking through the forums but I haven't come across somebody with the same situation. 

any thoughts on how to do this ?
here is what I started with
trigger updateClosedByField on Opportunity (after insert, after update){
for(opportunity opp : trigger.new) 
 
 {
 
    opp.closed_by__c = opp.StageName;
    }
    
    }

 
Best Answer chosen by Jonathan Wallace
Kevin CrossKevin Cross
Jonathan, the below code is meant to ensure that when you update a case that already was closed, it does not change your Closed_By__c and Closed_On__c incorrectly.  In other words, it is a failsafe in case someone goes into a closed record and updates a comment or something of that nature. 
if (Trigger.isUpdate) {
    Opportunity old = Trigger.OldMap.get(opp.Id);
    if (old.IsClosed == opp.IsClosed) {
        closedBy = old.Closed_By__c;
        closedOn = old.LastModifiedDate.date();
    }
}
Therefore, if you change this code to what you suggested, it will end up resetting the Closed_By__c and Closed_On__c instead.  I do not believe you want to do this unless the user's update is to re-open the Opportunity.  Note the code you accepted does that.  If the Opportunity's new status is not one that makes IsClosed true, it will set both fields to NULL.  If you want to make the default CreatedDate, just change the line "Date closedOn;" to "Date closedOn = opp.CreatedDate.date();" as that becomes the default value if Opportunity does not meet the IF condition immediately following that line of code.
trigger ClosedOpportunityTrigger on Opportunity (before insert, before update) {
    for(Opportunity opp : Trigger.New) {
        Id closedBy; // defaults to null
        Date closedOn = opp.CreatedDate.date(); 
        if (opp.IsClosed) {
            // set closed by to last modified user
            closedBy = opp.LastModifiedById;
            closedOn = opp.LastModifiedDate.date();
            // reset closed by to original user if record already closed
            // handles when user changes another field but leaves Opp closed
            if (Trigger.isUpdate) {
                Opportunity old = Trigger.OldMap.get(opp.Id);
                if (old.IsClosed == opp.IsClosed) {
                    closedBy = old.Closed_By__c;
                    closedOn = old.Closed_On__c;
                }
            }   
        } else {
            // do stuff with non-closed Opportunity records
            // e.g., find specific stage and change it
            if (opp.StageName == 'blah') opp.StageName = 'something else';
        }
        // setting these values here handle re-opening Opp
        opp.Closed_By__c = closedBy;
        opp.Closed_On__c = closedOn;
    }
}
I added some more notes to the code to see if it helps drive home my points.


 

All Answers

Kevin CrossKevin Cross
Hello, Jonathan!

Here are some programmatic notes that should help:
  • opp.IsClosed is a boolean flag that should let you know if the updated|inserted Opportunity is Closed.
  • opp.LastModifiedById should reference the User who updated record. In my testing, newly created Opportunity shows modification as well as creation info; however, you always can use CreatedById when the Trigger.isInsert (https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_context_variables.htm) applies.
  • In some other tests, I found that I could update previously closed records.  In those instances, the trigger still fires and your condition of opp.IsClosed would be met.  Therefore, you may want to look into Trigger.isUpdate as well.  When the trigger is an update, you will have both a Trigger.old and a Trigger.new with values as the names suggest.  You would want to ensure your old value was not also IsClosed (i.e., oldOpp.IsClosed != newOpp.IsClosed).
I hope that makes sense.  

As a side note, if you think the logic for handling the custom fields may get more complete, you can create a separate helper class to handle the actual business logic. 

Respectfully yours, Kevin
Jonathan WallaceJonathan Wallace
So this is what i have now, and i throw an error.. did i miss something? 
trigger OpportunityClosedByField on Opportunity (after insert, after update){
for(opportunity opp : trigger.new) 
 
 {

if(opp.IsClosed == true)  
 {
 opp.closed_by__C = opp.LastModifiedbyid;
    }
    else if (opp.isClosed == false)
    {
    opp.closed_by__c = null;
    }
    }
    }

​Error:Apex trigger OpportunityClosedByField caused an unexpected exception, contact your administrator: OpportunityClosedByField: execution of AfterUpdate caused by: System.FinalException: Record is read-only: ()
Kevin CrossKevin Cross
In this instance, you probably want a before insert|update trigger, so you can affect the field before the save.  Otherwise, you would create new Opportunity sObjects and put them into a list with the appropriate Closed_By__c value, then update in bulk.  Regarding the "null" case, is it your intention to reset the Closed_By__c if someone re-opens the Opportunity?  If not, you could eliminate that by only adding an Opportunity into the opportunities-to-update list if the opp.IsClosed condition is met.  In other words, you would not need the else.  Moreover, if an opp does not meet the condition IsClosed, it by definition is not closed so you can use an else instead of an else if for the scenario where you do want to change the value back to null.  
Jonathan WallaceJonathan Wallace
That worked, thanks for your help, one more thing , is there a way to add the close date as well? 
Kevin CrossKevin Cross
Ah, I am glad that helped.  Yes, you would add it the same way.  I was working on a sample code to give you code comments directly, so I will add it to that as an example. 
trigger ClosedOpportunityTrigger on Opportunity (before insert, before update) {
    for(Opportunity opp : Trigger.New) {
        Id closedBy; // defaults to null
        Date closedOn; 
        if (opp.IsClosed) {
            // set closed by to last modified user
            closedBy = opp.LastModifiedById;
            closedOn = opp.LastModifiedDate.date();
            // reset closed by to original user if record already closed
            if (Trigger.isUpdate) {
                Opportunity old = Trigger.OldMap.get(opp.Id);
                if (old.IsClosed == opp.IsClosed) {
                    closedBy = old.Closed_By__c;
                	closedOn = old.LastModifiedDate.date();
                }
            }   
        }
        opp.Closed_By__c = closedBy;
        opp.Closed_On__c = closedOn;
    }
}

Let me know if that helps!
Jonathan WallaceJonathan Wallace
Thanks again Kevin, this helps a ton, and I understand triggers even more. 
Kevin CrossKevin Cross
Jonathan, you are most welcome!  Minor change for you and future readers: I copied in the Closed_On__c code quickly neither changing nor mentioning that you can use the old.Closed_On__c to be consistent with old.Closed_By__c in case the old.LastModifiedDate happened after close. 

Happy coding!

Kevin
Jonathan WallaceJonathan Wallace
Kevin, I had one more question.  If I were to change the stage from being closed, can that be done within this trigger?
Kevin CrossKevin Cross
Just to make sure we are on the same page, under what condition would you change from Closed and to what status?
In any case, you could set opp.StageName = 'your_new_stage_here'.  Just note that only certain stages set the IsClosed flag; therefore, you may have to update the code a little in the rest of the trigger depending on when|why you change the status.
Jonathan WallaceJonathan Wallace
Kevin, i'm was trying to protect against an error, couldn't i use a trigger to see if it's not closed by putting in a   !=  ? 
Jonathan WallaceJonathan Wallace
could i do something like this? 
if (Trigger.isUpdate)               {
                Opportunity old = Trigger.OldMap.get(opp.Id);
               if (old.IsClosed == opp.IsClosed) {
               closedBy= null;
               closedOn= old.CreatedDate.date();
Kevin CrossKevin Cross
Jonathan, the below code is meant to ensure that when you update a case that already was closed, it does not change your Closed_By__c and Closed_On__c incorrectly.  In other words, it is a failsafe in case someone goes into a closed record and updates a comment or something of that nature. 
if (Trigger.isUpdate) {
    Opportunity old = Trigger.OldMap.get(opp.Id);
    if (old.IsClosed == opp.IsClosed) {
        closedBy = old.Closed_By__c;
        closedOn = old.LastModifiedDate.date();
    }
}
Therefore, if you change this code to what you suggested, it will end up resetting the Closed_By__c and Closed_On__c instead.  I do not believe you want to do this unless the user's update is to re-open the Opportunity.  Note the code you accepted does that.  If the Opportunity's new status is not one that makes IsClosed true, it will set both fields to NULL.  If you want to make the default CreatedDate, just change the line "Date closedOn;" to "Date closedOn = opp.CreatedDate.date();" as that becomes the default value if Opportunity does not meet the IF condition immediately following that line of code.
trigger ClosedOpportunityTrigger on Opportunity (before insert, before update) {
    for(Opportunity opp : Trigger.New) {
        Id closedBy; // defaults to null
        Date closedOn = opp.CreatedDate.date(); 
        if (opp.IsClosed) {
            // set closed by to last modified user
            closedBy = opp.LastModifiedById;
            closedOn = opp.LastModifiedDate.date();
            // reset closed by to original user if record already closed
            // handles when user changes another field but leaves Opp closed
            if (Trigger.isUpdate) {
                Opportunity old = Trigger.OldMap.get(opp.Id);
                if (old.IsClosed == opp.IsClosed) {
                    closedBy = old.Closed_By__c;
                    closedOn = old.Closed_On__c;
                }
            }   
        } else {
            // do stuff with non-closed Opportunity records
            // e.g., find specific stage and change it
            if (opp.StageName == 'blah') opp.StageName = 'something else';
        }
        // setting these values here handle re-opening Opp
        opp.Closed_By__c = closedBy;
        opp.Closed_On__c = closedOn;
    }
}
I added some more notes to the code to see if it helps drive home my points.


 
This was selected as the best answer
Jonathan WallaceJonathan Wallace
Kevin, that was a big help. The only thing we were concerned with, is if we had to change the status back to something else,  The Close Date and Closed By info would go back to previous values, I don't need the else statement. I was able to accomplish this without it. Thanks again for your help on this. Triggers are definitely tricky.
Jonathan WallaceJonathan Wallace
Kevin, Can you check this Test Class for the trigger? i'm preparing for deployment and the test class isn't cooperating..
@isTest
private class TestOpportunityTrigger {

    @isTest static void TestChangeOpportunityClosedby() {
        // Test data setup
       Opportunity opp = new Opportunity(Name= 'Opportunity',
                                       StageName='Prospecting',
                                       CloseDate=System.today().addMonths(1));
                                       
        insert opp;
        
        
    }
    
}
Kevin CrossKevin Cross
Remember, you need to close the opportunity to cover all of your trigger code; therefore, you should set some of your test data to Closed Won or Closed Lost.  You also want to try other scenarios you care about as well.  Once you have the full set of tests in place, you should get full coverage because the cases should match the conditions in the trigger: new record that is open, new record with closed as stage, updated record set to closed, updated record already closed, re-opened record, et cetera. 
Ankit Kalsara 14Ankit Kalsara 14
Hi Kevin,

I have similar requirement. I need to populate current user info in custom field when I create a new record. User should be able to see his own name before he saves the record. 

I wrote the trigger and it populates the current user info after I save the record.
I need to pre-populate the current user info when I create the new record.
Below is my trigger code.

trigger populate_current_user on Applications_Time_Tracking__c (before insert) {

    // populate the current user name on record creation 
    for(Applications_Time_Tracking__c tt : Trigger.new){

        if(tt.User__c == null){

            tt.User__c = UserInfo.getUserId();   
        }
    }

Can you please help.