From a33604b4984c811a81d31116c9e722b71fffab82 Mon Sep 17 00:00:00 2001 From: Christopher Gary Date: Wed, 14 Apr 2021 07:34:56 -0400 Subject: [PATCH] Additional feature to pre-determine children list to clone --- .forceignore | 6 + .../classes/OpportunityCloneService.cls | 37 +++++- .../classes/OpportunityCloneServiceTest.cls | 120 ++++++++++++++++++ .../OpportunityCloneServiceTest.cls-meta.xml | 5 + .../main/default/classes/TestCloneUtility.cls | 39 ++++++ .../classes/TestCloneUtility.cls-meta.xml | 5 + .../labels/CustomLabels.labels-meta.xml | 11 ++ .../opportunityCustomClone.html | 21 +-- .../opportunityCustomClone.js | 28 +++- manifest/package.xml | 32 ++--- 10 files changed, 267 insertions(+), 37 deletions(-) create mode 100644 force-app/main/default/classes/OpportunityCloneServiceTest.cls create mode 100644 force-app/main/default/classes/OpportunityCloneServiceTest.cls-meta.xml create mode 100644 force-app/main/default/classes/TestCloneUtility.cls create mode 100644 force-app/main/default/classes/TestCloneUtility.cls-meta.xml create mode 100644 force-app/main/default/labels/CustomLabels.labels-meta.xml diff --git a/.forceignore b/.forceignore index c27d65b..e81d1f8 100644 --- a/.forceignore +++ b/.forceignore @@ -20,5 +20,11 @@ package.xml # Exclude FlexiPage *.flexipage +# Exclude Listviews +*.listView + +# Exclude Settings +*.setting + # Exclude FieldSets *.fieldSet \ No newline at end of file diff --git a/force-app/main/default/classes/OpportunityCloneService.cls b/force-app/main/default/classes/OpportunityCloneService.cls index ae32bf4..73c4201 100644 --- a/force-app/main/default/classes/OpportunityCloneService.cls +++ b/force-app/main/default/classes/OpportunityCloneService.cls @@ -5,6 +5,7 @@ * @version 1.0 * @group Opportunity Cloning * @history 2021-04-02 - CJG - Created + * 2021-04-12 - CJG - Added setting to restrict child records shown to a particular List */ public with sharing class OpportunityCloneService { /** @@ -23,9 +24,22 @@ public with sharing class OpportunityCloneService { List childRelationshipList = Schema.Opportunity.getSObjectType().getDescribe().getChildRelationships(); opportunityChildrenMap = new Map(); + List restrictedRelationshipList = new List(); + String restrictedChildList = Label.SmartCloneChildList; + if(String.isNotBlank(restrictedChildList)){ + restrictedRelationshipList = restrictedChildList.split(';'); + } for(Schema.ChildRelationship childRel:childRelationshipList) { - Schema.DescribeSObjectResult childObjRes = childRel.getChildSObject().getDescribe(); - opportunityChildrenMap.put(childObjRes.getName(),childObjRes); + if(String.isNotBlank(childRel.getRelationshipName())){ + Schema.DescribeSObjectResult childObjRes = childRel.getChildSObject().getDescribe(); + if(!restrictedRelationshipList.isEmpty()){ + if(restrictedRelationshipList.contains(childObjRes.getName())){ + opportunityChildrenMap.put(childRel.getRelationshipName(),childObjRes); + } + } else { + opportunityChildrenMap.put(childRel.getRelationshipName(),childObjRes); + } + } } } /** @@ -73,6 +87,7 @@ public with sharing class OpportunityCloneService { List childRelationshipsToClone) { //dynamic query in order to find the original opportunity. String originalOppQueryString = ObtainQueryStringForOppAndChildren(oppId, childRelationshipsToClone); + System.debug(LoggingLevel.INFO,'\n\n*****\nQuery: ' + originalOppQueryString + '\n*****\n\n'); List oppList = Database.query(originalOppQueryString); //if the original opp cannot be found, throw and exception if(oppList.isEmpty()){ @@ -125,7 +140,11 @@ public with sharing class OpportunityCloneService { childQueryString += relationship + ')'; subqueryList.add(childQueryString); } - queryString += ', ' + String.join(subqueryList,',') + 'FROM Opportunity WHERE Id =\'' + oppRecordId + '\''; + if(!subqueryList.isEmpty()){ + queryString += ', ' + String.join(subqueryList,',') + 'FROM Opportunity WHERE Id =\'' + oppRecordId + '\''; + } else { + queryString += ' FROM Opportunity WHERE Id =\'' + oppRecordId + '\''; + } return queryString; } @@ -147,6 +166,10 @@ public with sharing class OpportunityCloneService { fieldNameList.add(fieldTokenDescribe.getName()); } } + //need to account for weird objects where not all of the fields come back + if(objectName.toUpperCase() == 'OPPORTUNITYLINEITEM'){ + fieldNameList.add('PricebookEntryId'); + } return fieldNameList; } @@ -155,6 +178,9 @@ public with sharing class OpportunityCloneService { for(SObject rec:recList){ SObject newRec = rec.clone(false,true,false,false); newRec.put(parentFieldName, newParentId); + if(newRec.getSObjectType().getDescribe().getName().toUpperCase() == 'OPPORTUNITYLINEITEM') { + newRec.put('UnitPrice',null); + } newList.add(newRec); } return newList; @@ -163,6 +189,9 @@ public with sharing class OpportunityCloneService { private static Object CreateObjectValueFromString(String fieldName, String fieldValue) { Map fieldSObjectMapping = Schema.Opportunity.getSObjectType().getDescribe().fields.getMap(); Schema.DisplayType fieldType = fieldSObjectMapping.get(fieldName).getDescribe().getType(); + if(String.isBlank(fieldValue)){ + return null; + } Object returnObj; switch on fieldType { when ADDRESS, COMBOBOX, EMAIL, ENCRYPTEDSTRING, MULTIPICKLIST, PHONE, PICKLIST, STRING, TEXTAREA,URL { @@ -175,7 +204,7 @@ public with sharing class OpportunityCloneService { returnObj = Double.valueOf(fieldValue); } when DATE { - returnObj = Date.parse(fieldValue); + returnObj = Date.valueOf(fieldValue); } when DATETIME { returnObj = Datetime.parse(fieldValue); diff --git a/force-app/main/default/classes/OpportunityCloneServiceTest.cls b/force-app/main/default/classes/OpportunityCloneServiceTest.cls new file mode 100644 index 0000000..e92feda --- /dev/null +++ b/force-app/main/default/classes/OpportunityCloneServiceTest.cls @@ -0,0 +1,120 @@ +/** + * @description The class will provide code coverage for the OpportunityCloneService + * @author Cloud Performer + * @version 1.0 + * @group Opportunity Cloning + * @history 2021-04-12 - CJG - Created + */ +@isTest +private class OpportunityCloneServiceTest { + @TestSetup + static void makeData(){ + List acctTests = new List(); + List productList = new List(); + List pricebookItems = new List(); + List oppTests = new List(); + List oliTestLines = new List(); + //create accounts + acctTests.add(new Account( + Name = TestCloneUtility.GenerateRandomString(10), + AccountNumber = TestCloneUtility.GenerateRandomString(8).toUpperCase(), + BillingStreet = '1 ' + TestCloneUtility.GenerateRandomString(10) + ' Street', + BillingCity = 'Atlanta', + BillingState = 'Georgia', + BillingPostalCode = '30303', + BillingCountry = 'USA' , + ShippingStreet = '2 ' + TestCloneUtility.GenerateRandomString(8) + ' Street', + ShippingCity = 'Atlanta', + ShippingState = 'Georgia', + ShippingPostalCode = '30303', + ShippingCountry = 'USA' + )); + insert acctTests; + //create Products + for(Integer i=0; i < 5;i++){ + productList.add(new Product2( + Name=TestCloneUtility.GenerateRandomString(5), + isActive = true, + ProductCode = TestCloneUtility.GenerateRandomString(6).toUpperCase() + )); + } + insert productList; + //obtain test Pricbook + Id testPricebookId = test.getStandardPricebookId(); + //create PricebookEntry records + for(Product2 prod:productList){ + pricebookItems.add(new PricebookEntry( + IsActive = true, + Pricebook2Id = testPricebookId, + UnitPrice = 1.0, + Product2Id = prod.Id + )); + } + insert pricebookItems; + //create test Opportunities + Schema.DescribeFieldResult fr = Schema.SObjectType.Opportunity.fields.StageName; + List stageValues = fr.getPicklistValues(); + for(Account a:acctTests){ + oppTests.add(new Opportunity( + Name = TestCloneUtility.GenerateRandomString(8), + CloseDate = System.today().addDays(30), + StageName = stageValues[0].getValue() + )); + } + insert oppTests; + //create OppLineItems + for(Opportunity opp:oppTests){ + for(PricebookEntry pbe:pricebookItems){ + oliTestLines.add(new OpportunityLineItem(PricebookEntryId=pbe.Id,Quantity=1.0,UnitPrice=pbe.UnitPrice, + OpportunityId=opp.Id)); + } + } + insert oliTestLines; + } + + @isTest + static void ObtainOpporunityChildRelationships(){ + Test.startTest(); + + Map resultMap = OpportunityCloneService.ObtainOpporunityChildRelationships(); + System.assert(!resultMap.isEmpty()); + + Test.stopTest(); + } + + @isTest + static void ObtainCloneOpportunityFields(){ + Test.startTest(); + + Map resultMap = OpportunityCloneService.ObtainCloneOpportunityFields('ClonedFields'); + System.assert(!resultMap.isEmpty()); + + Test.stopTest(); + } + + @isTest + static void ObtainQueryStringForOppAndChildren(){ + //get an opportunity + Opportunity testOpp = [SELECT Id FROM Opportunity LIMIT 1]; + List relStringList = new List{'OpportunityLineItems'}; + Test.startTest(); + + String queryStringTest = OpportunityCloneService.ObtainQueryStringForOppAndChildren(testOpp.Id, relStringList); + System.assert(String.isNotBlank(queryStringTest)); + + Test.stopTest(); + } + @isTest + static void CloneOpportunityWithChildren(){ + Opportunity testOpp = [SELECT Id, Name FROM Opportunity LIMIT 1]; + Map testReplacements = new Map(); + testReplacements.put('Name',TestCloneUtility.GenerateRandomString(10)); + Test.startTest(); + + Id testResult = OpportunityCloneService.CloneOpportunityWithChildren(testOpp.Id, testReplacements, + new List{'OpportunityLineItems'}); + System.assertNotEquals(null, testResult); + + Test.stopTest(); + } +} diff --git a/force-app/main/default/classes/OpportunityCloneServiceTest.cls-meta.xml b/force-app/main/default/classes/OpportunityCloneServiceTest.cls-meta.xml new file mode 100644 index 0000000..d75b058 --- /dev/null +++ b/force-app/main/default/classes/OpportunityCloneServiceTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 51.0 + Active + diff --git a/force-app/main/default/classes/TestCloneUtility.cls b/force-app/main/default/classes/TestCloneUtility.cls new file mode 100644 index 0000000..e7e230b --- /dev/null +++ b/force-app/main/default/classes/TestCloneUtility.cls @@ -0,0 +1,39 @@ +/** + * @description This is a class that provides utility methods for test classes + */ +public class TestCloneUtility { + /** + * @description This method will generate a string that is a certain length. Optionally, you can determine if the string will include numbers, or if the string will consist of numeric characters only. The option to consist of numeric characters only overrides the option of including numeric characters in the string characters. + * @param stringLength (Integer) Length of String + * @param includeNumbers (Boolean) Option to include numbers + * @param numbersOnly (Boolean) Option to only post numbers + * @return (String) Randomly Generated String + */ + static public String GenerateRandomString(Integer stringLength, Boolean includeNumbers, Boolean numbersOnly) { + String returnString = ''; + String characterBucket = ''; + String stringBucket = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ'; + String numericBucket = '123456789'; + characterBucket += stringBucket; + if(includeNumbers){ + characterBucket += numericBucket; + } + if(numbersOnly){ + characterBucket = numericBucket; + } + //now lets start to generate the String + while (returnString.length() < stringLength) { + Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), characterBucket.length()); + returnString += characterBucket.substring(idx, idx+1); + } + return returnString; + } + /** + * @description Generate a random string with wordly characters + * @param (Integer) stringLength; + * @return (String) random string + */ + static public String GenerateRandomString(Integer stringLength){ + return GenerateRandomString(stringLength, false, false); + } +} diff --git a/force-app/main/default/classes/TestCloneUtility.cls-meta.xml b/force-app/main/default/classes/TestCloneUtility.cls-meta.xml new file mode 100644 index 0000000..d75b058 --- /dev/null +++ b/force-app/main/default/classes/TestCloneUtility.cls-meta.xml @@ -0,0 +1,5 @@ + + + 51.0 + Active + diff --git a/force-app/main/default/labels/CustomLabels.labels-meta.xml b/force-app/main/default/labels/CustomLabels.labels-meta.xml new file mode 100644 index 0000000..ee2f647 --- /dev/null +++ b/force-app/main/default/labels/CustomLabels.labels-meta.xml @@ -0,0 +1,11 @@ + + + + SmartCloneChildList + SmartClone Config + en_US + true + SmartClone Child List + OpportunityLineItem + + diff --git a/force-app/main/default/lwc/opportunityCustomClone/opportunityCustomClone.html b/force-app/main/default/lwc/opportunityCustomClone/opportunityCustomClone.html index b5c24e2..741982d 100644 --- a/force-app/main/default/lwc/opportunityCustomClone/opportunityCustomClone.html +++ b/force-app/main/default/lwc/opportunityCustomClone/opportunityCustomClone.html @@ -6,19 +6,20 @@ +
+

