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
Daniel Jr. TibayanDaniel Jr. Tibayan 

Update a date field when opportunity was closed using trigger

I am trying to update a date/time field (Actual_Closed_Date__c) with the current date when an opportunity was closed (Stage__c = Closed Won || Stage__c = Closed Lost) using a trigger, any suggestions?
Piyush Gautam 6Piyush Gautam 6
Hi Daniel,

Please try below code:
trigger updateOpportunity on Opportunity (before insert, before update) {
    
    for(opportunity opp: trigger.new){
        
        if(opp.StageName=='Closed Won' || opp.StageName=='Closed Lost'){
            
            opp.Actual_Closed_Date__c= system.now();
        }
        
    }

}
If solve your problem, mark as solved. 
Thanks
Meghna Vijay 7Meghna Vijay 7
Hi Daniel,

It can be done via process builder in which in the criteria you can add (ISCHANGED(Stage__c) && Stage__c == Closed Won || Stage__c = Closed Lost) and update the opportunity records.
If you want to write a trigger then you can create a trigger on Opportunity with 'before update' event.

Hope it helps, if it does, mark it as solved.

Thanks
 
Daniel Jr. TibayanDaniel Jr. Tibayan
Hi Piyush,

Thank you so much for your help. I tried your code, looks very straight forward though I encoutnered an error saying "execution of AfterUpdate caused by: System.FinalException: Record is read-only". Should I add "before insert"?

Also, can this be modified so that it only happens if the StageName is originally not closed, meaning it only fires one time (only when it was closed). I can see that this may also be fired when update an already closed opportunity.
trigger OpptyTrigger2 on Opportunity (before update, before delete, after insert, after update) {

        for(opportunity opp: trigger.new){        
            if(opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost'){
                opp.Actual_Closed_Date__c= system.now();
            }            
        }

        TriggerHandler opptyHandler = new OpptyTriggerHandler2();
        opptyHandler.run();
}

 
Daniel Jr. TibayanDaniel Jr. Tibayan
Hi Meghna,

It was originally done using Process Builder, though now we need the value of the actual closed date before any opportunity trigger occurs and triggers run before processes/wokflow rules.
Piyush Gautam 6Piyush Gautam 6
Hi Daniel,

Please have a look on below code.
trigger updateOpportunity on Opportunity (before insert, before update) {
        
    for(opportunity opp: trigger.new){
        
        if(opp.StageName=='Closed Won' || opp.StageName=='Closed Lost'){
            
            if(trigger.oldMap.get(opp.Id).stageName!='Closed Won' && trigger.oldMap.get(opp.Id).stageName!='Closed Lost'){
                opp.Actual_Closed_Date__c= system.now(); // Can't assign a value to a record while using after insert or after update
            }
        }
    }
}

If solve your requirement. Please mark as solved.
Thanks
Daniel Jr. TibayanDaniel Jr. Tibayan
Hi Piyush,

Does removing after insert or after update affect any of the classes that were being called in the trigger? Not quite familiar with triggers & classes yet but I think they were put there on purpose by the other developer.
Piyush Gautam 6Piyush Gautam 6
Hi Daniel,

In that case, please use below code where the additional functionality is working only when the trigger is running before any update( or insert).
trigger updateOpportunity on Opportunity (before insert, before update) {
    
    if(trigger.isBefore){
        if( trigger.isInsert || trigger.isUpdate){
            
                for(opportunity opp: trigger.new){
        
                if(opp.StageName=='Closed Won' || opp.StageName=='Closed Lost'){
                    
                    if(trigger.oldMap.get(opp.Id).stageName!='Closed Won' && trigger.oldMap.get(opp.Id).stageName!='Closed Lost'){
                        opp.Actual_Closed_Date__c= system.now(); // Can't assign a value to a record while using after insert or after update
                    }
                }
            }
        }
    }    

}

If found satisfying, then mark as solved.
Thanks
Akash Pandey 19Akash Pandey 19
Hi Denial,
can you please share the proper code. so I can help you that removing After insert and After update does affect any of the classes that were being called in the trigger.
Thanks & Regards
Akash Pandey

 
Meghna Vijay 7Meghna Vijay 7
Hi Daniel,

Piyush replied with a good response and here is the additional answer to your question:-
Does removing after insert or after update affect any of the classes that were being called in the trigger?  :- No, it won't when you add the condition like if(Trigger.isBefore && (Trigger.isInsert || Trigger.isUpdate)) {// your code}.The previous developer added it but there is no need of after event in your case.

Before Trigger -> When DML is performed on the same object
After Trigger -> When DML is performed on other/related object. 

Thanks
Meghna Vijay 7Meghna Vijay 7
Hi Daniel,

To your reply:-
It was originally done using Process Builder, though now we need the value of the actual closed date before any opportunity trigger occurs and triggers run before processes/wokflow rules.

You are right that it runs before processes/workflow rules but it does the same thing which you are doing via code and salesforce best practice follows OOTB functionality where you don't have to write code.Just my opinion.

Thanks
Daniel Jr. TibayanDaniel Jr. Tibayan
Hi Guys,

The actual reason I want the field to be updated on the trigger is because the class that is being called uses the value of that field on creating/updating a record for another related object.

I added another condition to still be able call the classes that perform task for another object. Did I miss anything here or is there a better (more standard) approach here or I am good to go with it? Should the condition be elseif?
trigger OpptyTrigger2 on Opportunity (before insert, before update, after insert, after update) {
    
    if(trigger.isBefore){
        if( trigger.isInsert || trigger.isUpdate){            
            for(opportunity opp: trigger.new){        
                if(opp.StageName=='Closed Won' || opp.StageName=='Closed Lost'){                    
                    if(trigger.oldMap.get(opp.Id).stageName!='Closed Won' && trigger.oldMap.get(opp.Id).stageName!='Closed Lost'){
                        opp.Actual_Closed_Date__c= system.now(); // Can't assign a value to a record while using after insert or after update
                    }
                }
            }
        }
    }
    
   //To call the calss after insert/update
    if(trigger.isAfter){
        if( trigger.isInsert || trigger.isUpdate){
            TriggerHandler opptyHandler = new OpptyTriggerHandler2();
            opptyHandler.run();
        }
    }
}

 
Daniel Jr. TibayanDaniel Jr. Tibayan
Also, I tried creating an opportunity and made the stage "Closed Won" right away and encountered "execution of BeforeInsert caused by: System.NullPointerException: Attempt to de-reference a null object" maybe because there is no actual close date to be updated yet as the record is just being created at that moment. Is there any workaround in here?
Ajay K DubediAjay K Dubedi
Hi Daniel,
Try this code:
Trigger:
trigger OpportunityTrigger on Opportunity (before Update) {
    if(trigger.IsUpdate && trigger.IsBefore) {
        OpportunityTrigger_handler.checkStage(trigger.new);
    }
}
Trigger Handler:
public class OpportunityTrigger_handler {
    public static void checkStage(List<Opportunity> oppList) {
        try {
            List<Opportunity> oppListToUpdate = new List<Opportunity>();
            for(Opportunity op : oppList) {
                if(op.StageName == 'Closed Won' || op.StageName == 'Closed Lost') {
                    oppListToUpdate.add(op);
                }
            }
            if(oppListToUpdate.size() > 0) {
                for(Opportunity op : oppListToUpdate) {
                    op.Actual_Closed_Date__c = date.today();
                }
            }
        } catch (Exception e) {
            System.debug('Get Exception on line number  --------' +
                         e.getLineNumber() + ' due to following ' +e.getMessage());
        }
    }
}


I hope you find the above solution helpful. If it does, please mark as Best Answer to help others too.

Thanks,
Ajay Dubedi
Piyush Gautam 6Piyush Gautam 6
Hi Daniel,

