9

Is there a way to make a Validation Rule BYPASS via it's logic when a Trigger is doing it's job?

I've already tried various methods including

  • setting a hidden field on the object in the Trigger (can't because the Validation Rule denies editing of the object, Opportunity, when it's Closed Won)
  • setting a bypass field in the Parent (Account) but everywhere I try, I'm coming across limitations (it's read only, or it simply doesn't work when I know the Val Rule is correct)
  • many variations of these, and more

Situation:

  1. Opps are only editable (at ALL) by three profiles once they are Closed Won (by Validation Rule).

  2. Trigger has to TOUCH groups of Opps (including Closed Wons) at times (just edit/save, that's it).

  3. I can't bypass the Validation Rule based on Profile (but when a Profile causes the TRIGGER to run, I need the TRIGGER to bypass the Validation Rule).

Up until recently, there was not a problem because the Profiles in question would never touch the Opps. But the main Trigger needs (now) to ALSO be run if the ACCOUNT it touched, reading that Account's OPPORTUNITIES and writing data back to the Account. Trying to get this Trigger (below) OR my original (500+ line) trigger to work with this has become a nightmare.

So:

Opportunity Trigger "works fine", and doesn't care about the Validation Rule since the Profiles in question would never edit (they can't) a Closed Won opp. BUT if those Profiles edit an ACCOUNT, we want the Opp Trigger to run on each Opp (including Closed Wons).

Ideally, I wish I could "run the trigger as a different profile", but that apparently is not possible either except in Test Classes.

Account Trigger

trigger PiggybackAccountTrigger on Account (after update) {

    Account[] acctsInTriggerSet = new List<Account>();
    Opportunity[] opptysToUpdate= new List<Opportunity>();

    for(Account a :Trigger.new){  
        acctsInTriggerSet.add(a);
    }

    if( PiggybackTriggerManager.PBOppFromAccountCheck == true ){
        //Do nothing, the calculation is done
    } else {
        For( Opportunity o : [SELECT id, Name
                              FROM Opportunity 
                              WHERE Accountid 
                              IN :acctsInTriggerSet] ) {
            opptysToUpdate.add(o); 
        } 
        update opptysToUpdate;
    }

}

Opportunity Trigger

