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
DBManagerDBManager 

ENTITY IS LOCKED message when unlocking through approval

I have an approval process for a custom object, Purchase Order, that simply unlocks it after it has been approved. This object is a parent in a master-detail with another object, Purchase Items.

 

The approval process is very simple:

  • If the created date equals NULL, then enter step, else approve
  • Approve action: Unlock Record
  • Approve action: Update Purchase Order to 'Re-Opened' Stage

As an administrator, I can run this approval process without an issue.

 

Other users, however, get the following error message when they try:

There were custom validation error(s) encountered while saving the affected record(s). The first validation error encountered was "Apex trigger PurchaseOrderTrigger caused an unexpected exception, contact your administrator: PurchaseOrderTrigger: execution of AfterUpdate caused by: System.DmlException: Update failed. First exception on row 0 with id a0W200000091UOyEAM; first error: ENTITY_IS_LOCKED, the entity is locked for editing: []: Class.PurchaseItemTriggerMethods.copyStageToStatusFromOrder: line 131, column 1".

 

Here is the trigger reference in the error:

trigger PurchaseOrderTrigger on Purchase_Orders__c (after update) 
{
    PurchaseItemTriggerMethods.copyStageToStatusFromOrder(Trigger.newMap);
}

 

And here is the relevant part of the class reference above:

    public static void copyStageToStatusFromOrder(Map<Id, Purchase_Orders__c> purchOrderMap)
    {
        Map<Id,Purchase_Item__c> purchItemMap = new Map<Id,Purchase_Item__c>([Select Status_Copy__c, Stage__c From Purchase_Item__c Where Purchase_Order__c IN :purchOrderMap.KeySet()]);
        Map<Id,Purchase_Item__c> purchItemMapEmpty = new Map<Id,Purchase_Item__c>(); 
        copyStageToStatusFromItem(purchItemMap.Values(), purchItemMapempty, True);
        Update purchItemMap.Values();
    }

 

What I can't understand is why, if the Purchase Order (and therefore the Purchase Items) are unlocked before the update happens, why the error above is generated?

 

Can anyone help?

 

Thanks in advance.

Best Answer chosen by Admin (Salesforce Developers) 
sfdcfoxsfdcfox
 public static void copyStageToStatusFromOrder(Map<Id, Purchase_Orders__c> purchOrderMap)
    {
        Map<Id,Purchase_Item__c> purchItemMap = new Map<Id,Purchase_Item__c>([Select Status_Copy__c, Stage__c From Purchase_Item__c Where Purchase_Order__c IN :purchOrderMap.KeySet()]);
        Map<Id,Purchase_Item__c> purchItemMapEmpty = new Map<Id,Purchase_Item__c>(); 
        copyStageToStatusFromItem(purchItemMap.Values(), purchItemMapempty, True);
//        Update purchItemMap.Values();
Database.update(purchItemMap.values(),false); }

Try that out, see if it works. The first update will fail normally, but the field update will come back around and re-run the trigger (and your code) the second time around, and all should be well. If that doesn't work, the alternative is a @future method, like so:

 

 @future
 public static void copyStageToStatusFromOrder(Set<Id> purchOrderIds)
    {
Map<Id,Purchase_Item__c> purchItemMap = new Map<Id,Purchase_Item__c>([Select Status_Copy__c, Stage__c From Purchase_Item__c Where Purchase_Order__c IN :purchOrderIds]); Map<Id,Purchase_Item__c> purchItemMapEmpty = new Map<Id,Purchase_Item__c>(); copyStageToStatusFromItem(purchItemMap.Values(), purchItemMapempty, True); Update purchItemMap.Values(); }

Adding the @future annotation means the function is delayed until after the record is out of the current transaction entirely. Note that you have to pass a set of IDs instead of a map, because of the limitations of @future. You may also need to add a recursion flag (see the docs about @future methods for examples) if the trigger could conceivably call itself again.

All Answers

sfdcfoxsfdcfox

The record isn't unlocked until the record is approved. Just because it went from an insert to an update does not mean it is now approved. In fact, Approval Processes aren't processed until the end of the transaction, so it's considered locked until the end of the transaction, when it is updated via the workflow rule processing step. Furthermore, records that enter the approval process will stay locked until they are actually approved or removed from the process. You don't receive the error because administrators are exempt from record locks. You'll have to figure out a different way to achieve your goals.

DBManagerDBManager

Thanks for your response, sfdcfox.

 

What you have said makes sense on it's own. However, there is another approval process which auto-approves the records, providing that a certain user is submitting it. 

 

In that approval process, the record is locked and field updates are applied to it; then it is auto-approved, and more field updates are applied. Finally, on approval, the lock action is repeated.

 

This process can be used by anyone, and yet involves a lock, followed by field updates - why does this work but the other not work?

 

Thanks.

sfdcfoxsfdcfox

See the following link for the documentation on what I'm talking about: http://www.salesforce.com/us/developer/docs/apexcode/Content/apex_triggers_order_of_execution.htm.

 

When you save a record, it goes through a number of steps in a specific order. This order is why your trigger fails while the workflow field update does not.

 

Consider the following:

 

Step 4 is the primary system validation step (e.g. checks sharing, record locks, field level security, and so on).

Step 6 is the after commit trigger step.

Step 9 is the workflow step where workflow rules are evaluated.

Step 10 is the workflow step where updates from a workflow are applied.

Step 11 is where steps 3 and 6 are called with the new data from step 10.

 