Use this code:
trigger updateOpportunity on Opportunity (before insert, before update) {
    
    if(trigger.isBefore){
        if( trigger.isInsert || trigger.isUpdate){
            
                for(opportunity opp: trigger.new){
        
                if(opp.StageName=='Closed Won' || opp.StageName=='Closed Lost'){
                    
                    if(trigger.isInsert){
                        opp.Actual_Closed_Date__c= system.now(); 
                    }
                    
                    else if(trigger.oldMap.get(opp.Id).stageName!='Closed Won' && trigger.oldMap.get(opp.Id).stageName!='Closed Lost' && trigger.isUpdate){
                        opp.Actual_Closed_Date__c= system.now(); // Can't assign a value to a record while using after insert or after update
                    }
                        
                }
            }
        }
    }    

}

Thanks
Daniel Jr. TibayanDaniel Jr. Tibayan
Hi Piyush, got the following error "Apex trigger OpptyTrigger2 caused an unexpected exception, contact your administrator: OpptyTrigger2: execution of AfterInsert caused by: System.DmlException: Insert failed. First exception on row 0; first error: DUPLICATE_VALUE, duplicate value found: TvAUnique__c duplicates value on record with id: a0d9D0000004M3u: []: Class.TargetVsActualManager.ProcessTvAForOpportunities: line 54, column 1".

To give you the process here, when a capital(type) opportunity was closed won, a target vs actual(custom object) record will be created/updated with the opportunity amount. All of those function were working. We are revising the code so that instead of closedate field we use actual closed date field to create/update the correct target vs actual record. The error I think is saying that instead of calling the class to update the existing target it is creating a new one which causes a duplcate?
Meghna Vijay 7Meghna Vijay 7
Hi Daniel

The target vs Actual Custom Object has lookup/MD relationship to  Opportunity, I think.
 
trigger OpportunityTrigger on Opportunity( before insert, before update, after insert, after update) {
     
      if(Trigger.isBefore && (Trigger.isInsert || Trigger.isAfter)) {
            OpportunityTriggerHandler.onBeforeInsertOrUpdate(Trigger.new, Trigger.oldMap);
     }else if(Trigger.isAfter && (Trigger.isInsert || Trigger.isAfter)) {
           OpportunityTriggerHandler.onAfterInsertOrUpdate(Trigger.new, Trigger.oldMap);
}
}