trigger PiggybackTrigger on Opportunity (after delete, after insert, after update) {


    List<Opportunity> listOpportunity =     new List<Opportunity>();
    List<Id> listOpportunityId =            new List<Id>();
    List<Id> listAccountId =                new List<Id>();

    Map<Id,Opportunity> mapIdOpportunity =  new Map<Id,Opportunity>();
    List<String> listProductNames =         new List<String>();
    PiggybackTriggerManager objManager =    new PiggybackTriggerManager();


// ----------------------------------------------------------------------------------------------------
    if (trigger.isInsert || trigger.isUpdate) {       // INSERTED or UPDATED opp.........
        for(Opportunity thisOpportunity: trigger.new) {
            if (trigger.isUpdate) {
                if (Trigger.oldMap.get(thisOpportunity.Id).Product__c != null) {
                    listProductNames.add(Trigger.oldMap.get(thisOpportunity.Id).Product__c);
                }
            }

            if (thisOpportunity.Product__c != null) {
                listAccountId.add(thisOpportunity.AccountId);
                listProductNames.add(thisOpportunity.Product__c);
            }
        }
    } else { //  DELETED opps.............................
        for(Opportunity thisOpportunity: trigger.old) {
            if (thisOpportunity.Product__c != null) {
                listAccountId.add(thisOpportunity.AccountId);
                listProductNames.add(thisOpportunity.Product__c);
            }
        }
    }
// ----------------------------------------------------------------------------------------------------


    // Prepare Map for Products...
    List<Piggyback_OppToAcct__c> listPiggyback_OppToAcct =  new List<Piggyback_OppToAcct__c>();
    listPiggyback_OppToAcct =                               new List<Piggyback_OppToAcct__c>([
        Select  p.Trackable__c, 
                p.Subscription__c, 
                p.Subscription_Gaps__c, 
                p.Client_Marker_Only__c, 
                p.FIELD_Closed_Won_Owner__c, 
                p.Piggyback_Product_Reference__c, 
                p.CloseWon_Regardless__c,
                p.Name, 
                p.Id    
                    From Piggyback_OppToAcct__c p 
                    where name in :listProductNames
    ]);

    map<Id, User> userMap = new map<Id, User>([
        Select  u.Id, 
                u.Name 
                    From User u
    ]);


    //Get Account and all opportunities related to these Opp's Account...

    map<String, Piggyback_OppToAcct__c> mapStringPiggyback = new map<String, Piggyback_OppToAcct__c> ();

    for(Piggyback_OppToAcct__c curr:listPiggyback_OppToAcct) {
        mapStringPiggyback.put(curr.Name, curr);
    }

    Map<Id, RecordType> mapIdRecordType = new Map<Id, RecordType>([
        Select  r.SobjectType, 
                r.Name, 
                r.Id    
                    From RecordType r 
                    where r.SobjectType = 'Opportunity'
    ]);

    //Get Account and all opportunities related to these Opp's Account...
    // any fields being referenced need to be pulled in here.......................
    Map<Id, Account> 
        mapIdAccount = new  
        Map<Id, Account>([
            Select 
                a.Id, 
                a.pb_Filter_Edu_NewLd_DEC_Other_Latest__c, 
                pb_Filter_Coll_LostOpp_Latest__c,
                pb_Filter_Edu_LostOpp_Latest__c,
                    (Select     Id, 
                                RecordTypeId, 
                                CloseDate,
                                OwnerId, 
                                Product__c, 
                                CreatedDate, 
                                StageName, 
                                Sub_Stage__c, 
                                Purchase_Amount__c, 
                                Charting_Start__c, 
                                Charting_Expiration__c, 
                                Charting_Package__c, 
                                Charting_Charge_Type__c,
                                Summary_Of_Receivables__c,
                                Payment_Plan__c,
                                Promotions_Received__c,
                                Promotions_Received_Used__c
                                    From Opportunities 
                                    where Product__c != null order by createddate ) 
                    From Account a where id in :listAccountId
        ]);

        // NOTE "order by" above... which sorts the Opps into CreateDate order..................
//     (eventually needs updating to CloseDate order with backup of CreateDate possibly.............)



    Map<Id, Account> updateMap = new Map<Id, Account>{};
// previous method...    List<Account> updateList = new List<Account>();

    for(Id thisAccountId: mapIdAccount.keySet()) {    //  Begin For loop of ACCOUNTS.......
        SObject accObj = new Account(Id = thisAccountId);

        //Additional loop to clear out old values....
        for(Piggyback_OppToAcct__c o:listPiggyback_OppToAcct) {
// *=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
//   nulls set to ALL values internally 2012-10-24     accObj = objManager.SetNullFieldsForAccount(accObj, o.Piggyback_Product_Reference__c);
// *=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
        }

        Account objAccount = mapIdAccount.get(thisAccountId);

        List<Opportunity> listCurrOps = new List<Opportunity>();
        listCurrOps = objAccount.Opportunities;

        Map<String, List<Opportunity>> mapString_Opportunity = new Map<String, List<Opportunity>> ();
        mapString_Opportunity = objManager.GetDistinctProductsForAccount(objAccount);



.............................................................................
.... removed various Calculations performed to be "put into" the Account ....
.... removed accObj.put section where data is actually set to Account ....
.............................................................................




            PiggybackTriggerManager.PBOppFromAccountCheck = true;

            if( !updateMap.containsKey((String)accObj.get('Id') ) ) { //check that Acc Id not already added
                updateMap.put((String)accObj.get('Id'), (Account)accObj);

                if(updateMap.size() >= 100) {
                    Database.update( updateMap.values() );
                }
            }

        } // -- end FOR currProd mapString --

    } //  END For loop of ACCOUNTS.......

// catchall for any remaining...
    Database.update( updateMap.values() );

}

EDIT Code for BEFORE TRIGGER written... final fixed version

trigger PiggybackAccountTriggerNoVal on Account (before update) {

    Account[] acctsInTriggerSet2 = new List<Account>();

    for(Account a :Trigger.new){  

        Account oldVal = Trigger.oldMap.get(a.Id);

        if( a.Opp_Closed_Won_Val_Bypass__c == true ) {
              a.Opp_Closed_Won_Val_Bypass2__c = true;
        } else if ( PiggybackTriggerManager.PBOppFromAccountCheck != true ) {
              a.Opp_Closed_Won_Val_Bypass__c = true;
        }

    }

}

