5

Question

Is there something special you need to do before merging a record in apex to avoid an INVALID_FIELD_FOR_INSERT_UPDATE DML exceptions? I'm not actually setting these fields, so why does the system care about them?

System.DmlException: Merge failed. First exception on row 0 with id 00QV0000007HzSvMAK; first error: INVALID_FIELD_FOR_INSERT_UPDATE, Unable to create/update fields: Q_Calc_2__c, IsDeleted, Lead_RFP_Calc__c, Q_Calc_Score__c, Lead_Quote_Score__c, IsConverted, Q_Calc_1__c. Please check the security settings of this field and verify that it is read/write for your profile or permission set.: [Q_Calc_2__c, IsDeleted, Lead_RFP_Calc__c, Q_Calc_Score__c, Lead_Quote_Score__c, IsConverted, Q_Calc_1__c]

Background

I'm writing a test method for some merge related functionality (trigger to prevent deleting leads if not merging). It's complaining that some fields are read only, but I'm a bit lost about why it's caring about those fields in the first place since I'm not setting them.

Code

@isTest
private static void testMergeAllowed() {
    User testUser = TestUtil.generateUser();
    testUser.ProfileId = TestUtil.STANDARD_PROFILE_ID;
    testUser.Allow_Delete_Lead__c = false;

    Lead testLead1 = TestUtil.generateLead();
    Lead testLead2 = TestUtil.generateLead();
    insert new Lead[] { testLead1, testLead2 };

    System.runAs(testUser) {
        try {
            merge testLead1 testLead2;
        } catch (Exception e) {
            System.assert(false, 'exception on merge: ' + e);
        }
    }
}
// Leads
public static Lead createLead() {
    Lead record = generateLead();
    insert record;
    return record;
}
public static Lead generateLead() {
    Lead record = (Lead) Lead.SObjectType.newSObject(null, true);
    record.LastName = generateRandomString(16);
    record.Company = generateRandomString(16);
    return record;
}
3
  • If you look at the example on the merge page - developer.salesforce.com/docs/atlas.en-us.apexcode.meta/… - you can see that they explicitly select standard fields (Id, Name) from the inserted Accounts before merging. Have you tried that? I am pretty sure it has to do with updatable fields that you noted in your answer.
    – zgc7009
    Commented Apr 19, 2017 at 14:33
  • @zgc7009 definitely, i was able to get things working by cloning with just the fields i wanted Commented Apr 20, 2017 at 6:29
  • @zgc7009 just confused since i can't seem to find any docs as to how which fields are in memory for the sobject when they're passed to the merge statement affect the merged record Commented Apr 20, 2017 at 6:30

2 Answers 2

8

I'm not sure why, but the merge statement appears to be sensitive to the fields that are in memory for an sObject when passed to the merge statement. The operation fails if any of the fields are not updateable for the current user (e.g. audit fields, formula fields, field level security settings).

The above code is running into issues since the .newSObject(null, true) method populates default values for a lot of fields that must not be visible to the standard user in your test method. For the purposes of the test method you can just create a clone with just the id to perform the merge.

merge (new Lead(Id = testLead1.Id)) (new Lead(Id = testLead2.Id));

I haven't tested how the field values in memory affect the resulting merge, so you'll want to do some testing before adjusting for merge statements you're doing.

3

Props to @ralphcallaway as his answer saved me some time. Here's an expanded answer re: fields in memory and their impact on merge verb

Imagine you have:

Contact winner;
Contact loser;
makeMergeReady(winner,loser);
merge winner loser;

and a method that ensures that winner has valuable data from loser that needs to be preserved

void makeMergeReady(Contact winner, Contact loser) {
  if (loser.Foo__c > winner.Foo__c) {
    winner.Foo__c = loser.Foo__c;  // loser Foo__c dominates!
  }
}

As Ralph points out, if winner has any formula or audit/auto-number fields in memory when the merge winner loser; is executed, you get the error in the OP

More likely than not, you may not want to muck around with the SOQL that populated the winner so, a simple solution is to enhance the makeMergeReady method as follows:

Contact makeMergeReady(Contact winner, Contact loser) {
  if (loser.Foo__c > winner.Foo__c) {
      winner.Foo__c = loser.Foo__c;  // loser Foo__c dominates!
  }
  ... other dominance logic ...

  // Prepare winner to be merge verb ready...
  Contact mergeVerbReady = new Contact(Id = winner.Id);
  Map<String, Object> winnerFieldsByFieldName = 
    winner.getPopulatedFieldsAsMap(); // stuff in memory (via SOQL or assignments)
  Map<String, SObjectfield> CONTACTFLDTOKENSBYFLDNAME = Schema.SobjectType.Contact.fields.getMap();
  for (String fieldName: winnerFieldsByFieldName.keySet()) {
     SObjectField fieldToken = CONTACTFLDTOKENSBYFLDNAME.get(fieldName);
     if (fieldToken == null ||  // discard relationship fields
        !fieldToken.getDescribe().isUpdateable()) {continue;}   // discard non-updateable fields 
     mergeVerbReady.put(fieldToken,winner.get(fieldToken)); // retain!
  }
  return mergeVerbReady;
}

and then change your calling method to

Contact winner;
Contact loser;
merge makeMergeReady(winner,loser) loser;

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .