How To: Clone a record and delete the original record in Salesforce
April 25, 2018How To: Clone Person Accounts in Salesforce
September 23, 2018How To: Increment a sequence in a text field when cloning a record in Salesforce
Super Clone Pro is a flexible tool that lets you set field values for your cloned records. However, one feature that it doesn’t contain is the ability to set an incremented sequence number in a name or text field when a record is cloned. Super Clone Pro doesn’t contain the feature because it requires querying other existing records to find the next highest sequence number. Depending on the environment, processing to do this could lead to unexpected results or hitting governor limits.
The following code will allow you to set a sequence number. It can be run from either a process builder or trigger, and you can customize the logic to work within your environment’s constraints. Running it from a trigger is more efficient because it can set the field value in the before insert context, and it will not need to run an update on the record. Running the code from a process builder will run a DML update, but it will be easier for administrators to implement.
To use this code with Super Clone Pro, deploy the classes to your production environment. Then add a a call to the setVersionNumber method in the object’s before insert trigger or as a process builder Apex method when the object is created. Be careful not to execute the method on update because it could run recursively depending on how it is implemented. If you need this logic to only run when cloning a record with Super Clone Pro, you will need to set another field on the object as an indicator for the the logic to execute. For example, you could set a hidden checkbox field to checked that indicates the inserted record is coming from Super Clone Pro. You would then need to uncheck to checkbox after the clone logic ran in the trigger or process builder.
We provide no warranty for this code because everyone’s environment is a little different. Use at your own risk.
Class: IncrementSequence
/** * IncrementSequence - add an incremented sequence number to a text field on creation based on other records currently on file. * - Use code at own risk. We do not provide support or warranty for this code. * Call by Trigger: use this in a before insert method by calling the 'setVersionNumber' with true in the setFieldValueOnly. * This will set the sequence number without the need to re-update the record * Call by Process Builder: processes builders run in an after insert/update context. This code will update the records * after setting a sequence number in a field. Please be mindful of update recursion with your environment's other logic. */ public with sharing class IncrementSequence { // limit the number of records returned when trying to find the record with the current greatest sequence number private static final String QUERY_LIMIT = '5000'; // starting and ending characters that the sequence number is between, ex 'test (1)', 'test (2)', etc private static final String SEQ_DELIM_START = '('; private static final String SEQ_DELIM_END = ')'; @InvocableVariable( Label='Field name' Description='API field name of the field that will be assigned a sequence' Required=false) public String FieldName; @InvocableVariable( Label='Field value' Description='Value of the field to be assigned a sequence' Required=false) public String FieldValue; @InvocableVariable( Label='Record Id' Description='Record Id of the object' Required=false) public Id RecordId; @InvocableMethod(Label='Assign a sequence number' Description='Increment a field sequence number') public static void setVersionNumber(list<IncrementSequence> incrementSequenceList) { system.debug('incrementSequenceList: ' + incrementSequenceList); // create a list of sObjects for the method parameters map<String, list<sObject>> objectByFieldMap = new map<String, list<sObject>>(); for (IncrementSequence iseq : incrementSequenceList) { if (String.isBlank(iseq.FieldName) || String.isBlank(iseq.RecordId) || String.isBlank(iseq.FieldValue)) continue; iseq.FieldName = iseq.FieldName.toLowerCase(); list<sObject> objectList = objectByFieldMap.get(iseq.FieldName); if (null == objectList) { objectList = new list<sObject>(); objectByFieldMap.put(iseq.FieldName, objectList); } sObject so = iseq.RecordId.getSobjectType().newSObject(iseq.RecordId); so.put(iseq.FieldName, iseq.fieldvalue); objectList.add(so); } system.debug('objectByFieldMap: ' + objectByFieldMap); for (String fieldName : objectByFieldMap.keySet()) { list<sObject> objectList = objectByFieldMap.get(fieldName); IncrementSequence.setVersionNumber(fieldName, objectList, false); } } /** * set the field value in the sObject with the sequence number */ public static void setVersionNumber(String fieldName, list<sObject> objList, boolean setFieldValueOnly) { map<String, RootSeqCls> rsByRootMap; map<String, Integer> highSeqByRootMap; list<sObject> updateList = new list<sObject>(); // exit if objList is null or empty if (null == objList || objList.isEmpty()) return; // get object type from sObject in the list Schema.sObjectType objType = objList[0].getSObjectType(); // exit if invalid object, field, or missing permissions if (false == isObjectFieldOk(objType, fieldName)) return; // get the subclass map rsByRootMap = getRootSeqMap(fieldName, objList); system.debug('rsByRootMap: ' + rsByRootMap); // get the high sequence map highSeqByRootMap = getHighSequenceMap(objType, fieldName, rsByRootMap); system.debug('highSeqByRootMap: ' + highSeqByRootMap); // set the values on the object for (sObject obj : objList) { String value = String.valueOf(obj.get(fieldName)); if (String.isBlank(value)) continue; RootSeqCls rs = getRootSeqFromValue(value); // retrieve and increment the highest sequence for the root Integer highSeq = highSeqByRootMap.get(rs.root.toLowerCase()); if (null == highSeq) { highSeq = 1; } else { highSeq += 1; } highSeqByRootMap.put(rs.root.toLowerCase(), highSeq); // set the field value on the object field if (true == setFieldValueOnly) { // typically for use in a before trigger obj.put(fieldName, rs.root + ' ' + SEQ_DELIM_START + String.valueOf(highSeq) + SEQ_DELIM_END); } else { // typically for use in an after trigger, process builder flow, or call from custom Apex. Object ID is required. if (null != obj.Id) { sObject newObj = objType.newSObject(obj.Id); newObj.put(fieldName, rs.root + ' ' + SEQ_DELIM_START + String.valueOf(highSeq) + SEQ_DELIM_END); updateList.add(newObj); } } } // update the records if (!updateList.isEmpty()) { update updateList; } } /** * isObjectFieldOk - are the object and field api names ok, and does the running user have access/update permission */ private static Boolean isObjectFieldOk(Schema.SObjectType objType, String fieldName) { // check object type and field are received if (null == objType && String.isBlank(fieldName)) return false; // get the object field set from the object token Schema.DescribeSobjectResult objTypeDesc = objType.getDescribe(); if (null == objTypeDesc) return false; map<String, Schema.SObjectField> fsMap = objTypeDesc.fields.getMap(); if (null == fsMap) return false; // get the field token from the map Schema.SObjectField objFld = fsMap.get(fieldName); if (null == objFld) return false; Schema.DescribeFieldResult objFieldDesc = objFld.getDescribe(); if (null == objFieldDesc) return false; // check object accessible/updateable permission if (!objTypeDesc.isAccessible() || !objTypeDesc.isUpdateable()) return false; // check field accessible/updateable permission if (!objFieldDesc.isAccessible() || !objFieldDesc.isUpdateable()) return false; return true; } /** * getRootSeqMap - return a map of the root sequence subclass by the root value in lowercase */ private static map<String, RootSeqCls> getRootSeqMap(String fieldName, list<sObject> objList) { map<String, RootSeqCls> rsByRootMap = new map<String, RootSeqCls>(); for (sObject obj : objList) { String value = String.valueOf(obj.get(fieldName)); if (String.isBlank(value)) continue; RootSeqCls rs = getRootSeqFromValue(value); rsByRootMap.put(rs.root.toLowerCase(), rs); } return rsByRootMap; } /** * getHighSequenceMap - return a map with the highest sequence found for the root value */ private static map<String, Integer> getHighSequenceMap(Schema.SObjectType objType, String fieldName, map<String, RootSeqCls> rsMap) { set<String> likeSet = new set<String>(); set<String> processedSet = new set<String>(); map<String, Integer> rootSeqMap = new map<String, Integer>(); // build set with values with % for query like selection for (String rt : rsMap.keySet()) likeSet.add(rt+'%'); // form the soql looking for other records with the same name but different version // get the hightest version number of the records returned Integer HighInt = 0; for (sObject so : database.query('SELECT ' + fieldName + ' FROM ' + objType.getDescribe().getName() + ' WHERE ' + fieldName + ' like :likeSet ' + 'LIMIT ' + QUERY_LIMIT)) { String value = String.valueOf(so.get(fieldName)).toLowerCase(); // skip duplicates if (processedSet.contains(value)) continue; processedSet.add(value); // split the field value into root and sequence RootSeqCls rs = getRootSeqFromValue(value); // skip if root isn't in the root set if (!rsMap.containsKey(rs.root)) continue; // save if new root/seq, or save highest sequence Integer highSeq = rootSeqMap.get(rs.root); if (null == highSeq || rs.seq > highSeq) { rootSeqMap.put(rs.root, rs.seq); } } return rootSeqMap; } /** * getRootSeqFromValue - return the name value with out the sequence suffix in (#) */ private static RootSeqCls getRootSeqFromValue(String value) { RootSeqCls rs = new RootSeqCls(); rs.seq = getSequenceFromValue(value); if (rs.seq > 0) { rs.root = value.substring(0, value.lastIndexOf(SEQ_DELIM_START)).trim(); } else { rs.root = value; } return rs; } /** * getVersionFromName - extract the sequence from value. return 0 when not found or formmated incorrectly */ private static Integer getSequenceFromValue(String value) { if (!String.isBlank(value) && value.endsWith(SEQ_DELIM_END)) { integer lastIdx = value.lastIndexOf(SEQ_DELIM_START); if (lastIdx >=0) { String VerStr = value.substring(lastIdx+1, value.length()-1); if (VerStr.isNumeric()) { try { Integer VerInt = Integer.valueOf(VerStr); return VerInt; } catch (exception e) { system.debug('IncrementSequence.getSequenceFromValue error: ' + e); } } } } return 0; } /** * RootSeqCls - subclass to pass the root value separate from the sequence */ private class RootSeqCls { public String root {get; set;} public Integer seq {get; set;} } }
Test Class: IncrementSequenceTest
@isTest public class IncrementSequenceTest { @testSetup static void setup() { // Create common test accounts List<Account> acctList = new List<Account>(); for(Integer i=1;i<=3;i++) acctList.add(new Account(Name = 'TestAcct ('+i+')')); for(Integer i=4;i<=7;i++) acctList.add(new Account(Name = 'AnotherTestAcct ('+i+')')); insert acctList; } // test setting namve field without updating @isTest static void testSequenceAssignment1() { list<Account> accList = new list<Account>(); accList.add(new Account(Name='TestAcct')); accList.add(new Account(Name='TestAcct')); accList.add(new Account(Name='AnotherTestAcct')); accList.add(new Account(Name='AnotherTestAcct')); accList.add(new Account(Name='Origional Name')); IncrementSequence.setVersionNumber('Name', accList, true); system.assertEquals('TestAcct (4)', accList[0].Name); system.assertEquals('TestAcct (5)', accList[1].Name); system.assertEquals('AnotherTestAcct (8)', accList[2].Name); system.assertEquals('AnotherTestAcct (9)', accList[3].Name); system.assertEquals('Origional Name (1)', accList[4].Name); } // test setting name in an update @isTest static void testSequenceAssignment2() { list<Account> accList = new list<Account>(); accList.add(new Account(Name='TestAcct')); accList.add(new Account(Name='TestAcct')); accList.add(new Account(Name='AnotherTestAcct')); accList.add(new Account(Name='AnotherTestAcct')); accList.add(new Account(Name='Origional Name')); insert accList; set<Id> idSet = (new map<Id, Account>(accList)).keySet(); IncrementSequence.setVersionNumber('Name', accList, false); accList = [SELECT Id, Name FROM Account WHERE Id IN :idSet ORDER BY Name]; system.assertEquals('AnotherTestAcct (8)', accList[0].Name); system.assertEquals('AnotherTestAcct (9)', accList[1].Name); system.assertEquals('Origional Name (1)', accList[2].Name); system.assertEquals('TestAcct (4)', accList[3].Name); system.assertEquals('TestAcct (5)', accList[4].Name); } // test process builder interface @isTest static void testSequenceAssignment3() { list<Account> accList = new list<Account>(); accList.add(new Account(Name='TestAcct')); accList.add(new Account(Name='TestAcct')); accList.add(new Account(Name='AnotherTestAcct')); accList.add(new Account(Name='AnotherTestAcct')); accList.add(new Account(Name='Origional Name')); insert accList; set<Id> idSet = (new map<Id, Account>(accList)).keySet(); list<IncrementSequence> isList = new list<IncrementSequence>(); for (Account acc : accList) { IncrementSequence iseq = new IncrementSequence(); iseq.FieldName = 'Name'; iseq.FieldValue = acc.Name; iseq.RecordId = acc.Id; isList.add(iseq); } IncrementSequence.setVersionNumber(isList); accList = [SELECT Id, Name FROM Account WHERE Id IN :idSet ORDER BY Name]; system.assertEquals('AnotherTestAcct (8)', accList[0].Name); system.assertEquals('AnotherTestAcct (9)', accList[1].Name); system.assertEquals('Origional Name (1)', accList[2].Name); system.assertEquals('TestAcct (4)', accList[3].Name); system.assertEquals('TestAcct (5)', accList[4].Name); } }