/* Two WORKFLOWS work to clear the Bypass fields when needed to make this work */
  • The record is writeable in a before trigger - which is nearly the first thing to run as per the order of execution. It may be an idea to set a hidden field in a before trigger, based on which the validation rule bypasses the record. – techtrekker Apr 4 '13 at 15:12
  • How would I CLEAR the field afterward...? :-/ – AMM Apr 4 '13 at 15:26
  • 1
    You don't need to do an explicit update in a before trigger, just set the flag, the value will flow through to your after trigger. – techtrekker Apr 4 '13 at 16:00
  • 1
    Sadly... it's working FLAWLESSLY in Sandbox, and then deploys and fails (acts as if the hidden checkbox is never checked). I will be testing further tomorrow morning in PRODUCTION when no one is here to see what is missing. I'm not seeing any components "not deployed", so.... thanks for the help!! – AMM Apr 4 '13 at 21:04
  • 1
    End of discussion: I ultimately found that using TWO markers in the Before Trigger works. I'm frustrated because I'm fairly certain it CAN be brought down to ONE, but one fact is.. the Trigger.oldMap item did no good; even Tracking History on the marker field showed it going true-to-false-etc, but it was NOT firing correctly no matter WHAT I did, so using two markers, and some tricky IF-erry, it's working properly. YAY! – AMM Apr 5 '13 at 14:31
12

Use a hierarchical custom setting.

The setting just has two fields.

Location -> Lookup to user/profile
Skip_Validation__c -> Set it to true to skip validation.

Update your validation rules to not fire when Skip validation = true for the custom setting.

&& NOT($Setup.MyCustomSetting__c.Skip_Validation__c)

As the last step, your trigger shuts off validation at the start and enables it at the end.

MyCustomSetting__c tempSkipValidation = 
  MyCustomSetting__c.getInstance();

if(tempSkipValidation == null) {
  tempSkipValidation = new MyCustomSetting__c();
}

tempSkipValidation.Skip_Validation__c = true;
upsert tempSkipValidation;

//WHATEVER TRIGGER NORMALLY DOES HERE

tempSkipValidation.Skip_Validation__c = false;
update tempSkipValidation;
2

Used TWO markers (on the Account [parent record]) to bypass the Validation logic inside a Before Update trigger, after trying various solutions.

trigger PiggybackAccountTriggerNoVal on Account (before update) {

    Account[] acctsInTriggerSet2 = new List<Account>();

    for(Account a :Trigger.new){  

        Account oldVal = Trigger.oldMap.get(a.Id);

        if( a.Opp_Closed_Won_Val_Bypass__c == true ) {
              a.Opp_Closed_Won_Val_Bypass2__c = true;
        } else if ( PiggybackTriggerManager.PBOppFromAccountCheck != true ) {
              a.Opp_Closed_Won_Val_Bypass__c = true;
        }

    }

}
0

In order to accomplish this one mechanism to do so is to add a checkbox field to the object in question, possibly IgnoreValidationRules__c which would allow you a location on the records in the trigger collection to set in the before context. Don't include this field on any page layouts.

Your trigger may look something like this:

trigger Opportunity_BeforeUpdate on Opportunity (before update) {

    // change the field value so that the validation rule is ignored
    for (Opportunity oppy : Trigger.new) {
        oppy.IgnoreValidationRules__c = true;
    }

    // the rest of your code
}

You would alter the validation rules to inspect this field before evaluating the other criteria in the rule.

Your validation rules then would be modified something like this:

AND (
    NOT( IgnoreValidationRules__c ),
    ISPICKVAL(Status, 'Closed - Won')
)

You would then have a workflow rule to change this field back to false after update.

Great blog post from Bob Buzzard on the topic - Bob Buzzard Blog: Bypass Validation Rules from Apex

  • I'm very familiar with bypassing in such manner, but this is not possible due to the Opportunity not being touchable by the Profiles in question, "before" or "after". The alternative which I'm currently investigating from another post I received is to do it in a separate "Before" trigger... but to do it to the PARENT object, setting up the ValRule to read THAT field as the bypass. I'm hopeful it is going to work... ---- the new problem becomes, how do I UNSET the marker on the Parent when "after" triggers have to run first. I have no clue yet. – AMM Apr 4 '13 at 15:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.