The problem, then, is that the trigger will called before the record is actually unlocked.

 

Here's what happens:

 

1) The record is loaded from the database (and the system notes the record is locked).

2) Your record is validated (if coming from the UI).

3) The before commit update trigger is called.

4) The record is validated for system correctness, and cusotm validation rules. Sharing checks and record level locks are checked here. You pass through as long as you're an authorized approver.

5) The record is committed (with a save point) to the database.

6) The record runs through the after commit update trigger. This causes you to go back to step 1 recursively.

6-1) The record is reloaded from the database.

6-2) This step is skipped, it is not a UI update.

6-3) The before commit update trigger is called.

6-4) The record is validated for system correctness. A system administrator can update a locked record, while a normal user cannot.

6-5) The record is recommitted to the database.

6-6) The after commit update trigger is called.

7) Assignment rules are not evaluated here.

8) Auto-response rules are not evaluated here.

9) The workflow approval process is evaluated, and it notes that the record should be unlocked.

10) The new unlocked status is written to the record, and the field update is applied.

11) Before commit update trigger and after commit update triggers are called.

11-3) Nothing is here.

11-6) The trigger copies the status value.

12 through 17) The rest of the processing occurs.

 

As you can see, since the record isn't unlocked until after your DML statement, it fails completely. The solution, of course, is outlined above: Instead of using allOrNone = true, use allOrNone = false. It will fail on the first recursive save, but in step 11-6, it should correctly update the status. It should be a simple matter of changing the one line of code so that it uses Database.update(Sobject[], boolean) instead of calling update SObject[].

 

Also, you should note that step 10 isn't subject to step 4's validation, which is why approval processes (or any workflow field updates) are impervious to ENTITY_IS_LOCKED errors when applying their field updates.

 

Try that change and let me know the outcome.

DBManagerDBManager

Thanks for your detailed reply, sfdcfox.

 

Being a relative newb to apex, I just about managed to follow your reasoning, however, I am not sure how to include your suggested solution in the code that has been written.

 

Here is the trigger that fires during the Approval Process:

trigger PurchaseOrderTrigger on Purchase_Orders__c (after update) 
{
    PurchaseItemTriggerMethods.copyStageToStatusFromOrder(Trigger.newMap);
}

 

Here are the relevant parts of the Apex Class that update the Status field:

    // Copy Stage field to Status_Copy__c -- Note this differs from the Statement of Works.
    public static void copyStageToStatusFromItem(List<Purchase_Item__c> purchItemList, Map<Id,Purchase_Item__c> oldPurchItemMap, Boolean FromOrder)
    {
        for(Purchase_Item__c pi:purchItemList)
        {
            if(FromOrder||trigger.isInsert||(trigger.IsUpdate&&pi.stage__c!=oldPurchItemMap.get(pi.Id).stage__c))
                pi.Status_Copy__c = pi.Stage__c;
        }
    }
    
    public static void copyStageToStatusFromOrder(Map<Id, Purchase_Orders__c> purchOrderMap)
    {
        Map<Id,Purchase_Item__c> purchItemMap = new Map<Id,Purchase_Item__c>([Select Status_Copy__c, Stage__c From Purchase_Item__c Where Purchase_Order__c IN :purchOrderMap.KeySet()]);
        Map<Id,Purchase_Item__c> purchItemMapEmpty = new Map<Id,Purchase_Item__c>(); 
        copyStageToStatusFromItem(purchItemMap.Values(), purchItemMapempty, True);
        Update purchItemMap.Values();
    }

 

If you could indicate how I can implement your suggestion, it would be much appreciated.

 

Many thanks.

sfdcfoxsfdcfox
 public static void copyStageToStatusFromOrder(Map<Id, Purchase_Orders__c> purchOrderMap)
    {
        Map<Id,Purchase_Item__c> purchItemMap = new Map<Id,Purchase_Item__c>([Select Status_Copy__c, Stage__c From Purchase_Item__c Where Purchase_Order__c IN :purchOrderMap.KeySet()]);
        Map<Id,Purchase_Item__c> purchItemMapEmpty = new Map<Id,Purchase_Item__c>(); 
        copyStageToStatusFromItem(purchItemMap.Values(), purchItemMapempty, True);
//        Update purchItemMap.Values();
Database.update(purchItemMap.values(),false); }

Try that out, see if it works. The first update will fail normally, but the field update will come back around and re-run the trigger (and your code) the second time around, and all should be well. If that doesn't work, the alternative is a @future method, like so:

 

 @future
 public static void copyStageToStatusFromOrder(Set<Id> purchOrderIds)
    {
Map<Id,Purchase_Item__c> purchItemMap = new Map<Id,Purchase_Item__c>([Select Status_Copy__c, Stage__c From Purchase_Item__c Where Purchase_Order__c IN :purchOrderIds]); Map<Id,Purchase_Item__c> purchItemMapEmpty = new Map<Id,Purchase_Item__c>(); copyStageToStatusFromItem(purchItemMap.Values(), purchItemMapempty, True); Update purchItemMap.Values(); }

Adding the @future annotation means the function is delayed until after the record is out of the current transaction entirely. Note that you have to pass a set of IDs instead of a map, because of the limitations of @future. You may also need to add a recursion flag (see the docs about @future methods for examples) if the trigger could conceivably call itself again.

This was selected as the best answer
DBManagerDBManager

The first script worked - thanks very much, sfdcfox!