public class OpportunityTriggerHandler {
      public static void onBeforeInsertOrUpdate(List<Opportunity> opptyList, Map<Id, Opportunity opptyOldMap>) {
     for(Opportunity opp: opptyList) {
/****** oldMap = null during insert and oldMap != null during update also we want to **//*******perform the operation only when stageName is either 'Closed Won' or 'Closed **//*******Lost'**/
        if((oldMap == null && (opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost')) || (oldMap != null && oldMap.get(opp.Id).StageName != opp.StageName && (opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost')) {
                    opp.Actual_Close_date__c = system.today();  // this will set the actual close //date to today's date 
}
    
}

/**** create/update related object ***/
 public static void onAfterInsertOrUpdate(List<Opportunity> opptyList, Map<Id, Opportunity opptyOldMap>) {
      Map<String, Opportunity> opptyMap = new Map<String, Opportunity>();
     List<Custom Object> customObjectList = new List<Custom Object>();
       for(Opportunity oppty: opptyList) {
          if((oldMap == null && (opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost')) || (oldMap != null && oldMap.get(opp.Id).StageName != opp.StageName && (opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost')) {
         opptyMap.put(oppty.Id, oppty);
}
       }
if(!opptyMap.isEmpty) {
   for(Custom Object CO: [SELECT Id, <your field with which you want to fill with Opportunity Amount> Custom Object WHERE <lookup field name of opportunity>=:opptyMap.keyset() ]) {
   CO.<your field> = opptyMap.get(<lookup field to oppty>).Amount;
   customObjectList.add(CO);
}
}
if(customObjectList.size()>0) {
 upsert customObjectList;
}
}
}

Let me know if it works.

Thanks
Daniel Jr. TibayanDaniel Jr. Tibayan
Hi Meghna,

Thanks for your time, we already have a class for OpptyTriggerHandler2 so I think we don't need to revise that, I think the problem is coming from just the trigger itself calling the classes in either before of after. To make the picture clearer I have here the trigger and classes that were being used.

OpptyTrigger2 --> OpptyTriggerHandler --> TargetVsActualManager
 
/** OpptyTrigger2 **/

trigger OpptyTrigger2 on Opportunity (before update, before delete, after insert, after update) {

    if(Trigger.isBefore && (Trigger.isInsert || Trigger.isUpdate)) {

        for(Opportunity opp: Trigger.new) {
            if((Trigger.oldMap == null && (opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost')) || (Trigger.oldMap != null && Trigger.oldMap.get(opp.Id).StageName != opp.StageName && (opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost'))) {
                opp.Actual_Closed_date__c = system.today();
            }
        }

    }else if(Trigger.isAfter && (Trigger.isInsert || Trigger.isUpdate)) {
    
        TriggerHandler opptyHandler = new OpptyTriggerHandler2();
        opptyHandler.run();
        
    }
}


/** OpptyTriggerHandler2 **/
/** In this class we have the following **/

    protected override void afterInsert() {
       tvaManager.ProcessTvAForOpportunities();
    }
    
    protected override void afterUpdate() {
       tvaManager.ProcessTvAForOpportunities();
    }


/** TargetVsActualManager (tvaManager) **/

public void ProcessTvAForOpportunities() {
        Map<Id, Opportunity> previousOpportunities = (Map<Id, Opportunity>) Trigger.oldMap;
        Map<Id, Opportunity> newOpportunities = (Map<Id, Opportunity>) Trigger.newMap;
        Map<String, Target_vs_Actual__c> tobeinsertedTvA = new Map<String, Target_vs_Actual__c>();
        Map<String, Target_vs_Actual__c> tobeUpdatedTva = new Map<String, Target_vs_Actual__c>();
        Set<Id> tobeIgnoredTvALineItem = new Set<Id>();
        TargetVsActualOpptyHelper.FetchExistingTvaRowsForOpportunity(newOpportunities, tobeUpdatedTva, tobeIgnoredTvALineItem);

        for (Opportunity oppty : newOpportunities.values()) {
            if (tobeIgnoredTvALineItem.contains(oppty.Id)) {
                System.debug('FOUND DUPLICATE OPPORTUNITY IGNORING AND CONTINUING');
                continue;
            }
            if (TargetVsActualOpptyHelper.ShouldUpdateTargetVsActual(oppty, previousOpportunities)) {
                String tvaUnique = oppty.OwnerId + String.valueOf(oppty.Actual_Closed_date__c.month()) + String.valueOf(oppty.Actual_Closed_date__c.year());
                Decimal opptyAmount = (oppty.Amount == null) ? 0 : oppty.Amount;
                if (tobeUpdatedTva != null && tobeUpdatedTva.containsKey(tvaUnique)) {
                    this.AggregateOppAmount(tobeUpdatedTva.get(tvaUnique), opptyAmount);
                } else {
                    Target_vs_Actual__c newTarget = new Target_vs_Actual__c(OwnerId = (oppty.OwnerId),
                            Date__c = Date.valueOf(oppty.Actual_Closed_date__c.year() + '-' + oppty.Actual_Closed_date__c.month() + '-01'),
                            Closed_Call_Actual__c = 0,
                            Closed_Call_Target__c = 0,
                            Opportunity_Actual__c = (oppty.Amount),
                            Opportunity_Target__c = 0,
                            CurrencyIsoCode = oppty.CurrencyIsoCode);
                    if (tobeinsertedTvA != null && tobeinsertedTvA.containsKey(tvaUnique)) {
                        this.AggregateOppAmount(tobeinsertedTvA.get(tvaUnique), opptyAmount);
                    } else {
                        tobeinsertedTvA.put(tvaUnique, newTarget);
                    }
                }

                this.CreateOppLineItem(oppty);
            }
        }

        if (tobeUpdatedTva.size() > 0) {
            update tobeUpdatedTva.values();
        }

        /** ERROR HAPPENS IN HERE **/
        if (tobeinsertedTvA.size() > 0) {
            insert tobeinsertedTvA.values();
        }

        if (newTargetVsActualItems.size() > 0) {
            insert newTargetVsActualItems;
        }
    }