3

I need a mechanism to detect if a record is currently locked by an approval process and it would block the user running the Apex code from modifying the record.

First, some context for where this is a problem.

  1. There is a trigger on Opportunity that will create some master-detail related records when the Opportunity is modified.
  2. The trigger is part of a managed package that is installed in a subscriber org
  3. The subscriber org is using an approval process on the Opportunity that has locked the record.
  4. The subscriber org has their own master-detail custom object related to the Opportunity.
  5. When they modify their related object on the locked Opportunity it causes all the Opportunity triggers to fire (because they have a custom rollup summary field on Opportunity referencing their custom object).
  6. The managed Opportunity trigger is failing with the message:

    ENTITY_IS_LOCKED, This record is locked. If you need to edit it, contact your admin.: []

So, I clearly don't want to try to create my managed related object when the user doing the transaction can't modify the locked record.

But how can I tell if the current user can modify the Opportunity or not?

I can use Approval.isLocked(id) from Winter '16 to check if the record is locked by an approval process, but it doesn't tell me if the current user can still modify it. I checked for a System Admin user who could modify the record and it still returns true.

I guess I could just try the DML operation and handle the exception, but that sounds expensive.

Another option might be to try without sharing on the trigger and just outright ignore the approval lock - Are triggers able to modify records locked by an approval process?

2

Personally, I'd use the Database methods with allOrNone set to false. You can check if the record failed because of this reason (or any general failure):

Opportunity[] records = new Opportunity[0];
// populate records here //
Database.SaveResult[] results = Database.update(records, false);
for(Integer i = 0, s = results.size(); i < s; i++) {
  if(!results[i].isSuccess()) {
    // report an error here //

    // You can tell if it was an approval lock by: //
    for(Database.Error e : results[i].getErrors()) {
      if(e.getStatusCode() == StatusCode.ENTITY_IS_LOCKED) {
        // Record was locked by an approval process
      }
    }
  }
}

Note that this is the behavior you should always be using in your triggers, not just for this specific case. In the general sense, this is just standard bulkification best practices regarding triggers.

Also note that there's no exception happening here--I've reduced it to a normal code flow situation. This method adds barely any time at all and provides a much friendlier user and API experience.

You should not be using "without sharing" in a managed package, unless you want to be failed right off the AppExchange. You should always respect this setting, and the easiest way to do so is to simply use the above technique.

  • That might be an option. The triggers are currently enforcing the sharing rules, so I'd like to keep them like that to avoid issues with being listed on the app exchange. I've got some more complexity to deal with here as the ESAPI library wraps all DML operations to enforce field and object level security. It does support an setArrayOperationMode(OperationMode.BEST_EFFORT) that should work in this scenario. – Daniel Ballinger Jul 2 at 3:32
  • I'd still really like to find out before attempting the DML in the user has access. However, this does work well enough. – Daniel Ballinger Jul 4 at 4:12

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.