Child Records to Clone

+
+
+ +
+
+ +
-
-

Child Records to Clone

-
-
- -
-
- -
+ \ No newline at end of file diff --git a/force-app/main/default/lwc/opportunityCustomClone/opportunityCustomClone.js b/force-app/main/default/lwc/opportunityCustomClone/opportunityCustomClone.js index 272a3db..90d1462 100644 --- a/force-app/main/default/lwc/opportunityCustomClone/opportunityCustomClone.js +++ b/force-app/main/default/lwc/opportunityCustomClone/opportunityCustomClone.js @@ -1,12 +1,13 @@ import { LightningElement, api, wire, track } from 'lwc'; import { getObjectInfo } from 'lightning/uiObjectInfoApi'; import { getRecord } from 'lightning/uiRecordApi'; +import { NavigationMixin} from 'lightning/navigation'; import OPPORTUNITY_OBJECT from '@salesforce/schema/Opportunity'; import ObtainCloneOpportunityFields from '@salesforce/apex/OpportunityCloneService.ObtainCloneOpportunityFields'; import ObtainChildRelationships from '@salesforce/apex/OpportunityCloneService.ObtainOpporunityChildRelationships'; import CloneOpportunityWithChildren from '@salesforce/apex/OpportunityCloneService.CloneOpportunityWithChildren'; -export default class OpportunityCustomClone extends LightningElement { +export default class OpportunityCustomClone extends NavigationMixin(LightningElement) { @api recordId; formObjectApiName; @@ -100,7 +101,30 @@ export default class OpportunityCustomClone extends LightningElement { handleSubmit(evt){ evt.preventDefault(); var fields = evt.detail.fields; - window.console.log('Fields Submitted: %s',JSON.stringify(fields,null,"\t")); + var childRelList = []; + this.childRelationshipSelectionList.forEach( selection => { + childRelList.push(selection.value); + }); + window.console.log('Child Relationships Selected: %s',JSON.stringify(childRelList,null,"\t")); + CloneOpportunityWithChildren({'oppId':this.recordId,'newValues':fields,'childRelationshipsToClone':childRelList}) + .then((results)=>{ + window.console.log('Successful callout. %s',JSON.stringify(results,null,"\t")); + this.sendToOpp(results); + }) + .catch((err)=>{ + window.console.log('There was an error attempting to clone records.'); + }); + } + sendToOpp(oppId){ + window.console.log('Send to Opp executed.'); + this[NavigationMixin.Navigate]({ + type:'standard__recordPage', + attributes:{ + recordId:oppId, + objectApiName:OPPORTUNITY_OBJECT.objectApiName, + actionName:'view' + } + }); } get formFields(){ diff --git a/manifest/package.xml b/manifest/package.xml index cb2f64a..63393cc 100644 --- a/manifest/package.xml +++ b/manifest/package.xml @@ -1,36 +1,26 @@ - * + OpportunityCloneService + OpportunityCloneServiceTest + TestCloneUtility ApexClass - * - ApexComponent - - - * - ApexPage - - - * - ApexTestSuite - - - * - ApexTrigger - - - * + OppCustomCloneAura AuraDefinitionBundle - * + SmartCloneChildList + CustomLabel + + + opportunityCustomClone LightningComponentBundle - * - StaticResource + Opportunity.SmartClone + QuickAction 51.0 \ No newline at end of file