tag:blogger.com,1999:blog-8264093881794534632024-02-18T23:58:02.697-08:00Salesforce UnlimitedCustom Force.com solutions in advanced Salesforce implementationsAnonymoushttp://www.blogger.com/profile/06829141799899469516noreply@blogger.comBlogger22125tag:blogger.com,1999:blog-826409388179453463.post-48386335291531901852018-04-13T07:56:00.002-07:002018-04-13T07:56:53.523-07:00Dynamic URL for Email Templates<div dir="ltr" style="text-align: left;" trbidi="on">
In some cases, you need to add dymanic URL to Visualforce Email template to Open link regardless of Salesforce Org.
To put URL dynamically use following code:
<code><pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 80px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">
<a href="{!LEFT($CurrentPage.URL,FIND('/',$CurrentPage.URL,9))}{!relatedTo.Quote__r.id}">{!relatedTo.Quote__r.QuoteNumber}</a>
</pre></code>
<br /></div>
Unknownnoreply@blogger.com2tag:blogger.com,1999:blog-826409388179453463.post-86501397664644264622018-04-12T01:00:00.000-07:002018-04-12T01:00:58.021-07:00Google Calendar Integration part 2<div dir="ltr" style="text-align: left;" trbidi="on">
<br />Create Custom settings:<br/>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7Ogxqr-QNL3hToaWLpXbZs8xTG1m_tQI0WVqbRAso8l_IBxn_NILmCVLLoDS6ea8BmI2H7rwjJPLajGQHIOZnn81X2WMGdCzS37npruFPb2WRr0ltbF92OQ1I6WDIzUyuyiVq53ZhLAQW/s1600/Screenshot_10.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7Ogxqr-QNL3hToaWLpXbZs8xTG1m_tQI0WVqbRAso8l_IBxn_NILmCVLLoDS6ea8BmI2H7rwjJPLajGQHIOZnn81X2WMGdCzS37npruFPb2WRr0ltbF92OQ1I6WDIzUyuyiVq53ZhLAQW/s640/Screenshot_10.png" width="590" height="270" data-original-width="919" data-original-height="421" /></a>
Client id:
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhk3rcm66CDwG0ei2FVw8HinED6KP8Nbnt-C7dIz0bBoAhXEDsG9pq1vyxCCxQsHjKtPYhKaHZptPxfWAHXWR-CunUBJzKn6DuBpKlE3zNjNzPntanupB8pbObYm41GR7eLhv_u07PLAsV7/s1600/Screenshot_2.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhk3rcm66CDwG0ei2FVw8HinED6KP8Nbnt-C7dIz0bBoAhXEDsG9pq1vyxCCxQsHjKtPYhKaHZptPxfWAHXWR-CunUBJzKn6DuBpKlE3zNjNzPntanupB8pbObYm41GR7eLhv_u07PLAsV7/s640/Screenshot_2.png" width="590" height="161" data-original-width="814" data-original-height="230" /></a>
</div>
Client Secret:
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWiBJdypRPT4Jip4K6WQlLDzqVFYWmYJqgllYAyLAh5vPsPxAdoMnbRhfDqFd4K4-gsqEFTTaHTQ80rgGrLPr5xzOn3xrJPbXDyTpr9PUtwl9fJgyG7soBMYzlNduwgrBTUqOAYR4BZM2f/s1600/Screenshot_3.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWiBJdypRPT4Jip4K6WQlLDzqVFYWmYJqgllYAyLAh5vPsPxAdoMnbRhfDqFd4K4-gsqEFTTaHTQ80rgGrLPr5xzOn3xrJPbXDyTpr9PUtwl9fJgyG7soBMYzlNduwgrBTUqOAYR4BZM2f/s640/Screenshot_3.png" width="590" height="115" data-original-width="841" data-original-height="178" /></a>
URL Token:<br/>
https://developers.google.com/identity/protocols/OAuth2InstalledApp<br/>
URL EventInsert:<br/>
https://developers.google.com/calendar/v3/reference/events/insert<br/>
URL Calendars, URL Calendars:<br/>
https://developers.google.com/calendar/v3/reference/calendars
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-9814354875445476722018-04-11T03:16:00.000-07:002018-04-11T03:16:17.145-07:00Google Calendar Intergration part 1<div dir="ltr" style="text-align: left;" trbidi="on">
<br />This article describes how to create a Google Calendar on the Contact app. To create an Opportunity associated with Contact, an Google Calendar event is created. To delete or update Opportunity event will be deleted or updated.</div>
<div>1) Create project in Google Developer Console.</div>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCa3jizw4fjmHlSazaAJiSv9TbQDudHM2XusjBl27ULvd1lhG1Swi95GD2Jch2aASVR-ldUJKpEOY_qv8BQOn9xgFO6kKSKF4AN_BEZwKyZzC6quUS-nJ1UNBOkDdRjukl-1DJAD2lhaNH/s1600/Screenshot_1.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCa3jizw4fjmHlSazaAJiSv9TbQDudHM2XusjBl27ULvd1lhG1Swi95GD2Jch2aASVR-ldUJKpEOY_qv8BQOn9xgFO6kKSKF4AN_BEZwKyZzC6quUS-nJ1UNBOkDdRjukl-1DJAD2lhaNH/s400/Screenshot_1.png" width="400" height="51" data-original-width="736" data-original-height="93" /></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhcL7cogUry_MmQnprW8XmJ7vum81V7C5q1jAVx5YXqaK_bd0yX5QWFn7QXkfMVpqkK5Reif_EsIiqC3_GKEzvrg2sXdjCETydz-cSrFUOaDMbCl9QA183VvBPNUHKhESmh5KGFjTogdGVX/s1600/Screenshot_2.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhcL7cogUry_MmQnprW8XmJ7vum81V7C5q1jAVx5YXqaK_bd0yX5QWFn7QXkfMVpqkK5Reif_EsIiqC3_GKEzvrg2sXdjCETydz-cSrFUOaDMbCl9QA183VvBPNUHKhESmh5KGFjTogdGVX/s400/Screenshot_2.png" width="400" height="248" data-original-width="480" data-original-height="298" /></a>
2) Create Credentials:
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjeK0WU3syawVuiYxhh-2BJQjKA8L9zNLcdvjwHOZoZfhvOAa2OzG3hrSKTOUtnC40kjY3OLTlqT5ds7mg9IioIKHiT_3zctqYREVG8tHxbruqBK0BNOD3fv_H8AF2-2rlWQr1Cug5Ml5VF/s1600/Screenshot_3.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjeK0WU3syawVuiYxhh-2BJQjKA8L9zNLcdvjwHOZoZfhvOAa2OzG3hrSKTOUtnC40kjY3OLTlqT5ds7mg9IioIKHiT_3zctqYREVG8tHxbruqBK0BNOD3fv_H8AF2-2rlWQr1Cug5Ml5VF/s400/Screenshot_3.png" width="400" height="223" data-original-width="453" data-original-height="253" /></a>
<div>API key:</div>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgakJsxR0j8A_f5v_Dp_1mFYw65isDiEXRkjucRORovvvAmA3gVLXdu3bxMlZTCVUabT_h00z5Rk626_hdFLauXGNkwboAFERvKTc7F6pROWvzV0GWwIALstu1DfXfflZx50MN5ZEfmjcrI/s1600/Screenshot_4.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgakJsxR0j8A_f5v_Dp_1mFYw65isDiEXRkjucRORovvvAmA3gVLXdu3bxMlZTCVUabT_h00z5Rk626_hdFLauXGNkwboAFERvKTc7F6pROWvzV0GWwIALstu1DfXfflZx50MN5ZEfmjcrI/s400/Screenshot_4.png" width="400" height="177" data-original-width="634" data-original-height="280" /></a>
<div>OAuth 2.0 client IDs:</div>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKIBw3xz0rsgTcj1VI925KqMAidA4KXOE8k_B47gISh1GDK-8dGiXYG3rmNTdix2YLMWU3pZjZpyWVy3k23YzWZ3q7Vt0gR3SzKOEPsReidTe2EFIsGXaLxdMUTNPrdPflMrWN9q9HK8gb/s1600/Screenshot_6.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKIBw3xz0rsgTcj1VI925KqMAidA4KXOE8k_B47gISh1GDK-8dGiXYG3rmNTdix2YLMWU3pZjZpyWVy3k23YzWZ3q7Vt0gR3SzKOEPsReidTe2EFIsGXaLxdMUTNPrdPflMrWN9q9HK8gb/s400/Screenshot_6.png" width="400" height="233" data-original-width="470" data-original-height="274" /></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgDB8MNo2hxILlqljxaWzdKJgdw0coROTwQEBvpPaTzw_3WpGIZ2MM1pyYs4m0g4MHm-0LUiCDMzEAqbk0VGfif_te83IfCjNTRCQ-lgFJwrpHdC851ZkeYmTj0GElhEm237_FIiRcpnSYi/s1600/Screenshot_8.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgDB8MNo2hxILlqljxaWzdKJgdw0coROTwQEBvpPaTzw_3WpGIZ2MM1pyYs4m0g4MHm-0LUiCDMzEAqbk0VGfif_te83IfCjNTRCQ-lgFJwrpHdC851ZkeYmTj0GElhEm237_FIiRcpnSYi/s400/Screenshot_8.png" width="400" height="89" data-original-width="1262" data-original-height="280" /></a>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk2C4448YfNfyWMbM3OsqauyFaYdb-hwP48umLEi14XIoGvx0aouWC9TRQxKeNMe321E56n6MLOvEjv90mkVzTvkNMsFrcxtXz7NfkSZSB4BBt0hPtnquW40MB2Rb976MV99Lhu4YZ6HNx/s1600/Screenshot_7.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk2C4448YfNfyWMbM3OsqauyFaYdb-hwP48umLEi14XIoGvx0aouWC9TRQxKeNMe321E56n6MLOvEjv90mkVzTvkNMsFrcxtXz7NfkSZSB4BBt0hPtnquW40MB2Rb976MV99Lhu4YZ6HNx/s400/Screenshot_7.png" width="250" height="400" data-original-width="361" data-original-height="578" /></a>
Google console project created.
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-35988489506676798362018-04-11T03:15:00.000-07:002018-04-11T03:15:57.180-07:00Multiple Organizaton<div dir="ltr" style="text-align: left;" trbidi="on">
1) Create Role hierarchy for each organization.<br/>
2) Make all custom and standard objects Private to make inaccessible to users from another organization.<br/>
3) Created new Account Record Type - Organization. Create new Records with this record Type for all organizations. <br/>
4) For Reports create custom field "Organization" on Opportunity and "Company" on User. Here we write down the organization name to which the user belongs. The value entered in the field must match the account name for the corresponding organization.<br/>
5) Create trigger which will fill out "Organization" field on Opportunity regard of User's Company.<br/>
</div>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-16047296033866661012018-02-28T06:46:00.001-08:002018-02-28T06:46:53.435-08:00Lead perfomance and tracking of Status, Owners and Activities<div dir="ltr" style="text-align: left;" trbidi="on">
<p><strong>Lead Status and Owner Tracking</strong></p>
Quite often requires tracking changes of Lead Status and Owners and Lead Activities. <br/>
To store Lead changes, time between changes, and Activities we used Lead Tracking custom object:<br/>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2Clov_tflbm7i_BQWz-KOSTld5rAhRNHg8eXFQegX5jwads726Tpm3T3TOlGwq8Yo_59ExzLBSDr8SoKy4APoGpLCLiAq-Rq6MC5BEImIfPay9BkGpjaaRxCVNb1j3I11owdeUfKOlAnG/s1600/11111.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2Clov_tflbm7i_BQWz-KOSTld5rAhRNHg8eXFQegX5jwads726Tpm3T3TOlGwq8Yo_59ExzLBSDr8SoKy4APoGpLCLiAq-Rq6MC5BEImIfPay9BkGpjaaRxCVNb1j3I11owdeUfKOlAnG/s400/11111.png" width="213" height="400" data-original-width="222" data-original-height="417" /></a><br/>
When Lead Status or Owner changed, will be inserted new Lead Tracking record. In related list show "New Status", "Previous Status"(if End Status is empty - current Status), "Start" Date, "End" Date and "Elapsed Time". If Lead Status again to change, record with empty "New Status" field will be updated: will write down a new Lead Status and date of the change. <br/>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitui2uCHP92UPoTukhay7xPam8O3XueFyZJ0i_hqKNoUzgSI43BxSOB-iDbxQAj7YPVWeid0cimc0djnpZhOrDrsFM8cLLpgQseLVRsi7ZY4LZ_iYb7oJin_Z8zmxb2wlgCQGCi_1XqA_N/s1600/44444.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitui2uCHP92UPoTukhay7xPam8O3XueFyZJ0i_hqKNoUzgSI43BxSOB-iDbxQAj7YPVWeid0cimc0djnpZhOrDrsFM8cLLpgQseLVRsi7ZY4LZ_iYb7oJin_Z8zmxb2wlgCQGCi_1XqA_N/s640/44444.png" width="580" height="60" data-original-width="1086" data-original-height="136" /></a>
If Lead Owner changed, Lead Tracking inserts similar like when changed Status, but "Field" will be "Onwer", "Previous" and "New" a new Owner and Previous Owner.
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjC0QXdOiq9qPhEzPsu-A1KWdz_GstGM86cqQHwQAmRoZAxoZqO1UGc4SeQHJWztVKXk9_VfM6Qgu_R0sIoeXlI37oWtxn8XZyTMjna4QZUQY9vfVLavag_GOX__gRMRHQQ6yZEVvtMDkVK/s1600/555555.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjC0QXdOiq9qPhEzPsu-A1KWdz_GstGM86cqQHwQAmRoZAxoZqO1UGc4SeQHJWztVKXk9_VfM6Qgu_R0sIoeXlI37oWtxn8XZyTMjna4QZUQY9vfVLavag_GOX__gRMRHQQ6yZEVvtMDkVK/s640/555555.png" width="580" height="108" data-original-width="1092" data-original-height="218" /></a>
Trigger handler:
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 250px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">
public Map<Id, String> getNewOwnerName(Set<Id> ownIds){
Map<Id, String> ownerMap = new Map<Id, String>();
List<Group> groups = [SELECT Id, Name FROM Group WHERE Id IN :ownIds];
List<User> users = [SELECT Id, Name FROM User WHERE Id IN :ownIds];
for(Group g : groups){
ownerMap.put(g.Id, g.Name);
}
for(User u: users){
ownerMap.put(u.Id, u.Name);
}
return ownerMap;
}
public void afterInsert( List<lead> newleads){
List<Lead_Tracking__c> toInsert = new List<Lead_Tracking__c>();
Set<Id> ownIds = new Set <Id>();
for(Lead l : newleads){
ownIds.add(l.OwnerId);
}
Map<Id, String> ownerMap = getNewOwnerName(ownIds);
for(Lead l : newleads){
Lead_Tracking__c tr = new Lead_Tracking__c();
tr.Start__c = l.CreatedDate;
tr.Start_Value__c = l.Status;
tr.Lead__c = l.Id;
tr.Changed_by__c = UserInfo.getUserId();
tr.Field__c = 'Status';
toInsert.Add(tr);
Lead_Tracking__c tr1 = new Lead_Tracking__c();
tr1.Start__c = l.CreatedDate;
tr1.Start_Value__c = ownerMap.get(l.OwnerId);
tr1.Lead__c = l.Id;
tr1.Changed_by__c = UserInfo.getUserId();
tr1.Field__c = 'Owner';
tr1.Owner_Type__c = (String.valueOf(l.OwnerId).StartsWith('005')? 'User' : 'Queue');
toInsert.Add(tr1);
}
insert toInsert;
}
public void afterUpdate(List<Lead> newLeads, map<Id, Lead> oldMap){
Map<Id, Lead> newValMap = new map<Id, Lead>();
Set<Id> ownerIds = new Set<Id>();
for(Lead l: newLeads){
if(l.Status != oldMap.get(l.Id).Status || l.OwnerID != oldMap.get(l.Id).OwnerID){
newValMap.put(l.Id, l);
ownerIds.add(l.OwnerId);
}
}
Map <Id, String> newOwnerMap = getNewOwnerName(ownerIds);
List<Lead_Tracking__c> toUpdList = new List<Lead_Tracking__c>();
toUpdList = [SELECT Id, End__c, End_Value__c, Lead__c, Start__c, Start_Value__c, Changed_by__c, Lead__r.OwnerId, Field__c
FROM Lead_Tracking__c
WHERE Lead__c IN: newValMap.keySet() AND End__c = null AND End_Value__c =null ];
List<Lead_Tracking__c>toInsert = new List<Lead_Tracking__c>();
set<Id> existLeads = new set<Id>();
for(Lead_Tracking__c tr: toUpdList){
existLeads.add(tr.Lead__c);
if(tr.Field__c == 'Status' && tr.Start_Value__c == oldMap.get(tr.Lead__c).Status && oldMap.get(tr.Lead__c).Status != newValMap.get(tr.Lead__c).Status ){
tr.End_Value__c = newValMap.get(tr.Lead__c).Status;
tr.End__c = newValMap.get(tr.Lead__c).LastModifiedDate;
tr.Changed_by__c = UserInfo.getUserId();
Lead_Tracking__c new_tr = new Lead_Tracking__c();
new_tr.Lead__c = tr.Lead__c;
new_tr.Start__c = newValMap.get(tr.Lead__c).LastModifiedDate;
new_tr.Start_Value__c = newValMap.get(tr.Lead__c).Status;
new_tr.Changed_by__c = UserInfo.getUserId();
new_tr.Field__c = 'Status';
toInsert.add(new_tr);
}
if(tr.Field__c == 'Owner' && oldMap.get(tr.Lead__c).OwnerId != newValMap.get(tr.Lead__c).OwnerId ){
tr.End_Value__c = newOwnerMap.get(tr.Lead__r.OwnerId);
tr.End__c = newValMap.get(tr.Lead__c).LastModifiedDate;
tr.Changed_by__c = UserInfo.getUserId();
Lead_Tracking__c new_tr = new Lead_Tracking__c();
new_tr.Lead__c = tr.Lead__c;
new_tr.Start__c = newValMap.get(tr.Lead__c).LastModifiedDate;
new_tr.Start_Value__c = newOwnerMap.get(tr.Lead__r.OwnerId);
new_tr.Changed_by__c = UserInfo.getUserId();
new_tr.Field__c = 'Owner';
new_tr.Owner_Type__c = (String.valueOf(tr.Lead__r.OwnerId).StartsWith('005')? 'User' : 'Queue');
toInsert.add(new_tr);
}
}
for(Lead l : newLeads){
if(!existLeads.contains(l.id) && (l.Status != oldMap.get(l.Id).Status || l.OwnerID != oldMap.get(l.Id).OwnerID)){
Lead_Tracking__c tr = new Lead_Tracking__c();
tr.Start__c = system.now();
tr.Start_Value__c = l.Status;
tr.Lead__c = l.Id;
tr.Changed_by__c = UserInfo.getUserId();
tr.Field__c = 'Status';
toInsert.Add(tr);
Lead_Tracking__c tr1 = new Lead_Tracking__c();
tr1.Start__c = system.now();
tr1.Start_Value__c = newOwnerMap.get(l.OwnerId);
tr1.Lead__c = l.Id;
tr1.Changed_by__c = UserInfo.getUserId();
tr1.Field__c = 'Owner';
tr1.Owner_Type__c = (String.valueOf(l.OwnerId).StartsWith('005')? 'User' : 'Queue');
toInsert.Add(tr1);
}
}
insert toInsert;
update toUpdList;
}
public void beforeDelete(map<Id, Lead> oldMap){
List <Lead_Tracking__c> toDelete = [SELECT Id FROM Lead_Tracking__c WHERE Lead__c IN: oldMap.keySet()];
delete toDelete;
}
</code></pre>
<p><strong>Elapsed time before first Activity</strong></p>
When the first Activity of certain Lead Owner created, the trigger on Activity writes downtime between lead assignment and creation of activity.
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 250px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">
public void afterInsert(List<Task> callList){
Set<Id> leadIds = new Set<Id>();
Map<Id, Lead_Tracking__c> trackMapUpd = new Map<Id, Lead_Tracking__c>();
List<Lead_Tracking__c> toUpd = new List<Lead_Tracking__c>();
for(Task t : callList){
System.debug(t);
if(t.Subject.contains('Call')){
leadIds.add(t.WhoId);
}
}
for(Lead_Tracking__c tr : [SELECT Id, Lead__c, Elapsed_time_before_first_activity__c, Start__c, CreatedDate, End_Value__c FROM Lead_Tracking__c WHERE Lead__c IN: leadIds AND Field__c = 'Owner' AND End_Value__c = null ORDER BY Start__c ASC]){
if(tr.Elapsed_time_before_first_activity__c == null){
trackMapUpd.put(tr.Lead__c, tr);
}
}
if(trackMapUpd.size()>0){
for(Task t : callList){
trackMapUpd.get(t.WhoId).Elapsed_time_before_first_activity__c = GetElapsedTime(trackMapUpd.get(t.WhoId).CreatedDate, System.now());
toUpd.add(trackMapUpd.get(t.WhoId));
}
update toUpd;
}
}
public static Decimal GetElapsedTime(DateTime startTime, DateTime endTime){
if(startTime == null || endTime == null){
return null;
}
return ((endTime.getTime())/1000/60) - ((startTime.getTime())/1000/60);
}
</code></pre>
Lead Tracking:
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhztwe-SPr-4REkjv5aeZ2nR65RBQKij-VVkkjix1aOg1H9gPu8I3Sy86jPzIjSmrvVjMfg2gkQNKWcAfRr-WNoZ_0IXDtC2Q9wq0oBSeE1umFHcKl4WBqdKj4ZfaMuqGYJezzQtrGUbyHK/s1600/7777.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhztwe-SPr-4REkjv5aeZ2nR65RBQKij-VVkkjix1aOg1H9gPu8I3Sy86jPzIjSmrvVjMfg2gkQNKWcAfRr-WNoZ_0IXDtC2Q9wq0oBSeE1umFHcKl4WBqdKj4ZfaMuqGYJezzQtrGUbyHK/s640/7777.png" width="580" height="50" data-original-width="1079" data-original-height="101" /></a>
<br /></div>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-19667848591918509022017-07-27T05:03:00.000-07:002017-07-27T08:30:45.030-07:00"Who knows who" relations in Salesforce Contact and Accounts - part 2: Solution description<div dir="ltr" style="text-align: left;" trbidi="on">
<p><strong>"Who Knows Who" - insert, update and delete reverse records</strong></p>
"Who Knows Who" custom object has Master-Detail relation with "reverse" record which inserts after insertion first record.
When "Who Knows Who" record inserted, fires trigger which inserts "reverse" record:
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 250px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"> if(trigger.IsInsert){
for(Linked_Relationship__c l: trigger.new){
if(l.Master_Relationship__c == null && l.Firm__c != l.Related_To_Firm__c){
Linked_Relationship__c lc = new Linked_Relationship__c();
lc.Firm__c = l.Related_To_Firm__c;
lc.Contact__c = l.Related_To_Contact__c;
lc.Related_To_Contact__c = l.Contact__c;
lc.Related_To_Firm__c = l.Firm__c;
lc.Reverse_Type__c = l.Type__c;
lc.Type__c = l.Reverse_Type__c;
lc.Comments__c = l.Comments__c;
lc.Master_Relationship__c = l.id;
lc.Reverse_Contact_Relationship__c = l.Contact_Relationship__c;
lc.Contact_Relationship__c = l.Reverse_Contact_Relationship__c;
toInsert.Add(lc);
}
}
}
</code></pre>
After "Who Knows Who" record update "reverse" record also will be updated (no difference - master or detail):
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">if(trigger.IsUpdate){
for(Linked_Relationship__c lr : trigger.new){
if(lr.Master_Relationship__c == null){
lrIds.Add(lr.Id);
}
else{
masterIds.Add(lr.Master_Relationship__c);
}
}
List<Linked_Relationship__c> updateList = [SELECT Id, Firm__c, Contact__c, Related_To_Contact__c, Related_To_Firm__c,
Reverse_Type__c, Type__c, Comments__c, Master_Relationship__c,
Reverse_Contact_Relationship__c, Contact_Relationship__c
FROM Linked_Relationship__c WHERE
Master_Relationship__c IN: lrIds OR Id IN: masterIds];
for(Linked_Relationship__c l : updateList ){
if(l.Master_Relationship__c != null){
lrMap.put(l.Master_Relationship__c, l);
}
else{
lrMap.put(l.Id, l);
}
}
for(Linked_Relationship__c lr : trigger.new){
if(lrMap.containskey(lr.Id)) {
if (!(lrMap.get(lr.Id).Firm__c==lr.Related_To_Firm__c&&lrMap.get(lr.Id).Related_To_Firm__c==lr.Firm__c&&lrMap.get(lr.Id).Related_To_Contact__c==lr.Contact__c&&lrMap.get(lr.Id).Contact__c==lr.Related_To_Contact__c&&lrMap.get(lr.Id).Type__c==lr.Reverse_Type__c&&lrMap.get(lr.Id).Reverse_Type__c==lr.Type__c&&lrMap.get(lr.Id).Comments__c==lr.Comments__c&&lrMap.get(lr.Id).Reverse_Contact_Relationship__c == lr.Contact_Relationship__c&&lrMap.get(lr.Id).Contact_Relationship__c == lr.Reverse_Contact_Relationship__c )){
lrMap.get(lr.Id).Firm__c = lr.Related_To_Firm__c;
lrMap.get(lr.Id).Contact__c = lr.Related_To_Contact__c;
lrMap.get(lr.Id).Related_To_Contact__c = lr.Contact__c;
lrMap.get(lr.Id).Related_To_Firm__c = lr.Firm__c;
lrMap.get(lr.Id).Reverse_Type__c = lr.Type__c;
lrMap.get(lr.Id).Type__c = lr.Reverse_Type__c;
lrMap.get(lr.Id).Comments__c = lr.Comments__c;
lrMap.get(lr.Id).Reverse_Contact_Relationship__c = lr.Contact_Relationship__c;
lrMap.get(lr.Id).Contact_Relationship__c = lr.Reverse_Contact_Relationship__c;
toInsert.Add(lrMap.get(lr.Id));
}
}
if(lrMap.containskey(lr.Master_Relationship__c)){
if(!(lrMap.get(lr.Master_Relationship__c).Firm__c==lr.Related_To_Firm__c&&lrMap.get(lr.Master_Relationship__c).Related_To_Firm__c==lr.Firm__c&&lrMap.get(lr.Master_Relationship__c).Related_To_Contact__c==lr.Contact__c&&lrMap.get(lr.Master_Relationship__c).Contact__c==lr.Related_To_Contact__c&&lrMap.get(lr.Master_Relationship__c).Type__c==lr.Reverse_Type__c&&lrMap.get(lr.Master_Relationship__c).Reverse_Type__c==lr.Type__c&&lrMap.get(lr.Master_Relationship__c).Comments__c==lr.Comments__c&&lrMap.get(lr.Master_Relationship__c).Reverse_Contact_Relationship__c == lr.Contact_Relationship__c&&lrMap.get(lr.Master_Relationship__c).Contact_Relationship__c == lr.Reverse_Contact_Relationship__c )){
lrMap.get(lr.Master_Relationship__c).Firm__c = lr.Related_To_Firm__c;
lrMap.get(lr.Master_Relationship__c).Contact__c = lr.Related_To_Contact__c;
lrMap.get(lr.Master_Relationship__c).Related_To_Contact__c = lr.Contact__c;
lrMap.get(lr.Master_Relationship__c).Related_To_Firm__c = lr.Firm__c;
lrMap.get(lr.Master_Relationship__c).Reverse_Type__c = lr.Type__c;
lrMap.get(lr.Master_Relationship__c).Type__c = lr.Reverse_Type__c;
lrMap.get(lr.Master_Relationship__c).Comments__c = lr.Comments__c;
lrMap.get(lr.Master_Relationship__c).Reverse_Contact_Relationship__c = lr.Contact_Relationship__c;
lrMap.get(lr.Master_Relationship__c).Contact_Relationship__c = lr.Reverse_Contact_Relationship__c;
toInsert.Add(lrMap.get(lr.Master_Relationship__c));
}
}
}
}
</code></pre>
If "Who Knows Who" record deleted, "reverse" record also will be deleted:
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 150px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">for(Linked_Relationship__c lr: trigger.old){
lrMap.put(lr.Firm__c, lr);
}
List<Linked_Relationship__c> lrList = [SELECT Id, Firm__c, Contact__c, Related_To_Contact__c, Related_To_Firm__c,
Reverse_Type__c, Type__c, Comments__c, Master_Relationship__c,
Reverse_Contact_Relationship__c, Contact_Relationship__c
FROM Linked_Relationship__c WHERE
Related_To_Firm__c IN: lrMap.keySet() ];
for(Linked_Relationship__c l : lrList ) {
if(lrMap.get(l.Related_To_Firm__c).Firm__c==l.Related_To_Firm__c&&lrMap.get(l.Related_To_Firm__c).Related_To_Firm__c==l.Firm__c&&lrMap.get(l.Related_To_Firm__c).Related_To_Contact__c==l.Contact__c && lrMap.get(l.Related_To_Firm__c).Contact__c==l.Related_To_Contact__c&&lrMap.get(l.Related_To_Firm__c).Reverse_Type__c==l.Type__c&&lrMap.get(l.Related_To_Firm__c).Type__c==l.Reverse_Type__c&&lrMap.get(l.Related_To_Firm__c).Contact_Relationship__c==l.Reverse_Contact_Relationship__c&&lrMap.get(l.Related_To_Firm__c).Reverse_Contact_Relationship__c==l.Contact_Relationship__c){
toDelete.Add(l);
}
}
delete toDelete;
</code></pre>
<p><strong>Trigger on Contact</strong></p>
When Contact record inserted, trigger insert "Who Knows Who" records("User knows Contact" scenario).
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 130px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">for(Contact c: trigger.new){
Linked_Relationship__c l = new Linked_Relationship__c();
l.Firm__c = knowContList[0].AccountId;
l.Contact__c = knowContList[0].Id;
l.Related_To_Firm__c = c.AccountId;
l.Related_To_Contact__c = c.Id;
toInsert.Add(l);
}
}
</code></pre>
After insertion of the first record, trigger on "Who Knows Who" inserts second - reverse record.<br/>
After update of Contact - change Contact's Account, triggers updates related "Who Knows Who" records.<br/>
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 250px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">for(Contact c: trigger.new){
if(c.AccountId != trigger.oldMap.get(c.Id).AccountId ){
contIds.Add(c.id);
Linked_Relationship__c l = new Linked_Relationship__c();
l.Firm__c = c.AccountId;
l.Related_To_Firm__c = c.AccountId;
l.Related_To_Contact__c = c.Id;
l.Reverse_Contact_Relationship__c = 'Employee';
l.Type__c = 'Employer';
toInsert.Add(l);
}
}
List<Linked_Relationship__c> l_oldList = [SELECT Id, Firm__c, Type__c, Comments__c, Contact_Relationship__c,
Related_To_Firm__c, Related_To_Contact__c, Reverse_Type__c,
Reverse_Contact_Relationship__c, Contact__c FROM Linked_Relationship__c
WHERE Related_To_Contact__c IN: contIds ];
Map<Id,List<Linked_Relationship__c>> lrMap = new Map<Id, List<Linked_Relationship__c>>();
List<Linked_Relationship__c> otherList = new List<Linked_Relationship__c>();
for(Linked_Relationship__c lr : l_oldList ){
if(lr.Reverse_Contact_Relationship__c != null && lr.Reverse_Contact_Relationship__c.contains('Employee')){
if(lrMap.containskey(lr.Related_To_Contact__c)){
lrMap.get(lr.Related_To_Contact__c).Add(lr);
}
else{
lrMap.put(lr.Related_To_Contact__c, new List<Linked_Relationship__c>{lr});
}
}
else{
otherList.Add(lr);
}
}
for(Contact c: [SELECT Id, AccountId, Account.Name FROM Contact WHERE Id IN: trigger.new]){
Linked_Relationship__c l_new = new Linked_Relationship__c();
if(!lrMap.containskey(c.Id)){
l_new.Firm__c = c.AccountId;
l_new.Reverse_Type__c = 'Former Employer';
l_new.Contact__c = c.Id;
l_new.Contact_Relationship__c = 'Former Employee';
l_new.Related_To_Firm__c = trigger.oldMap.get(c.Id).AccountId;
l_new.Comments__c = string.valueof(date.today()) + ' - ' + c.Account.Name ;
toInsert.Add(l_new);
}
else{
for(Linked_Relationship__c l_old : lrMap.get(c.Id)){
l_old.Type__c = 'Former Employer';
l_old.Reverse_Contact_Relationship__c = 'Former Employee';
l_old.Comments__c = string.valueof(date.today())+ ' - ' + c.Account.Name;
l_old.Related_To_Firm__c = c.AccountId;
l_old.Related_To_Contact__c = c.Id;
toInsert.Add(l_old);
}
}
for(Linked_Relationship__c l_other : otherList ){
if(l_other.Contact__c == c.Id){
l_other.Firm__c = c.AccountId;
l_other.Contact__c = c.Id;
toInsert.Add(l_other);
}
}
</code></pre>
If Contact deleted, all related "Who Knows Who" records also will be deleted.
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 150px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">for(Contact c: trigger.old){
contIds.Add(c.Id);
}
List<Linked_Relationship__c> lrList = [SELECT Id, Related_To_Contact__c, Contact__c
FROM Linked_Relationship__c
WHERE Contact__c IN: contIds OR Related_To_Contact__c IN: contIds];
if(lrList.size()>0){
delete lrList;
}
</code></pre>
<br /></div>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-67128543756399902412017-07-27T05:02:00.000-07:002017-07-27T08:30:45.034-07:00"Who knows who" relations for Salesforce Contact and Accounts - part 1: How this works<div dir="ltr" style="text-align: left;" trbidi="on">
Sometimes we need to store information about Contact-Contact, Account-Contact, Account-Account relations:
<br />
<ul>
<li>User knows Contact and Account</li>
<li>Contact is employee of a Company;</li>
<li>Account is Employer of a Contact;</li>
<li>Contact is former employee of a Company;</li>
<li>Account is former Employer of a Contact, etc.</li>
</ul>
For this purpose we used Custom object "Who Knows Who":
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrSflf6LHYpSjvDupJ507tJ0SAWisce5MJ3ylJLvGW6RWlBkBBhfX77q_Qh-r9MUYIwcRpvUa2004lv8lBnki3ZShnbLZ80gcaRxH7BgX2GicS1kqowGPhzaRxpPdCz32cmSqicO67G35n/s1600/1111.png" imageanchor="1" style="clear: left; float: none; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="388" data-original-width="264" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrSflf6LHYpSjvDupJ507tJ0SAWisce5MJ3ylJLvGW6RWlBkBBhfX77q_Qh-r9MUYIwcRpvUa2004lv8lBnki3ZShnbLZ80gcaRxH7BgX2GicS1kqowGPhzaRxpPdCz32cmSqicO67G35n/s400/1111.png" width="272" /></a></div>
<br />
The records can be created manually and automatic. After insert "Who Knows Who" record fires trigger which inserts "reverse" record.<br />
<div style="font-size: 1rem;">
<b>Automatic scenarios</b></div>
<b>User "knows" Account and created Contact</b><br />
When new Contact created, new "Who Knows Who" record is inserted:
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEju2_MUgUyHkFGGEjIzkczbFfxgM9m2LKTZ5i9O3Gw8whGCgWFjfSEOsg3FXCKlM5Hbrn3KfLK23Zq_Ml1tsj2qybwS4M6n5WHcOPN70eymMCkhDS5N8xkR9BVfsaeVfwY1KlYmtVvdhFt2/s1600/2222.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="125" data-original-width="579" height="118" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEju2_MUgUyHkFGGEjIzkczbFfxgM9m2LKTZ5i9O3Gw8whGCgWFjfSEOsg3FXCKlM5Hbrn3KfLK23Zq_Ml1tsj2qybwS4M6n5WHcOPN70eymMCkhDS5N8xkR9BVfsaeVfwY1KlYmtVvdhFt2/s640/2222.png" width="580" /></a></div>
Contact related lists:
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidYx8iIEzkLClWH60P6HI6rJGgrwqZlbno6BgX2IPDKROEg_Mcac_SdWY8NVrMMHBIpvrOJp5mQDeBcK9ZxQj_4kURjNmg3WOHgPPW30Q02DDip9uUC5i5mm3seQV8vCnaFMpu13aIRjb9/s1600/3333.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="209" data-original-width="1008" height="103" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidYx8iIEzkLClWH60P6HI6rJGgrwqZlbno6BgX2IPDKROEg_Mcac_SdWY8NVrMMHBIpvrOJp5mQDeBcK9ZxQj_4kURjNmg3WOHgPPW30Q02DDip9uUC5i5mm3seQV8vCnaFMpu13aIRjb9/s640/3333.png" width="580" /></a></div>
This Contact and Company related to User:
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEixn0Vi2U5cdK0ZPY3aj4Bqu-am_xGQpKVVSZIHP5bTXayZV22NCsz4Jo1Pa-CkCAMudUl7bREgSA0rxbaefehrt9H0nEiXq8xmDYXOT1OdZYG2bBqah7rDiGnSvYJZDGeaOMC3dIqmiXrx/s1600/4444.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="196" data-original-width="850" height="118" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEixn0Vi2U5cdK0ZPY3aj4Bqu-am_xGQpKVVSZIHP5bTXayZV22NCsz4Jo1Pa-CkCAMudUl7bREgSA0rxbaefehrt9H0nEiXq8xmDYXOT1OdZYG2bBqah7rDiGnSvYJZDGeaOMC3dIqmiXrx/s640/4444.png" width="580" /></a></div>
The user who creates this Contact "knows" it and Contact's Company (reverse record).
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiS__ClSr7_F3RGP7hU8Jw_D_mS17I2xqFz3Zz-0BWAKWuhR-Fp96QMYXW3d1plfezqRQHgRzgLpjtxF0FL1uqL7v9JmaAA9AL9e0-PRN-CbNw_tyCPhQwesxzQTuZrfv0Di0x_S5AXP2XE/s1600/5555.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="162" data-original-width="837" height="94" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiS__ClSr7_F3RGP7hU8Jw_D_mS17I2xqFz3Zz-0BWAKWuhR-Fp96QMYXW3d1plfezqRQHgRzgLpjtxF0FL1uqL7v9JmaAA9AL9e0-PRN-CbNw_tyCPhQwesxzQTuZrfv0Di0x_S5AXP2XE/s640/5555.png" width="580" /></a></div>
"Known" Contact("User" field - lookup to User):
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA1trTiqFIfytfIf9btyz5UFJr5rz3l3cg2rh1dgIahKIYuMDQ1K9-f-9k4AZxnVUnWdhtrfhCFP5OySie85adMHXxbzifyk1EJi0aoG9uspQtbTDuo9KXjkmc4f4pBJs0rCCluyU9NKbc/s1600/6666.png" imageanchor="1" style="clear: left; float: none; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="256" data-original-width="376" height="272" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA1trTiqFIfytfIf9btyz5UFJr5rz3l3cg2rh1dgIahKIYuMDQ1K9-f-9k4AZxnVUnWdhtrfhCFP5OySie85adMHXxbzifyk1EJi0aoG9uspQtbTDuo9KXjkmc4f4pBJs0rCCluyU9NKbc/s400/6666.png" width="400" /></a></div>
<br />
<b>Employer - Former Employer</b><br />
When Contact reassigned to other Account, insert 2 "Who Know Who" records - "Employer" and "Former Employer":
Account is Former Employer of Contact:
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiu3zgylas8xzRIg0vYZDNvqzIOLvWeisld5WbS9QvQZtu15oMScf3xyXZzAFaalp9O-LeJJku6LNInA4Rx8qwdK_c67vlFHDdNNG9ZMQQLGjZxi-M0KJES3hJQwGLoki6VO5dMXOHWflfU/s1600/7777.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="169" data-original-width="875" height="94" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiu3zgylas8xzRIg0vYZDNvqzIOLvWeisld5WbS9QvQZtu15oMScf3xyXZzAFaalp9O-LeJJku6LNInA4Rx8qwdK_c67vlFHDdNNG9ZMQQLGjZxi-M0KJES3hJQwGLoki6VO5dMXOHWflfU/s640/7777.png" width="580" /></a></div>
In left column information about Contact: current Contact's Company(Account field), and date of termination in previous company.
Reverse record:<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLbIqz6T_iQ0XncN52yksc5cU1NsSL8XT7Qvt7XpTP5BhfTWr1sGyGzdsF3FgXxyDpKTYTU8jzU0ni00PE492cB6EOO4Sko3Dvfa_tAmTQFN7nggwhYZA71sHyWpixgteXNeu_4gaYa5fI/s1600/1111.png" imageanchor="1"><img border="0" data-original-height="158" data-original-width="859" height="98" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLbIqz6T_iQ0XncN52yksc5cU1NsSL8XT7Qvt7XpTP5BhfTWr1sGyGzdsF3FgXxyDpKTYTU8jzU0ni00PE492cB6EOO4Sko3Dvfa_tAmTQFN7nggwhYZA71sHyWpixgteXNeu_4gaYa5fI/s640/1111.png" width="580" /></a>
In this way, information about Account's former employees stores in "Who Knows Who" related list.
The account is the employer of Contact:
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhSABJ5fZtM_qN8tf6kzMhFPlkVH9UYCQah0dZRCZ8vPRUmcw_H7np-WvPvG6qgYbh2A1lrGuy47iZLvNbFdpSvSRJCWbbsOdIjeehqY1QaYK1sWGB5XqTmSvYmDRGtla7hmG4oVdp4jkjB/s1600/2222.png" imageanchor="1"><img border="0" data-original-height="129" data-original-width="849" height="77" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhSABJ5fZtM_qN8tf6kzMhFPlkVH9UYCQah0dZRCZ8vPRUmcw_H7np-WvPvG6qgYbh2A1lrGuy47iZLvNbFdpSvSRJCWbbsOdIjeehqY1QaYK1sWGB5XqTmSvYmDRGtla7hmG4oVdp4jkjB/s640/2222.png" width="580" /></a>
Current Account stores info about former employer of Contact in "Who Knows Who" related list:
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiY3Kq0pRkfmrIy4EBCN4A-NjbE9I84zOgtuCFxXJ0O73KDAbSXChmGpJkOhyDsfZgFw1rR0nB5S9FR9kq8cFucoPynWK7nH15Ur7ZlJ_dQnbfvIebxFCfjpmv5WKGyg567RrruK33EVEnW/s1600/3333.png" imageanchor="1"><img border="0" data-original-height="113" data-original-width="1032" height="60" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiY3Kq0pRkfmrIy4EBCN4A-NjbE9I84zOgtuCFxXJ0O73KDAbSXChmGpJkOhyDsfZgFw1rR0nB5S9FR9kq8cFucoPynWK7nH15Ur7ZlJ_dQnbfvIebxFCfjpmv5WKGyg567RrruK33EVEnW/s640/3333.png" width="580" /></a>
<br />
<div style="font-size: 1rem;">
<b>Manually scenarios</b></div>
If you want to create a relation between some Contacts or Account, you can manually to insert "Who Knows Who" record and select kind of relationship in "Account relationship" and "Contact relationship" picklists.</div>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-45650450724190212402017-01-16T14:16:00.001-08:002017-01-16T14:18:03.270-08:00Pre-poulate the "Name" field when creating a new record and update after saving<div dir="ltr" style="text-align: left;" trbidi="on">
Sometimes there is a need to not use Auto-number as a record name. Instead, you want to pre-populate a Name field when creating a new record with a default value and update the Name based on some rules.<br />
Unfortunately, it's impossible to create your own New custom button. The only option to achieve the requirement is to override the New button with custom VF page. Further please find solution description.<br />
<br />
<h4>
#Edit a new button - override with Visualforce page</h4>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZ8FwWJ5CV6g9b_IAKjKJfJsTWkk_8Bk48Rfro3n_GiV4YAl78eeS7GK9YYwvJXTIiPDaxoozI8hZnRBjQx_wjxs6gIjMGdNhYwLnRzNPAm9U1M47ZJKnNc2kRH60resQEJMeGX3owppRv/s1600/override.png" style="clear: left; float: none; margin-bottom: 1em; margin-right: 1em;"><img border="0" height="85" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZ8FwWJ5CV6g9b_IAKjKJfJsTWkk_8Bk48Rfro3n_GiV4YAl78eeS7GK9YYwvJXTIiPDaxoozI8hZnRBjQx_wjxs6gIjMGdNhYwLnRzNPAm9U1M47ZJKnNc2kRH60resQEJMeGX3owppRv/s400/override.png" width="400" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
Visualforce page was created which redirects to a hack URL with parameter "Name=Partner+Plan".<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">
<apex:page standardController="Partner_Plan__c">
<script>
window.top.location.href = '/a0w/e?nooverride=1&retURL=%2Fa0w%2Fo&Name=Partner+Plan';
</script>
</apex:page>
</code>
</pre>
<h4>
#To prevent multiple redirect it's necessary to add parameter "nooverride=1".</h4>
After clicking on the button "New" you will be redirected to the page with these paramerers and field "Name" will be filled.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlW7ZWSPn4cE2bN4s7dsSTrgfEgJZP5VECGz1YpNtwwPLnGOJmsGN1W7dkrGWdIw51YXgMXzeML8P87pP2InIjvPXeExiD6TdbxCvXH-ncbsZ8aTIdQYvxcS16eN_0aQxfOKn7YzSl4cT9/s1600/partner_plan.png" style="clear: left; float: none; margin-bottom: 1em; margin-right: 1em;"><img border="0" height="158" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlW7ZWSPn4cE2bN4s7dsSTrgfEgJZP5VECGz1YpNtwwPLnGOJmsGN1W7dkrGWdIw51YXgMXzeML8P87pP2InIjvPXeExiD6TdbxCvXH-ncbsZ8aTIdQYvxcS16eN_0aQxfOKn7YzSl4cT9/s320/partner_plan.png" width="320" /></a></div>
Partner Plan Name required update after saving. It should be consists "Partner Plan" + "Partner Name" + "Year".<br />
<br />
<b>#This was done using Workflow Rule.</b><br />
<ul>
<li>Rule Criteria: true;</li>
<li>Evaluation Criteria: Evaluate the rule when a record is created, and every time it's edited</li>
<li>Immediate Workflow Action: field update;</li>
<li>Formula value: "Partner Plan - " + Practice_Group_Lead__r.FirstName + " " + Practice_Group_Lead__r.LastName + " " - " + TEXT(Plan_Year__c)</li>
</ul>
After saving:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKka7CjzMN7CI__zBI_RqO3f42YdJrsytVNWTK83bNBhQVP38anc_iNoMWIReiP1WxieJihCtsILnZMd90ZIqp55vTvFb9ThN78YsRgcLF6WHO8YhouQbOAMypmpDKXqTe8r1AS8h123LC/s1600/1111.png" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" height="81" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKka7CjzMN7CI__zBI_RqO3f42YdJrsytVNWTK83bNBhQVP38anc_iNoMWIReiP1WxieJihCtsILnZMd90ZIqp55vTvFb9ThN78YsRgcLF6WHO8YhouQbOAMypmpDKXqTe8r1AS8h123LC/s320/1111.png" width="320" /></a></div>
</div>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-55153074918913628262016-08-22T06:06:00.002-07:002016-08-22T06:06:55.754-07:00Storage Usage issue - remove legacy and redundant Task recordsSalesforce Orgs with long usage history (more than 3 years) and small Data Storage limits (~1GB) could suffer storage limit issues (> 100%).<br />
<br />
In most cases Task object brings the issues with Data Storage overuse.<br />
<br />
Unfortunately as per SFDC limitation there is no quick way to delete/remove Tasks created 2 years ago and backwards.<br />
<br />
However there is a workaround using custom reports/data
export and data loader. Please do not hesitate to <a href="http://websolo.ca/contact_us/"><b>contact us</b></a> if you will need help to leverage this approach.<br />
<br />
<b style="font-style: italic;">STEP 1: </b><i>Export Data</i><br />
Go to the Setup -> Data Management -> Data Export and click Export Now or Schedule Export button.<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkXrc-A1yd0oLwsjFMBvFuOcDDLQQHKp2MrGXDPhoExD5-g_tmTfRRAgJVzZEru2S_vTQaUburrWYFZo6ZfrD0vv7v2og6LJ2CctIpKRqDmwhvkg5I6bkBtMHgn2nU7QQ47_BA5bdBzdQ/s1600/blog.jpg" imageanchor="1"><img border="0" height="275" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkXrc-A1yd0oLwsjFMBvFuOcDDLQQHKp2MrGXDPhoExD5-g_tmTfRRAgJVzZEru2S_vTQaUburrWYFZo6ZfrD0vv7v2og6LJ2CctIpKRqDmwhvkg5I6bkBtMHgn2nU7QQ47_BA5bdBzdQ/s400/blog.jpg" width="400" /></a><br />
<br />
Tick Task then click Start Export.<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEii1sdKdTQT77HiQ5GBkbE0ib7bZ6QxUNu_DSrPw0P9GvUt5aWyBfokdyC2z011PCFPZpsmPARmNABn_UTHH2sycR2O9h-7x35SjzsnkDtPLKBFmouaobv_ViEC6VQ99dbkMMBankdsQYA/s1600/blog2.jpg" imageanchor="1"><img border="0" height="247" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEii1sdKdTQT77HiQ5GBkbE0ib7bZ6QxUNu_DSrPw0P9GvUt5aWyBfokdyC2z011PCFPZpsmPARmNABn_UTHH2sycR2O9h-7x35SjzsnkDtPLKBFmouaobv_ViEC6VQ99dbkMMBankdsQYA/s400/blog2.jpg" width="400" /></a><br />
<br />
As a result you will be able to download .zip file which contains .csv with tasks data.<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgIdOJs6ImufWwMZr4VCIYlv4HqU1h8_a9C2CBguDTqrLzhh3ovalEz2Ju9lf4fVbs44hGdDseRgYV4OVZTxITBkEUlUfzqVzMVm6pTrxfACv_AMsz7b2aHgPS8BRqqQOpH9g5eGDW7nM/s1600/blog3.jpg" imageanchor="1"><img border="0" height="267" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgIdOJs6ImufWwMZr4VCIYlv4HqU1h8_a9C2CBguDTqrLzhh3ovalEz2Ju9lf4fVbs44hGdDseRgYV4OVZTxITBkEUlUfzqVzMVm6pTrxfACv_AMsz7b2aHgPS8BRqqQOpH9g5eGDW7nM/s400/blog3.jpg" width="400" /></a><br />
<br />
<br />
<b style="font-style: italic;">STEP 2: Do clean up using Data Loader</b><br />
Open as excel file and sort data by created date, leave id of tasks which needed to delete. Then use Data Loader to delete these tasks<br />
<br />
Approach is a little bit time consuming but works.WShttp://www.blogger.com/profile/14116271758608639764noreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-23585750076163293322016-07-13T07:22:00.001-07:002016-07-13T07:22:57.838-07:00Native fields in Lead mapping for conversionFor Leads conversion process Salesforce provides out-of-box functionality for mapping Lead fields to Account/Contact/Opportunity.<br />
<br />
Unfortunately there is a limitations - you can't map native Leads fields to target object's native field. For instance you can't map value from LeadSource ("native" Lead field) to AccountSource ("native" Account field).<br />
<br />
Here is one of the possible ways to workaround the limitation. Solution could be modified based on particular business requirements. Please do not hesitate to <a href="http://websolo.ca/contact_us/"><b>contact us</b></a> if you want to leverage this solutions in your Saleforce instance.<br />
<br />
<b style="font-style: italic;">STEP 1: </b><i>Create custom Formula(Text) field for Lead object</i><br />
<b>Name</b>: LeadSourceCustom<br />
<b>Formula</b>: <i>TEXT(</i>LeadSource<i>)</i><br />
<i><br /></i>
<b style="font-style: italic;">STEP 2: </b><i>Create custom Text(100) field for Account object</i><br />
<b>Name</b>: AccountSourceCustom<br />
<br />
<b style="font-style: italic;">STEP 3: </b><i>Amend Lead fields mapping to write value from LeadSourceCustom to AccountSourceCustom.</i><br />
<br />
<b style="font-style: italic;">STEP 4: </b><i>Create simple Trigger on Account object</i><br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : Websolo inc.
Date : 07/2016
Description :
Update History :
*/
trigger AccountSourceTrigger on Account (before update,before insert) {
for(Account a: Trigger.new){
if(a.AccountSourceCustom__c != null && a.AccountSourceCustom__c != '' && (a.AccountSource == null || a.AccountSource == '')){
a.AccountSource = a.AccountSourceCustom__c;
}
}
}
</code></pre>
<br />
As a result this trigger will update native AccountSource field with value from "native" LeadSource field.<br />
<br />
<b>EXTRA NOTE:</b><br />
We can't use WorkFlow as a workaround because AccountSource field is picklist and when we create workflow action to update this field we can write only specific value from the picklist.Anonymoushttp://www.blogger.com/profile/06829141799899469516noreply@blogger.com0Toronto, ON, Canada43.653226 -79.38318429999998243.285985499999995 -80.028631299999986 44.0204665 -78.737737299999978tag:blogger.com,1999:blog-826409388179453463.post-10884054680033285022016-04-01T09:17:00.000-07:002016-11-16T07:52:31.189-08:00Account Contact Roles on Contact layoutSalesforce has Contact Roles feature. Contact Role(s) could be applied for any Account and appear on Account layout as a related list. These Roles represent the role(s) that Contact(s) from the same or any other Account(s) plays in the Account. Such Roles could be also specified for Case, Contract, or Opportunity. When Contact roles defined, your team has more information to determine who to contact in particular circumstances.<br />
<br />
Unfortunately Salesfroce does not provide any out-of-box tool to let User to see in which Account(s) the particular Contact involved. Means on Contact layout User can's see in which Account the Contact plays "Contact Role".<br />
<br />
We want to share pretty easy solution to provide your Users such capability. Solution could be modified based on particular business requirements. Please do not hesitate to <a href="http://websolo.ca/contact_us/"><b>contact us</b></a> if you want to leverage this solutions in your Salesforce instance.<br />
<br />
<i><b>STEP 1: </b>Create VF page and APEX controller </i><br />
<br />
First you need to create custom Visualforce page which will display list of related to Contact Accounts and Contact Roles. Page's controller will consist of SOQL SELECT related Account for current Contact. In our example we build Contact Roles for Account.<br />
<span style="background-color: transparent; line-height: 11.25pt;"><i style="font-weight: bold;"><br />VF page: </i><b>ContactAccountsRolesDetails</b></span>
<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : WebSolo Inc.
Date : 03.2016
Description : VF page "ContactAccountsRolesDetails"
History :
-->
<apex:page standardController="Contact" extensions="ContactAccountsRolesDetailsContrExt" sidebar="false" showHeader="false" cache="false">
<style>
ul {
margin-left: 0;
padding-left: 0;
}
li {
list-style-type: none;
}
</style>
<apex:pageBlock >
<apex:Messages />
<apex:pageBlockTable value="{!listAccountContactRole}" var="acr" rendered="{!records}">
<apex:column HeaderValue="Account Name">
<!--<apex:outputText value="{!acr.Account.Name}"/>-->
<a href="../{!acr.AccountId}" target="_parent"><apex:outputText value="{!acr.Account.Name}"/></a>
</apex:column>
<apex:column HeaderValue="Role">
<apex:outputText value="{!acr.Role}"/>
</apex:column>
<apex:column HeaderValue="Primary">
<apex:outputField value="{!acr.IsPrimary}"/>
</apex:column>
</apex:pageBlockTable>
</apex:pageBlock>
</apex:page>
</code></pre>
<br />
<span style="background-color: transparent; line-height: 11.25pt;"><i style="font-weight: bold;"><br />Controller: </i><b>ContactAccountsRolesDetailsContrExt</b></span>
<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">
/*
Version : 1.0
Company : Websolo inc.
Date : 03/2016
Description :
Update History :
*/
public class ContactAccountsRolesDetailsContrExt
{
public Id idContact;
public Boolean records {get; set;}
public List<accountcontactrole> listAccountContactRole {get; set;}
public ContactAccountsRolesDetailsContrExt(ApexPages.StandardController Controller)
{
records = false;
idContact = Controller.getRecord().id;
listAccountContactRole = [SELECT id, AccountId, Account.Name, ContactId, Role, IsPrimary FROM AccountContactRole WHERE ContactId =: idContact];
if (listAccountContactRole.size() != 0)
{
records = true;
}
else
{
Apexpages.Message noRecords = new Apexpages.Message(ApexPages.severity.INFO, 'No records to display');
Apexpages.addMessage(noRecords);
}
}
}
</accountcontactrole></code></pre>
<br />
<i><b>STEP 2: </b>Add VF page to layout</i><br />
<i><br /></i>
When created VF page could be added to Contact Layout as in example below.<br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQhJI4KFHe-aOoyKUC2rk_gLvrrHcEWb4CbEYuJFs7dvDSrxkvxNPo4Iwm3Wkh4o2epyBtONKTbSZ66iusm_ggN-_pNhmvMTshtpUSoTVzhfKMy-JxLB-P0Yen0rksrbdoiYZyH2lln5vA/s1600/Image+8.png" imageanchor="1"><img border="0" kasperskylab_antibanner="on" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQhJI4KFHe-aOoyKUC2rk_gLvrrHcEWb4CbEYuJFs7dvDSrxkvxNPo4Iwm3Wkh4o2epyBtONKTbSZ66iusm_ggN-_pNhmvMTshtpUSoTVzhfKMy-JxLB-P0Yen0rksrbdoiYZyH2lln5vA/s400/Image+8.png" width="570" /></a><br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5gDWZV0Cxliy415G7nR688gEVKGY5UQ1GdB1FXuMLRmiwXuTyMe8vLxjTtJAG-y8-mS9mfpRW1Brpmmr1fzRbtr2DAJ3-rSqe9zQqzScSi5wbv3kYasqFL2T8mi8XX11Kqh6BMcOdg8EB/s1600/Image+11.png" imageanchor="1"><img alt="" border="0" kasperskylab_antibanner="on" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5gDWZV0Cxliy415G7nR688gEVKGY5UQ1GdB1FXuMLRmiwXuTyMe8vLxjTtJAG-y8-mS9mfpRW1Brpmmr1fzRbtr2DAJ3-rSqe9zQqzScSi5wbv3kYasqFL2T8mi8XX11Kqh6BMcOdg8EB/s1600/Image+11.png" title="" width="570" /></a><br />
<br />
<br />WShttp://www.blogger.com/profile/14116271758608639764noreply@blogger.com0Toronto, ON, Canada43.653226 -79.38318429999998243.285985499999995 -80.028631299999986 44.0204665 -78.737737299999978tag:blogger.com,1999:blog-826409388179453463.post-19338764195223173212016-03-04T06:23:00.002-08:002016-11-16T07:52:02.226-08:00Disable and animate APEX command button after click to avoid double submission<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
There is wide known scenario when click to APEX command button on custom VF page would take quite long time to perform calculation on server side and return the result back to UI. User may try to click the button again which will result in unexpected and undesired consequences. Especially if click to the button invokes DML operations.<br />
<div>
<br /></div>
<div>
There are a few workarounds to avoid such scenarios. In this post you will the most popular workarounds with analysis about their <b>PROS </b>and <b>CONS</b>.</div>
<div>
<div>
<br /></div>
Solution could be modified based on particular business requirements. Please do not hesitate to <a href="http://websolo.ca/contact_us/"><b>contact us</b></a> if you want to leverage any of this solutions in your Salesforce instance.<br />
<br />
<b>Notes related to all examples below:</b><br />
<br />
<ul>
<li>As a $Resource.statusbar you can use any .gif animated "in progress" image</li>
<li>In all demos we used external controller to build mathematic curves to simulate server activity</li>
</ul>
<div>
<h4>
<span style="color: #e69138;">
#1 Disabling of APEX command button using jQuery. Replacing button with animated GIF</span></h4>
<div>
Disabling the button once the user clicks the button could be the straight forward solution. In this solution we will replace the button once the user clicks the button using jQuery<br />
<div class="MsoNormal" style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 12pt;">
<span style="background-color: transparent; line-height: 11.25pt;"><i style="font-weight: bold;"><br />VF page: </i><b>DisableApexCommandBut</b></span><b>ton to demo disable Button</b></div>
</div>
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : WebSolo Inc.
Date : 03.2016
Description : VF page "DisableApexCommandButton" to demo disable Button
History :
-->
<apex:page sidebar="false" showheader="false" cache="false" expires="0" controller="DisableApexCommandButtonContr">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<apex:composition template="{!$Site.Template}">
<apex:define name="body">
<apex:form style="width:550px;" id="f1">
<apex:outputPanel >
<apex:commandButton style="margin-top:20px;margin-left:250px;margin-bottom:25px;height:30px;font-size:12px" styleClass="b1" value="Generate" status="status" action="{!run}" rerender="f1"/>
<div style="display: none; margin-left: 250px;" class="statusbardiv">
<apex:image width="70px" url="{!$Resource.statusbar}" />
</div>
<apex:selectRadio value="{!chart}">
<apex:selectOptions value="{!items}"/>
</apex:selectRadio><p/>
<apex:actionStatus id="status" onstart="$('.statusbardiv').css('display','block');$('.b1').css('display','none');" onstop="$('.statusbardiv').css('display','none');$('.b1').css('display','block');" />
<div>
<apex:chart height="300" width="500" data="{!data}">
<apex:legend position="top"/>
<apex:axis title="Y" type="Numeric" position="left" grid="true" fields="valY"/>
<apex:axis title="X" type="Numeric" position="bottom" fields="valX" />
<apex:lineSeries title="{!chart}" axis="left" xField="valX" yField="valY"/>
</apex:chart>
</div>
</apex:outputPanel>
</apex:form>
</apex:define>
</apex:composition>
</apex:page>
</code></pre>
<b><br /></b>
<b>PROS</b>: Easy to implement. Looks good.<br />
<b>CONS</b>: Requires jQuery library.</div>
<div style="text-align: center;">
<button onclick="window.open('http://tvad-developer-edition.na34.force.com/DisableApexCommandButton', '','scrollbars=yes,location=no,resizable=no,status=no,toolbar=no,menubar=no, width=' + 700 + ',height=' + 500 + ',left=' + ((window.innerWidth - 700)/2) + ',top=' + ((window.innerHeight - 500)/2) );" type="button">DEMO #1</button>
</div>
<br />
<h4>
<span style="color: #e69138;">
#2 Call custom div as an overhead "modal window"</span></h4>
<div class="SFDCH1">
<span lang="EN-US"><o:p></o:p></span></div>
<div>
Another solution would be not try to "disable" buttons, but to call custom modal window which will prevent user from multiple clicks and multiple form submission.<br />
<br />
<b style="line-height: 11.25pt;"><i>VF page: </i>DisableApexCommandButton</b></div>
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : Websolo inc.
Date : 03/2015
Description : Call custom div as an overhead "modal window"
Update History :
-->
<apex:page sidebar="false" showheader="false" cache="false" expires="0" controller="DisableApexCommandButtonContr">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<style>
.disabledbutton {
pointer-events: none;
opacity: 0.4;
background-color:#E1E1E1;
}
</style>
<apex:composition template="{!$Site.Template}">
<apex:define name="body">
<apex:form style="width:550px;" id="f1">
<apex:actionStatus id="status" onstart="$('#mydiv').addClass('disabledbutton');$('.statusbardiv').css('display','block');" onstop="$('#mydiv').removeclass('disabledbutton');$('.statusbardiv').css('display','none');" />
<div style="display: none; position: fixed; z-index: 999; margin-left: 250px; margin-top: 200px" class="statusbardiv">
<apex:image width="70px" url="{!$Resource.statusbar2}" />
</div>
<div id="mydiv">
<apex:outputPanel >
<apex:commandButton style="margin-top:20px;margin-left:250px;margin-bottom:25px;height:30px;font-size:12px" styleClass="b1" value="Generate" status="status" action="{!run}" rerender="f1"/>
<apex:selectRadio value="{!chart}">
<apex:selectOptions value="{!items}"/>
</apex:selectRadio><p/>
<div>
<apex:chart height="300" width="500" data="{!data}">
<apex:legend position="top"/>
<apex:axis title="Y" type="Numeric" position="left" grid="true" fields="valY"/>
<apex:axis title="X" type="Numeric" position="bottom" fields="valX" />
<apex:lineSeries title="{!chart}" axis="left" xField="valX" yField="valY"/>
</apex:chart>
</div>
</apex:outputPanel>
</div>
</apex:form>
</apex:define>
</apex:composition>
</apex:page>
</code></pre>
<b><br /></b>
<b>PROS: </b>Works better in some cases<br />
<b>CONS: </b>Not visually perfect for some users who not used to such approach. Also requires jQuery library.<br />
<div style="text-align: center;">
<button onclick="window.open('http://tvad-developer-edition.na34.force.com/DisableApexCommandButton_v2', '','scrollbars=yes,resizable=no,status=no,toolbar=no,menubar=no, width=' + 700 + ',height=' + 500 + ',left=' + ((window.innerWidth - 700)/2) + ',top=' + ((window.innerHeight - 500)/2) );" type="button">DEMO #2</button></div>
<br />
<h4>
<span style="color: #e69138;">
#3 Disabling of APEX command button using apex:facet method</span></h4>
<div>
In this solution we will replace the button with "Processing..." text once the user clicks the button<br />
<div class="MsoNormal" style="background: white; line-height: 11.25pt; margin-bottom: 12.0pt;">
<b style="background-color: transparent;"><i><br />VF page: </i>DisableApexCommandButton</b></div>
</div>
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : Websolo inc.
Date : 03/2015
Description : Disabling of APEX command button using apex:facet method
Update History :
-->
<apex:page sidebar="false" showheader="false" cache="false" expires="0" controller="DisableApexCommandButtonContr">
<apex:composition template="{!$Site.Template}">
<apex:define name="body">
<apex:form style="width:550px;" id="f1">
<apex:outputPanel >
<apex:actionStatus id="status">
<apex:facet name="stop">
<apex:commandButton style="margin-top:20px;margin-left:250px;margin-bottom:25px;height:30px;font-size:12px;width:90px" action="{!run}" status="status" value="Generate" disabled="false" rerender="f1"/>
</apex:facet>
<apex:facet name="start">
<apex:commandButton style="margin-top:20px;margin-left:250px;margin-bottom:25px;height:30px;font-size:12px;width:90px" action="{!run}" status="status" value="Processing..." disabled="true"/>
</apex:facet>
</apex:actionStatus>
<apex:selectRadio value="{!chart}">
<apex:selectOptions value="{!items}"/>
</apex:selectRadio><p/>
<div>
<apex:chart height="300" width="500" data="{!data}">
<apex:legend position="top"/>
<apex:axis title="Y" type="Numeric" position="left" grid="true" fields="valY"/>
<apex:axis title="X" type="Numeric" position="bottom" fields="valX" />
<apex:lineSeries title="{!chart}" axis="left" xField="valX" yField="valY"/>
</apex:chart>
</div>
</apex:outputPanel>
</apex:form>
</apex:define>
</apex:composition>
</apex:page>
</code></pre>
<b><br /></b>
<b>PROS</b>: Efficient and user friendly<br />
<b>CONS</b>: None<br />
<div style="text-align: center;">
<button onclick="window.open('http://tvad-developer-edition.na34.force.com/DisableApexCommandButton_v3', '','scrollbars=yes,resizable=no,status=no,toolbar=no,menubar=no, width=' + 700 + ',height=' + 500 + ',left=' + ((window.innerWidth - 700)/2) + ',top=' + ((window.innerHeight - 500)/2) );" type="button">DEMO #3</button></div>
</div>
</div>
WShttp://www.blogger.com/profile/14116271758608639764noreply@blogger.com7Toronto, ON, Canada43.653226 -79.38318429999998243.285985499999995 -80.028631299999986 44.0204665 -78.737737299999978tag:blogger.com,1999:blog-826409388179453463.post-4224831754782410322015-12-27T06:02:00.003-08:002016-01-01T15:55:43.833-08:00Self recurring class to create test records in Salesforse objectWhile working on <a href="http://salesforce.websolo.ca/2015/06/Salesforce-Round-Robin-Assignment-Remedyforce.html" target="_blank">Round Robin Incidents Assignment Routine for BMC Remedyforce</a> we've built a tool to auto create test Incident records. This tool creates new record every one minute simulating real environment.<br />
<br />
After minor modifications the same solution could be used for similar purposes to simulate creating records in standard or custom from SFDC UI. Below we provided example of self recurring class to create test Contact records.<br />
<br />
The code has comments however please feel free to <b><a href="http://www.websolo.ca/contact_us/" target="_blank">Contact Us</a></b> if you have any questions.<br />
<br />
<b><i>APEX Class: </i></b>AutoContactsCreator<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : WebSolo inc.
Date : 12.2015
Description : APEX class to automatically create new test Contact record (with some test data) each 5 minutes
History :
*/
global class AutoContactsCreator implements Schedulable{
//Execute method
global void execute(SchedulableContext SC) {
//Code to check if test Account with name 'Test Account' exists and to create one if not.
List<account> acc = [SELECT id FROM Account WHERE Name = 'Test Account' limit 1];
if(acc.size() == 0){
Account NewAcc = new Account();
NewAcc.Name = 'Test Account';
insert NewAcc;
acc.add(NewAcc);
}
//Code to check if test Contacts (created before with name AutoContact) exist and to identify the most recent Name (to auto increase name for new one).
Contact con = new Contact();
con.AccountId = acc[0].id;
//Get next number of autocontact
//Get last AutoContact record and parce LastName value
List<contact> lastCon = [SELECT Name FROM Contact WHERE LastName LIKE 'AutoContact%' ORDER BY CreatedDate DESC LIMIT 1];
//If exist, substring most recent name OR start form 1
Integer NumOfNextAutoCon;
if(lastCon.size() != 0){
String nameOfLastAutoCon = lastCon[0].Name;
NumOfNextAutoCon = Integer.valueOf(nameOfLastAutoCon.substringAfter('AutoContact')) + 1;
}
else{
NumOfNextAutoCon = 1;
}
//Prepare values for First/Last Name, Phone, Email
con.FirstName = 'Test';
con.LastName = 'AutoContact' + NumOfNextAutoCon;
//Generate random number for phone from 1000000 to 9999999
Integer uniqueVal = Math.round(Math.random()*1000000) + 999999 ;
con.Phone = '416' + uniqueVal;
con.Email = 'AutoContact' + NumOfNextAutoCon + '@TestAccount.com';
insert con;
//This code section will schedule next class execution in 5 minutes from now
datetime nextScheduleTime = system.now().addMinutes(1);
string minute = string.valueof(nextScheduleTime.minute());
string second = string.valueof(nextScheduleTime.second ());
string cronvalue = second+' '+minute+' * * * ?';
string jobName = 'AutoContactsCreator ' + nextScheduleTime.format('hh:mm');
AutoContactsCreator p = new AutoContactsCreator();
system.schedule(jobName, cronvalue , p);
//This code section to be used to abort auto-scheduled job
system.abortJob(sc.getTriggerId());
}
//Method to start shedule job form console
public void startJob(){
datetime nextScheduleTime = system.now().addMinutes(1);
string minute = string.valueof(nextScheduleTime.minute());
string second = string.valueof(nextScheduleTime.second ());
string cronvalue = second+' '+minute+' * * * ?' ;
string jobName = 'AutoContactsCreator ' + nextScheduleTime.format('hh:mm');
AutoContactsCreator p = new AutoContactsCreator();
system.schedule(jobName, cronvalue , p);
}
//Method to abort shedule job (with 'AutoContactsCreator' name) form console
public void deleteJob(){
CronTrigger job = [SELECT Id, CronJobDetail.Id, CronJobDetail.Name, CronJobDetail.JobType FROM CronTrigger where CronJobDetail.Name LIKE 'AutoContactsCreator%'];
system.abortJob(job.Id);
}
}
</contact></account></code></pre>
<br />
<b><i>APEX Class: </i></b>AutoContactsCreator_TESTclass<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : WebSolo inc.
Date : 12.2015
Description : APEX test coverage class for AutoContactsCreator class
History :
*/
@isTest
private class AutoContactsCreator_TESTclass {
static testMethod void myUnitTest() {
AutoContactsCreator autoCrCon = new AutoContactsCreator();
autoCrCon.StartJob();
autoCrCon.deleteJob();
system.schedule('Test', '0 0 * * * ?' , autoCrCon);
}
}
</code></pre>
<br />
<h4>
<b>SFDC Developer Console commands to use with AutoContactsCreator class</b></h4>
<b>To execute self recurring class</b><br />
AutoContactsCreator autoCrCon = new AutoContactsCreator();<br />
autoCrCon.StartJob();<br />
<br />
<b>To abort self recurring class</b><br />
AutoContactsCreator autoCrCon = new AutoContactsCreator();<br />
autoCrCon.deleteJob();<br />
<b>Note</b>: You also can abort the job manually here Setup -> Jobs -> Scheduled Jobs<br />
<br />
<b>Helpful command to DELETE all previously created by self recurring class test records</b><br />
DELETE [SELECT id FROM Contact WHERE LastName LIKE 'AutoContact%'];<br />
<br />WShttp://www.blogger.com/profile/14116271758608639764noreply@blogger.com2Toronto, ON, Canada43.653226 -79.38318429999998243.285985499999995 -80.028631299999986 44.0204665 -78.737737299999978tag:blogger.com,1999:blog-826409388179453463.post-26420872386865544342015-06-08T10:01:00.004-07:002015-07-18T10:52:49.560-07:00Custom Salesforce Round Robin Incidents Assignment Routine for BMC Remedyforce<b>Salesforce Round Robin Assignment Routine for BMC Remedyforce </b>was built for companies wanting to automatically distribute and assign incoming Incidents to the Members of different Support Teams (Groups) depending on Incident creation time and Group's Time Zone.<br />
<br />
We have multiple Assignment Routine implementations for different businesses who use <a href="http://www.bmc.com/it-solutions/remedyforce.html" rel="nofollow" target="_blank">BMC Remedyforce</a> and Salesfroce. Armed with multiple extra features <b>Round Robin Incidents Assignment Routine for BMC Remedyforce</b> works extremely well and deserved great references.<br />
<br />
For instance please take a look at this reference - shared by our client who we recently helped with the RemedyForce Round Robin customization:<br />
<a href="https://communities.bmc.com/ideas/3675#comment-62436" rel="nofollow" target="_blank">https://communities.bmc.com/ideas/3675#comment-62436</a><br />
<br />
Please do not hesitate to <b><a href="http://www.websolo.ca/contact_us/" target="_blank">Contact Us</a></b> if you have any questions or wish to add this extremely useful functionality to your <b>Salesforce </b>with<b> Remedyforce</b> instance. We'll happy to reply with answers, licensing options and pricing details.<br />
<br />
<br />
<b><span style="font-size: large;">Round Robin for BMC Remedyforce<br />
Most important Functional Features:</span></b><br />
- "Assignment Groups" Tab (Console) to manage multiple Groups of Users involved in "Round Robin" assignment routine and monitor their current indicators such as Time Zone, Assignment Score, Status, etc.<br />
- Capability to enable/disable particular Group(s) from round robin cycle<br />
- Capability to assign Members to Groups based on Shifts ("Assignment Time Frame" feature)<br />
- Capability to activate/deactivate Group's Members from round robin cycle if the Member is sick or on vacation<br />
- Potential capability for Members to "check out/check in" from/to round robin cycle ("Self Management" feature)<br />
- Capability to set Share for each Member in the Group - as a % of the total records received from assignment Queue during the day ("Not Equal Share" feature)<br />
- Reporting capabilities to help manager to track Round Robin Assignment statistics <br />
<br />
<b><span style="font-size: large;">Other Details:</span></b><br />
- 100% Native SFDC Application. Means no servers or outside services required.<br />
- Supports following Salesforce.com Editions: Enterprise, Unlimited, Developer<br />
- Instant processing time, no assignment delays<br />
<br />
Solution functionality could be amended and enhanced to meet additional particular business requirements. Here are few possible scenarios:<br />
- "Day time" Group(s) to handle the normal working day Incidents and "on call" Group for off-hours support<br />
- Assign Incidents to Group(s) designated to particular Incident Priority/Impact or any other set of custom criteria<br />
- Auto login/logout Members from/to Round Robin cycle based on their login and log out time in Salesforce. So that they wouldn't receive Incidents if they are not actually working now.<br />
<br />
<div class="separator" style="clear: both; text-align: left;">
<b><span style="font-size: large;">Screens</span></b></div>
<div class="separator" style="clear: both; text-align: left;">
<b>1) Assignment Groups Console</b><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjffJqQuaaVQWrLPmG2bzDV04vscunspbSV8fM4D9zRiOtZLFo8rhDyn1qD7Xoimmh4TTf6PIgnyGD4wUvufHsK62ENnbh0iJ2ME2z15WUmmborFZPOfKc9ET9tKMM_FyMdz3a-T8hEOWL7/s1600/RRR_01.png" imageanchor="1" style="margin-left: 0em; margin-right: 0em;"><img alt="Assignment Groups Console" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjffJqQuaaVQWrLPmG2bzDV04vscunspbSV8fM4D9zRiOtZLFo8rhDyn1qD7Xoimmh4TTf6PIgnyGD4wUvufHsK62ENnbh0iJ2ME2z15WUmmborFZPOfKc9ET9tKMM_FyMdz3a-T8hEOWL7/s1600/RRR_01.png" style="height: 217px; width: 560px;" title="Assignment Groups Console" /></a></div>
<b><br />
</b> <b>2) Assignment Group layout</b><br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlBBG_0lJiXUbWEyRZoWHH9MLHDbO1pfsOSICI3t14Y2qQMXzSc_ki6shwI8Ax8bezSEhqLx70UAPAu1gWrmbbcpAKUn8kPb5YMNMnrgkKCcjvMT6tb5XDyusEz-FnMyz6U1bLSucIKLM4/s1600/RRR_02.png" imageanchor="1" style="margin-left: 0em; margin-right: 0em;"><img alt="Assignment Group layout" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlBBG_0lJiXUbWEyRZoWHH9MLHDbO1pfsOSICI3t14Y2qQMXzSc_ki6shwI8Ax8bezSEhqLx70UAPAu1gWrmbbcpAKUn8kPb5YMNMnrgkKCcjvMT6tb5XDyusEz-FnMyz6U1bLSucIKLM4/s1600/RRR_02.png" style="height: 620px; width: 560px;" title="Assignment Group layout" /></a></div>
<br />
3<b>) Group Member layout</b><br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjc1xOZIVRqtHj3BBgC26dTE3DL7EMK1mPxwwhdSEoRS3mjNFcXYkiUOJN_s0mov02AyfVaWfaJ4OpmNHcWHRJ7HAkvw0xqZH8r1VkS8bSyELEJ5Wsrrc-7o-RacdLL51TQxPHAY5Opmy1V/s1600/RRR_03.png" imageanchor="1" style="margin-left: 0em; margin-right: 0em;"><img alt="Group Member layout" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjc1xOZIVRqtHj3BBgC26dTE3DL7EMK1mPxwwhdSEoRS3mjNFcXYkiUOJN_s0mov02AyfVaWfaJ4OpmNHcWHRJ7HAkvw0xqZH8r1VkS8bSyELEJ5Wsrrc-7o-RacdLL51TQxPHAY5Opmy1V/s1600/RRR_03.png" style="height: 301px; width: 560px;" title="Group Member layout" /></a></div>
WShttp://www.blogger.com/profile/14116271758608639764noreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-68971515012756257862015-05-14T11:58:00.000-07:002015-05-20T10:29:37.716-07:00Let your Contacts update their Email preferences using Force.com Site without authentication. Configuring the public Force.com Site.<div>
Force.com Sites is relatively known feature which allows SFDC developers to set up public-facing sites/pages hosted by Salesforce platform. Using Force.com Site you can build micro web sites with one landing page or complicated sites with multiple pages. Such sites could have complicated flows to expose/collect information from/to Salesfroce. Advanced Force.com sites could even (for instance) accept credit card payments.</div>
<div>
<br /></div>
In this post we will show how to built quite easy but useful Force.com site and page with a purpose to let your Salesforce Contacts update their Email preferences themself, without contacting your support team. I.e. unsubscribe by updating Email Opt Out checkbox in related Salesforce record.<br />
<br />
Solution could be modified based on particular business requirements.<br />
Please do not hesitate to <a href="http://websolo.ca/contact_us/"><b>contact us</b></a> if you want to leverage this solutions in your Saleforce instance.<br />
<br />
<div>
Here's how the solution flow works:<br />
<ol>
<li>SFDC (or mass mail application) sends marketing emails out to customers (SFDC Contacts).</li>
<li>Email will consist of "manage my subscription" or "unsubscribe" link. The link will be unique for each Contact and consist of "encrypted" Contact's ID. Encrypting is recommended to hide real SFDC Contatc ID (15-character) for security reasons. See the link with “hash id” (in yellow) below as an example.</li>
<li>End user clicks on that URL and opens a Force.com site page, change his Email preferences and click "Update"to commit updates to SFDC.</li>
</ol>
Unique URL in email for end customer will looks like this:<br />
<span style="background-color: white;">http://yourdomain.force.com/emailpreferences/profile/</span><span style="background-color: yellow;">fd90b5cd3bda11119f79a0c8131ab2ac7266d3f54add67f276c396e74f906da4</span><br />
<br />
<b><u>Step 0:</u> Create new trigger to populate HashId field on Contact object</b><br />
<b><br /></b>
As mentioned above we suggest to encrypt native SFDC ID to improve your Force.com site security.</div>
<div>
<div>
This technique allows to hide and not publicly expose your SFDC Contact record's ID. Instead of this Force.com site will use Crypto generated (algorithm SHA256) compact representations of the original ID value which will be "decrypted" back by URL rewriter controller class (see below).</div>
<div>
<br /></div>
First please create new HashId [HashId__c, Text (255)] field on Contact object. To make the field indexed by SFDC (and improve Force.com site response time) set External ID check box to TRUE.</div>
<div>
Then create ContactHashId trigger to populate the field with encrypted value. </div>
<div>
<br /></div>
<div>
NOTE: <i>Trigger fires on "after insert", "before update" events on Contact object. Please make sure you will "update" existing Contacts using APEX scheduled Job or DataLoader.</i></div>
<div>
<b><br /></b>
<br />
<b><i>APEX trigger: </i></b>ContactHashId<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : WebSolo inc.
Date : 05.2015
Description : trigger to populate the HashId field on Contact object (used by PublicEmailPreferencesURLRewriter class)
History :
*/
trigger ContactHashId on Contact (after insert, before update) {
// Generate hash ids for all contacts that don't have them.
List<Contact> contactsToUpdate = new List<Contact>();
for (Contact c : Trigger.new) {
if (c.HashId__c == null) {
String hashId = EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf((String)c.Id)));
if (Trigger.isInsert) {
contactsToUpdate.add(new Contact(Id = c.Id, HashId__c = hashId));
} else {
c.HashId__c = hashId;
}
}
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
</code></pre>
<br />
<b><u>Step 1:</u> Set up Unique Domain Name for your SFDC instance</b><br />
<br />
You can set up your Force.com Site by going to Setup | Develop | Sites<br />
<br />
If your company does not have Force.com Site yet, you will be able to register a Force.com domain name that you like (upon availability).<br />
<br />
NOTE: <i>Be careful when registering the domain name on your Production instance. After registration you will not be able to change it! As per Salesforce "You cannot modify your Force.com domain name after the registration process."</i><br />
<b><u><br /></u></b>
<b><u>Step 2:</u> Create Force.com site components (VF page, </b><b>APEX </b><b>controllers, template) </b><br />
<b><br /></b>
Create and save following Force.com site components.<br />
<br />
<b><i>VF page (Active Site Home Page): </i></b>PublicEmailPreferences<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : WebSolo Inc.
Date : 05.2015
Description : VF page "PublicEmailPreferences" to serve as a page in Force.com site
History :
-->
<apex:page controller="PublicEmailPreferencesController" cache="false" expires="0" showHeader="false" sidebar="false">
<style>
body { font-size: 100%; }
.contactInfo { margin: 20px 0px; }
.contentPanel { float: left; margin: 5px 0px 20px 20px; }
.contactLabel { float: left; font-weight: bold; width: 10em; }
h1 { color: #cc6611; font-size: 200%; padding-top: 25px;}
input[type="checkbox"] { height: 20px; width: 20px; }
.detailPanel { float: left; margin-left: 10px; width: 16em;margin-top:10px }
.logo { margin: 0px 100px 0px 20px; width: 250px; }
.selectionLabelPanel { float: left; margin-bottom: 10px; }
.selectionLabel { font-weight: bold; }
.selectionCheckboxPanel { float: right; }
</style>
<apex:composition template="{!$Site.Template}">
<apex:define name="logo">
<!--
You can add the logo here as a link to static resource
<apex:image styleClass="logo" value="{!$Resource.LOGO_RESOURCE_NAME}" />
-->
</apex:define>
<apex:define name="headline">
<h1>Email Opt Out Preferences</h1>
</apex:define>
<apex:define name="body">
<apex:form >
<apex:outputPanel id="leftPanel" layout="block" rendered="{!showPrefs}" styleClass="contentPanel" >
<apex:outputPanel layout="block" >
<apex:outputText value="Keep your email preferences up to date." />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="contactInfo" >
<apex:outputPanel layout="block" styleClass="contactLabel" >
<apex:outputText value="Email" />
</apex:outputPanel>
<apex:outputPanel layout="block" >
<apex:outputText value="{!contact.Email}" />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="clearBoth" />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="contactInfo" >
<apex:outputPanel layout="block" styleClass="contactLabel" >
<apex:outputText value="First Name" />
</apex:outputPanel>
<apex:outputPanel layout="block" >
<apex:outputText value="{!contact.FirstName}" />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="clearBoth" />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="contactInfo" >
<apex:outputPanel layout="block" styleClass="contactLabel" >
<apex:outputText value="Last Name" />
</apex:outputPanel>
<apex:outputPanel layout="block" >
<apex:outputText value="{!contact.LastName}" />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="clearBoth" />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="contactInfo" >
<apex:outputPanel layout="block" styleClass="clearBoth" />
</apex:outputPanel>
<apex:outputPanel layout="block" >
<apex:outputText value="To request an update to your profile information, please kindly contact our " />
<apex:outputLink target="_top" style="color:#cc6611" value="mailto:SUPPORT%40YOURCOMAPNYDOMAIN.com?subject=Please%20update%20my%20information">support team</apex:outputLink>
<apex:outputText value="." />
</apex:outputPanel>
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="clearBoth" />
<apex:outputPanel id="rightPanel" layout="block" rendered="{!showPrefs}" styleClass="contentPanel" >
<apex:outputPanel layout="block" >
<apex:outputText value="Select the checkbox if you do not want to receive emaild form us." />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="detailPanel" >
<apex:outputPanel layout="block" styleClass="selectionPanel" >
<apex:outputPanel layout="block" styleClass="selectionLabelPanel" >
<apex:outputText styleClass="selectionLabel" value="Opt Out Of Emails" />
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="selectionCheckboxPanel" >
<apex:inputCheckbox styleClass="selectionCheckbox" value="{!subEmailOptOut}" />
</apex:outputPanel>
</apex:outputPanel>
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="clearBoth" />
<apex:outputPanel id="buttonPanel" layout="block" >
<apex:actionStatus id="saveStatus">
<apex:facet name="start">
<apex:outputPanel >
Updating...&nbsp;<img src="{!$Resource.AnimatedBusy}" />
</apex:outputPanel>
</apex:facet>
<apex:facet name="stop">
<apex:commandButton action="{!updatePreferences}" rerender="messagesPanel" status="saveStatus" style="background: #cc6611; color: white; font-size: 200%; margin-top: 20px;" styleClass="updateBtn" value="Update" />
</apex:facet>
</apex:actionStatus>
</apex:outputPanel>
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="clearBoth" />
<apex:outputPanel id="messagesPanel" layout="block" style="max-width: 400px;" >
<apex:outputPanel layout="block" rendered="{!hasMessages}" style="{!messagePanelStyle}" >
<apex:repeat value="{!messages}" var="message">
<apex:outputText value="{!message}" />
</apex:repeat>
</apex:outputPanel>
</apex:outputPanel>
</apex:form>
</apex:define>
</apex:composition>
</apex:page>
</code></pre>
<br />
NOTE: <i>For $Resource.AnimatedBusy please use any animated GIF illustrate "progress"</i><br />
<i><br /></i>
<b><i>VF page controller (for Active Site Home Page): </i></b>PublicEmailPreferencesController<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : WebSolo Inc.
Date : 05.2015
Description : controller for PublicEmailPreferences VF page
Update History :
*/
public without sharing class PublicEmailPreferencesController {
public Contact contact { public get; private set; }
public Boolean subEmailOptOut { public get; public set; }
public String rsaEmail { public get; private set; }
public String rsrEmail { public get; private set; }
public List<String> messages { public get; private set; }
public Boolean hasMessages { public get {return messages != null && !messages.isEmpty();} private set; }
public String messagePanelStyle { public get {return hasErrorMessage ? ERROR_STYLE : INFO_STYLE;} private set; }
public Boolean showPrefs { public get; private set; }
private Boolean hasErrorMessage = false;
public final static String URL_PARM_ID = 'id';
public final static String INFO_STYLE = 'color: green; background-color: #efe; padding: 10px; margin: 10px; border: 1px solid green;';
public final static String ERROR_STYLE = 'color: red; background-color: #fee; padding: 10px; margin: 10px; border: 1px solid red;';
public final static String ERR_CONTACT_NOT_FOUND = 'We are unable to retrieve your information at this time.';
public final static String ERR_PREFS_UPDATE_FAILED = 'We are unable to update your preferences at this time.';
public final static String ERR_PREFS_UPDATE_SUCCEEDED = 'Your preferences have been updated.';
public PublicEmailPreferencesController() {
clearMessages();
String contactId = ApexPages.currentPage().getParameters().get(URL_PARM_ID);
System.debug('id=' + contactId);
if (contactId != null) {
try {
contact = getContactForId(contactId);
if (contact != null) {
subEmailOptOut = contact.HasOptedOutOfEmail;
showPrefs = true;
}
} catch (Exception e) {
addErrorMessage(ERR_CONTACT_NOT_FOUND);
}
} else {
addErrorMessage(ERR_CONTACT_NOT_FOUND);
}
}
private void addErrorMessage(String message) {
if (messages == null) messages = new List<String>();
messages.add(message);
hasErrorMessage = true;
}
private void addInfoMessage(String message) {
if (messages == null) messages = new List<String>();
messages.add(message);
}
private void clearMessages() {
if (messages == null) messages = new List<String>();
messages.clear();
hasErrorMessage = false;
}
private Contact getContactForId(String contactId) {
Contact contact = null;
List<Contact> contacts = [SELECT HasOptedOutOfEmail, Email, FirstName, Id, LastName FROM Contact WHERE Id = :contactId];
if (contacts.size() == 1) {
contact = contacts[0];
} else {
addErrorMessage(ERR_CONTACT_NOT_FOUND);
}
return contact;
}
public PageReference updatePreferences() {
try {
clearMessages();
contact.HasOptedOutOfEmail = subEmailOptOut;
update contact;
addInfoMessage(ERR_PREFS_UPDATE_SUCCEEDED);
} catch (Exception e) {
addErrorMessage(ERR_PREFS_UPDATE_FAILED);
}
return null;
}
}
</code></pre>
<br />
<br />
<b><i>VF page (template for site VF pages): </i></b>PublicSiteTemplate<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : WebSolo Inc.
Date : 05.2015
Description : VF page "PublicSiteTemplate" to serve as a Force.com Site Template
History :
-->
<apex:page showHeader="false" sidebar="false" id="PublicSiteTemplate" cache="false" expires="0" >
<head>
<title>FPC</title>
<style>
#fullContainer { }
#headerContainer { padding: 0px 10px; }
#logoContainer { float: left; }
#contentContainer { padding-left: 10px; }
#headlineContainer { float: left; margin-top:10px}
#footerContainer { padding-left: 10px; }
#footerbar { background-color: #cc6611; line-height: 0.5em; max-width: 650px; }
.clearBoth { clear: both; }
</style>
</head>
<body>
<div id="fullContainer">
<div id="headerContainer">
<div id="logoContainer">
<apex:insert name="logo"/>
</div>
<div id="headlineContainer">
<apex:insert name="headline"/>
</div>
<div class="clearBoth"></div>
</div>
<div id="contentContainer">
<div id="mainContent">
<apex:insert name="body"/>
</div>
<div class="clearBoth"></div>
</div>
<div id="footerContainer">
<div id="footerLinks"></div>
<div class="clearBoth"></div>
<div id="footerbar">&nbsp;</div>
<div class="clearBoth"></div>
</div>
</div>
</body>
</apex:page>
</code></pre>
<br />
<br />
<b><i>APEX class (URL rewriter): </i></b>PublicEmailPreferencesURLRewriter<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; height: 300px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : WebSolo inc.
Date : 05.2015
Description : controller to support URL Rewrite for Force.com VF page "PublicEmailPreferences"
History :
*/
global class PublicEmailPreferencesURLRewriter implements Site.UrlRewriter {
public static final String EMAIL_PROFILE_FRIENDLY = '/profile/';
public static final String EMAIL_PROFILE_VF_PAGE_BASE = '/PublicEmailPreferences';
public static final String EMAIL_PROFILE_VF_PAGE = EMAIL_PROFILE_VF_PAGE_BASE + '?' + PublicEmailPreferencesController.URL_PARM_ID + '=';
global PageReference[] generateUrlFor(PageReference[] urls) {
System.debug('generateUrlFor has been invoked for ' + urls);
PageReference[] pageRefs = new List<PageReference>();
Set<Id> cIds = new Set<Id>();
for (PageReference pRef : urls) {
String urlStr = pRef.getUrl();
if (urlStr.startsWith(EMAIL_PROFILE_VF_PAGE_BASE)) {
cIds.add(urlStr.substring(EMAIL_PROFILE_VF_PAGE.length()));
}
}
Map<Id, Contact> contactsById = new Map<Id, Contact>([SELECT Id, HashId__c FROM Contact WHERE Id IN :cIds]);
for (PageReference pRef : urls) {
String urlStr = pRef.getUrl();
pageRefs.add(
urlStr.startsWith(EMAIL_PROFILE_VF_PAGE_BASE) ?
new PageReference(EMAIL_PROFILE_FRIENDLY + contactsById.get(urlStr.substring(EMAIL_PROFILE_VF_PAGE.length())).HashId__c) :
pRef
);
}
return pageRefs;
}
global PageReference mapRequestUrl(PageReference url) {
PageReference pageRef = null;
String urlStr = url.getUrl();
System.debug('mapping url=' + urlStr);
if (urlStr.startsWith(EMAIL_PROFILE_FRIENDLY)) {
String hashId = urlStr.substring(EMAIL_PROFILE_FRIENDLY.length());
if (hashId != '') {
List<Contact> contacts = [SELECT Id FROM Contact WHERE HashId__c = :hashId];
if (!contacts.isEmpty()) {
pageRef = new PageReference(EMAIL_PROFILE_VF_PAGE + contacts[0].Id);
} else {
pageRef = new PageReference(EMAIL_PROFILE_VF_PAGE);
}
}
}
return pageRef;
}
}
</code></pre>
<br />
<br />
<b><u>Step 3:</u> Configuring new Force.com Public Site</b><br />
<br />
Press the "New" button to create a new Force.com Site and enter the following values – you can leave all the other fields as-is. Click Save when done.<br />
<ol>
<ul>
<li>Site Label – name (may contain spaces) used to identify the site in the Salesforce user interface. In this example we use EmailPreferences. See screenshot below.</li>
<li>Site Name – name (cannot contain spaces) used to identify the site in code</li>
<li>Site Contact – Salesforce user who will receive email notifications about any problems with the site.</li>
<li>Default Web Address – the part of the site URL that will appear immediately after the domain name, e.g. “domainname.force.com/defaultwebaddress”.</li>
<li>Active – makes the site available for use. Can be set manually, or the Activate and Deactivate buttons on the Site page can be used to change tise setting.</li>
<li>Active Site Home Page – name of the Visualforce page that serves as the home page of the site when it is active: PublicEmailPreferences.</li>
<li>Inactive Site Home Page – name of the Visualforce page that serves as the home page of the site when it is not active: InMaintenance (automatically generated by Salesforce).</li>
<li>Site Template – name of the template that specifies the structure of all site pages: PublicSiteTemplate.</li>
<li>URL Rewriter Class – name of the class that translates public-facing URLs to internal ones: PublicEmailPreferencesURLRewriter. This class converts the public-facing contact ids into Salesforce record ids, so it provides one level of data security.</li>
<li>Clickjack Protection Level – the recommended value is compatible with the operation of the site.</li>
</ul>
</ol>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjqOW178Dmmy7J0eTixxhcOpWZdjBBm-V8h5bQ1eAZ8RrBGqPEsuzz5bO78v4vO-NNkm-Shd5eZjKfB7HB4q9aRNO4WRoCLseYnVIeYdwKWo1diaNNEkSYFRVBeLw7Mk5IhUM_iz3ImyTXa/s1600/EmailPreferences.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Configuring new Force.com Public Site" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjqOW178Dmmy7J0eTixxhcOpWZdjBBm-V8h5bQ1eAZ8RrBGqPEsuzz5bO78v4vO-NNkm-Shd5eZjKfB7HB4q9aRNO4WRoCLseYnVIeYdwKWo1diaNNEkSYFRVBeLw7Mk5IhUM_iz3ImyTXa/s1600/EmailPreferences.png" title="Configuring new Force.com Public Site" /></a></div>
<br />
<br />
<b><u>Step 4:</u> Adjusting Site Profile Permissions</b><br />
<b><br /></b>
Please proceed up with following steps:<br />
<ul><ul>
<li>On the Site Details page, click Public Access Settings.</li>
<li>Click Object Settings select Contact object and click Edit.</li>
<li>For Email Opt Out field tick Read and Edit </li>
<li>Click Save.</li>
<li>Return to Profile Overview page select Visualforce Page Access and click Edit.</li>
<li>On the Enabled Visualforce Page list, select following pages and click Save:</li>
<ul>
<li>PublicEmailPreferences</li>
<li>PublicEmailTemplate</li>
</ul>
<li>Return to Profile Overview page select Apex Class Access and click Edit.</li>
<li>On the Enabled Apex Classes list, select following classes and click Save:</li>
<ul>
<li>PublicEmailPreferencesController </li>
<li>PublicEmailPreferencesURLRewriter</li>
</ul>
<li>Note that the Enabled Visualforce Pages list may (and should) automatically include default pages associated with the site.</li>
</ul>
</ul>
<div>
<b><u>Step 5:</u> Test your new Force.com Site</b></div>
<div>
<br /></div>
<div>
When all done you can open the Force.com site URL an test it in action.</div>
<div>
See how it suppose to looks like on our screenshot.</div>
<div>
<br /></div>
<div>
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEigQarNOCqM8DAcrsGs2u-3OXSwkuz0lt6H51aDnlaMjxnFYQ-NeNrMYemyFYmJ24oKFr5wPWtdCT6wHKQT2rGaVYttNzZ7s3LemIHXWsrLGJpgs6aO8Md6xrtkJg8wCOoAHi2jKdMZntMU/s1600/EmailPreferencesSite.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img alt="Force.com Site" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEigQarNOCqM8DAcrsGs2u-3OXSwkuz0lt6H51aDnlaMjxnFYQ-NeNrMYemyFYmJ24oKFr5wPWtdCT6wHKQT2rGaVYttNzZ7s3LemIHXWsrLGJpgs6aO8Md6xrtkJg8wCOoAHi2jKdMZntMU/s1600/EmailPreferencesSite.png" title="Force.com Site" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;"><br /></td></tr>
</tbody></table>
</div>
</div>
WShttp://www.blogger.com/profile/14116271758608639764noreply@blogger.com5Toronto, ON, Canada43.653226 -79.38318429999998243.285985499999995 -80.028631299999986 44.0204665 -78.737737299999978tag:blogger.com,1999:blog-826409388179453463.post-59913184126541255742015-03-18T07:32:00.004-07:002015-03-30T10:51:40.894-07:00Pop Up VisualForce page with Auto Close and Parent Refresh featuresBuilding custom solutions in Salesforce you often need to use pop up window with embedded VisualForce page to support required functionality. You can use "modal dialog box" solution. But generally it's more complicated and required usage of heavy external javascript libraries<br />
<br />
Let's say you want to create a custom button in layout to call Visual force page as a popup window. In most cases following two features will make final solution much more efficient and user friendly: <br />
<br />
<b><i>Auto Close the pop up if window lost the focus or click on Cancel button. </i></b><br />
Without such feature pop up window will require manual close and new click to the button will create another pop up window. Which is confusing the end user.<br />
<br />
<b><i>Refresh Parent window after click on Save button.</i></b><br />
If your Visual force page in pop up should update some date in parent cage (recalculate the field, update related list, etc.) you might want this feature. Without it parent SFDC page will stay AS IS as end user will have to refresh the page manually to see updated results.<br />
<div>
<br /></div>
<b><u>Step 1:</u> Create a Visual force page and controller</b><br />
<b><i>VF page: </i></b>PopUpAutoCloseAndRefresh<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : WebSolo Inc.
Date : 03.2015
Description : VF page "PopUpAutoCloseAndRefresh"
History :
-->
<apex:page controller="PopUpAutoCloseAndRefreshContr" sidebar="false" showHeader="false" >
<div id="ch" style="display: none;">{!checkParam}</div>
<script>
window.onload = function(){
window.onblur = function(){
window.close();
}
if(document.getElementById('ch').innerHTML == "Ok!")
{
window.opener.location.href="/{!$CurrentPage.parameters.id}";
window.top.close();
}
}
</script>
<p style="font-size: 16px; font-weight: bold; margin-left: 10px">Pop Up Auto Close And Refresh Demo</p>
<apex:form >
<apex:pageBlock >
<apex:pageMessage severity="info" strength="1" summary="{!messsage}" escape="false"/>
<apex:commandButton value="Close" action="{!CloseAndRefresh}"/>
</apex:pageBlock>
</apex:form>
</apex:page>
</code></pre>
<br />
<b><i>APEX Class: </i></b>PopUpAutoCloseAndRefreshContr<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : WebSolo inc.
Date : 03.2015
Description : controller for VF page "<span style="font-family: 'Andale Mono', 'Lucida Console', Monaco, fixed, monospace;">PopUpAutoCloseAndRefresh</span><span style="font-family: 'Andale Mono', 'Lucida Console', Monaco, fixed, monospace;">"</span>
History :
*/
public class PopUpAutoCloseAndRefreshContr{
public String messsage {get;set;}
public String checkParam {get;set;}
public PopUpAutoCloseAndRefreshContr(){
checkParam = 'not Ok!';
messsage = 'Please click outside of the pop up window for Auto Close<br/>Please click Close to see how parent window will be refreshed';
}
public PageReference CloseAndRefresh(){
checkParam = 'Ok!';
return null;
}
}
</code></pre>
<br />
<b><u>Step 2:</u> Create a custom button (for target object) to call the VF page</b><br />
<b><i>Custom Button: </i></b>CloseAndRefresh<br />
<pre style="background-color: #eeeeee; border: 1px dashed rgb(153, 153, 153); overflow: auto; padding: 5px; width: 100%;"><code style="word-wrap: normal;"><span style="font-size: 12px; line-height: 14px;">Label: CloseAndRefresh
Object Name : YOUR OBJECT
Name: CloseAndRefresh
Behavior: Execute JavaScript
Display Type: Detail Page Button
OnClick JavaScript:
var url = "/apex/PopUpAutoCloseAndRefresh?id={!Opportunity.Id}";
var width = "520";
var height = "400";
window.open(url, '','scrollbars=no,resizable=no,status=no,toolbar=no,menubar=no, width=' + width + ',height=' + height + ',left=' + ((window.innerWidth - width)/2) + ',top=' + ((window.innerHeight - height)/2) );</span><span style="font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace;"><span style="font-size: 12px; line-height: 14px;">
</span></span></code></pre>
<br />
<b><u>Step 3:</u> Add created custom button to layout.</b><br />
<br />
To see how the provided example works in real solution please see out post "<a href="http://salesforce.websolo.ca/2014/07/custom-validation-rule-for-lead.html">Custom Validation For Lead Conversion</a>"Anonymousnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-54678776017709970872015-02-24T10:54:00.001-08:002015-06-08T12:21:33.094-07:00Round Robin Lead Assignment RoutineThere are two most popular Assignment Routines to assign new Leads to Sales Representatives: <b>Lead Bucket</b> and <b>Round Robin</b>. Both have it's PROS and CONS but in case if you wants to automatically distribute and assign incoming Leads to the members of your Sales Team you may want to use "round-robin" allocation process.<br />
<br />
Pure "round robin" technique assumes that you want to divvy out all new Leads equally among the Reps in the Sales Team. Let's say you have three Sales Reps. In this case Lead_1 will go to <b>Rep_1</b>, Lead_2 will go to Rep_2, Lead_3 will go to Rep_3, Lead_4 will go to <b>Rep_1</b> (!), Lead_5 will go to Rep_2 and so on.<br />
<br />
One of our customers gets about 300 Leads per day submitted to Salesfroce from Web site through Web-to-Lead form. Sales Team has about ten Sales Reps with different level of experience (including Interns, Senior Reps and Sales Reps with less skill).<br />
<br />
<i><b>BUSINESS REQUIREMENTS</b></i>: Create Assignment Routine to automatically assign Leads (based on "round-robin" logic) with ability to easy activate/deactivate Reps (from round robin cycle if the Rep is sick or on vacation) and set it's <b>Share </b>- as a % of the total Leads received from Leads Queue during the day ("not equal share" feature).<br />
<div>
<br /></div>
<div>
Let's review all possible "round robin" solutions which could meet the requirement (fully or partially) along with implementation details and all its <b>PROS </b>and <b>CONS</b>.<br />
<br />
<b>Update 06/08/2015</b>: Please also see <a href="http://salesforce.websolo.ca/2015/06/Salesforce-Round-Robin-Assignment-Remedyforce.html"><b>Custom Salesforce Round Robin Incidents Assignment Routine for BMC Remedyforce</b></a><br />
<br /></div>
<div>
<h4>
#1 Auto Number field and Lead Assignment Rules</h4>
We want to present this option as a "native" Salesforce solution which does not required any APEX coding. Here is a plan for Sales Team with 3 members:<br />
<ol>
<li>On the Lead object create new "Lead RR Number" field (Data Type: Auto Number, Display Format: {0}, Starting Number:1<br />As a result every new Lead will get an unique number with 1 as an increment.</li>
<li>On the Lead object create new "Lead RR ID" field (Data Type: Formula, Formula Value: MOD(VALUE({!Lead_RR_Number__c}),3) +1<br />Formula will return number from 1 to 3 depend on value in "Lead RR Number" field.</li>
<li>Create 3 new Lead Assignment Rules with criteria for the rule entry as following:<br />Lead: Lead RR ID equals 1 / User to Assign the Lead: Rep_1; Lead: Lead RR ID equals 2 / User to Assign the Lead: Rep_2; Lead: Lead RR ID equals 3 / User to Assign the Lead: Rep_3 <br />Activate new Rules and check how the solution works in action. Every new created user suppose to be assigned to Sales Rep as per expected "round robin" logic.</li>
</ol>
<b>PROS</b>: Use only SFDC native "out of box" tools to build<br />
<b>CONS</b>: Bulky, quite complicated and time consuming to manage. Every time when you want to add/delete/activate/deactivate the Rep in the Sale Team you have to edit the formula and Assignment rules. Easy to make a mistake and compromise the assignment flow. Can't support "not equal share" requirements.<br />
<br />
<h4>
#2 Custom Force.com solution with new object and APEX trigger</h4>
After careful consideration we proposed and built following Force.com solution which meets all requirement.<br />
<br />
SOLUTIONS DETAILS:</div>
<div>
1) New Custom object "Sales Representatives" to keep info about Users involved in "round robin" assignment flow.<br />
Consist of following fields:<br />
- User (lookup to User)<br />
- Status (Active/Inactive)<br />
- Split Weight (number from 1 to 100)<br />
- Split % (dynamically calculated based on number of Active users and their "Split Weight". For instance if you have 3 active Users and their Split Weight set to 20/30/30 their Split % will be calculated as 25%/37.5%/37.5%)<br />
- Next Assign To (check box to dynamically show who will get next Lead from Queue)<br />
- Today's Score (number of assigned TODAY Leads)<br />
<br />
In SFDC interface Administrator can easy amend the Weight and Status for each user. Also he has an ability to add/delete records (Sales Reps) in the object.</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJY3hCMwxG2FHbRcPD8gK6f4OtY-XyZHgWkG4fe9LYT1KyznayMLYV-oGm4lXP0fl2sxEa9C7rto6HTdIU0KVp8OrVJLCyK-E_BpIE4YgD30kWcOR8JbMlgPzv17niGpw7JBEnCe9BVSE6/s1600/Sales-Representatives.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img alt="Round Robin Lead Assignment Routine" border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJY3hCMwxG2FHbRcPD8gK6f4OtY-XyZHgWkG4fe9LYT1KyznayMLYV-oGm4lXP0fl2sxEa9C7rto6HTdIU0KVp8OrVJLCyK-E_BpIE4YgD30kWcOR8JbMlgPzv17niGpw7JBEnCe9BVSE6/s1600/Sales-Representatives.png" title="Round Robin Lead Assignment Routine" /></a></div>
<div>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
2) Trigger SplitCalculation to auto recalculate Split % for each active user if Split Weight or Status of any User changed. Split % for each inactive user to be set to 0. Split % will be used in assignment logic.<br />
<br />
3) Overnight job (at 0:00) to reset all users Today's Score to 0.<br />
<br />
4) Trigger LeadAssignment (before insert) to fire for each created Lead (some filters could be applied). Trigger (and dedicated Helper class) does following:<br />
- assign the Lead to the User who is next in the line (based on Next Assign To check box)<br />
- update assigned User's Today's Score based on number of assigned today Leads (+1)<br />
- identify Next Assign To User as per following logic (assigning Lead to the users on a round robin basis. Count Users score and skip User if he's got as much Leads to hit his User's Split %).<br />
<div>
<b><br /></b></div>
<div>
<b>PROS</b>: Meets ALL Business Requirements</div>
<b>CONS</b>: None. Required new object and some APEX coding. <br />
<br />
Please do not hesitate to <a href="http://websolo.ca/contact_us/">contact us</a> if you want to leverage this solutions in your Saleforce instance.<br />
<br />
<h4>
#3 App by Salesforce Labs: Round Robin Record Assignment</h4>
Advanced Salesfroce users with Administrator experience might want to install and amend the <a href="https://appexchange.salesforce.com/listingDetail?listingId=a0N3000000178fsEAA" rel="nofollow" target="_blank">Round Robin Record Assignment</a> app from Force Labs. This free solution was built to manage Cases assignment. But APEX code is open to modify. So with some experience and knowledge you can amend the app to work with Leads object. </div>
<div>
<br />
<b>PROS</b>: Free app from Force Labs. Has some extra features.<br />
<b>CONS</b>: Does not support "not equal share" requirements. Has some limitations and average 3.7 of 5 stars reviews. </div>
<div>
<br /></div>
WShttp://www.blogger.com/profile/14116271758608639764noreply@blogger.com0Toronto, ON, Canada43.653226 -79.38318429999998243.285985999999994 -80.028631299999986 44.020466 -78.737737299999978tag:blogger.com,1999:blog-826409388179453463.post-41910101081795046042015-01-09T09:31:00.001-08:002015-01-21T08:23:58.962-08:00How to instantly see potential duplicates on the Lead page <div>
Over the time company's sales representatives could create <b>Duplicated Leads in Salesforce</b>. Often it creates awkwardness and challenges. For instance when different reps call to the same individual. As a result the company could lose the sale opportunity right from the start.</div>
<br />
<div>
Salesforce has native “Find Duplicates” button located on the Lead layout but reps not always use it BEFORE the actual call or email to potential buyer. It also time consuming to check out every Lead.</div>
<div>
<br /></div>
<div>
Recently we received following request from one of our Customers: "<i>Is it possible to see duplicates on the lead’s front page (ie without clicking find duplicates)? Sort of like how I can see open activities and activity history without scrolling down. This would save me a click every time on Find Duplicates button to check the same.</i>"</div>
<div>
<br /></div>
<div>
So Customer wants from one glance to see if the current Lead has potential duplicates - without extra clicks. And only click to "Find Duplicates" button for search and merge duplicated Lead if needed.<br />
<br />
You can find details of solution we've built below.<br />
<br /></div>
<div>
<div>
<h4>
Solution to instantly show list of duplicated Leads on the Lead layout.</h4>
<div>
1) VF page (SeeDuplicatesLeads) to be embedded to Lead Layout. VF page shows list of potential duplicates of the current Lead. List consists of following columns: </div>
<div>
Name | Last Name | Company | Email | Phone | Lead Owner </div>
<div>
<br /></div>
<div>
2) APEX controller uses similar to native "Find Duplicates" button's matching logic to identify Lead's duplicates (the same Lead Name OR the same Company OR the same Email OR the same Email Domain OR the same Phone).<br />
<br />
See code samples below.<br />
<br />
<b>Extra note</b>: Solution uses the <b>Dynamic SOQL </b>approach to avoid issues when one or more of the filed(s) used in SOQL have NULL value.</div>
</div>
</div>
<br />
<b><i>VF page: </i></b>SeeDuplicatesLeads<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : WebSolo Inc.
Date : 01.2015
Description : VF page "SeeDuplicatesLeads" to to instantly show list of duplicated Leads on the Lead layout
History :
-->
<apex:page standardController="Lead" extensions="SeeDuplicatesLeads">
<apex:pageBlock >
<apex:pageBlockTable value="{!LeadList}" var="tl">
<apex:column headerValue="Name">
<a href="/{!tl.id}" target="_top" id="{!tl.id}">{!tl.Name}</a>
</apex:column>
<apex:column headerValue="Email" value="{!tl.Email}"/>
<apex:column headerValue="Company" value="{!tl.Company}"/>
<apex:column headerValue="Phone" value="{!tl.Phone}"/>
<apex:column headerValue="Lead Owner" value="{!tl.Owner.Name}"/>
</apex:pageBlockTable>
</apex:pageBlock>
</apex:page>
</code></pre>
<br />
<b><i>APEX Class: </i></b>SeeDuplicatesLeads<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : WebSolo inc.
Date : 01.2015
Description : controller for VF page "SeeDuplicatesLeads"
History :
*/
public class SeeDuplicatesLeads
{
public id leadId {get; set;}
public list<Lead> LeadList {get; set;}
public SeeDuplicatesLeads(ApexPages.StandardController controller)
{
leadId = ((Lead) controller.getRecord()).id;
LeadList = new list<Lead>();
Lead leadobj = [select id, Name, Email, Company, Phone, OwnerId, Owner.Name from Lead where id=: leadId limit 1];
String sqlStart = 'select id, Name, Email, Company, Phone, OwnerId, Owner.Name from Lead where ';
String sqlEnd = ' and id !=\'' + leadobj.id + '\'';
String sqlwhere = '';
if(leadobj.Email == null)
{
if(leadobj.Company == null)
{
if(leadobj.Phone == null)
{
sqlwhere = 'Name =\'' + leadobj.Name + '\'';
}
else
{
sqlwhere = '(Name =\'' + leadobj.Name + '\' or Phone =\'' + leadobj.Phone + '\')';
}
}
else
{
if(leadobj.Phone == null)
{
sqlwhere = '(Name = \'' + leadobj.Name + '\' or Company =\'' + leadobj.Company + '\')';
}
else
{
sqlwhere = '(Name = \'' + leadobj.Name + '\' or Company = \'' + leadobj.Company + '\' or Phone = \'' + leadobj.Phone + '\')';
}
}
}
else
{
list<String> EmailDomain = leadobj.Email.split('@');
list<Lead> LeadListNew = new list<Lead>();
if(leadobj.Company == null)
{
if(leadobj.Phone == null)
{
sqlwhere = '(Name =\'' + leadobj.Name + '\' or Email LIKE \'' + EmailDomain[1] + '\')';
}
else
{
sqlwhere = '(Name =\'' + leadobj.Name + '\' or Phone =\'' + leadobj.Phone + '\' or Email LIKE \'' + EmailDomain[1] + '\')';
}
}
else
{
if(leadobj.Phone == null)
{
sqlwhere = '(Name = \'' + leadobj.Name + '\' or Company =\'' + leadobj.Company + '\' or Email LIKE \'' + EmailDomain[1] + '\')';
}
else
{
sqlwhere = '(Name = \'' + leadobj.Name + '\' or Company = \'' + leadobj.Company + '\' or Phone = \'' + leadobj.Phone + '\' or Email LIKE \'' + EmailDomain[1] + '\')';
}
}
}
LeadList = Database.query(sqlStart + sqlwhere + sqlEnd);
if(LeadList.size() > 100)
{
list<Lead> goodLeadlist = new list<Lead>();
Integer Igood = 100;
for(Integer I = 0; I < Igood; I++)
{
goodLeadlist.add(LeadList[I]);
}
LeadList.clear();
LeadList.addall(goodLeadlist);
}
}
}
</code></pre>
Anonymousnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-37720267222988095442014-10-06T11:15:00.001-07:002015-01-21T08:24:24.406-08:00Workaround to increase field label character limitsSalesforce has 40 character limit for Custom Field Labels. It's Ok in most cases but not always.<br />
<div>
There was a particular project which required quite long names for the fields to be used on VisualForce page layout. <br />
<br />
The VisualForce page (to store the Coach/Trainee training session Report) had a lot of sections/fields. Short labels were just not sufficient enough to be self explanatory. Some labels were needed to be up to 200 characters long.<br />
<br />
Two workarounds to meet the requirements were considered. Please find below workarounds description.<br />
<br />
<h4>
#1 Use the field's Description to store the "long version" of the Label and then use this Description on VF page instead of "native" field's Label</h4>
<div>
<br /></div>
Unfortunately appeared that Description available in SFDC metadata API bat can be called through the Apex/VisualForce calls. SFDC does not support such functionality.<br />
<br />
<h4>
#2 Use a custom settings for the same purposes - to store the filed label's "long version"</h4>
<div>
<br /></div>
After few experiments the POC (Proof Of Concept) was built and used as the solution in mentioned project. About 20 custom fields with related in Custom Settings "long label version" were created. Visual Force and related controller (using hard coded mapping table) calls the field's "long" label to show instead on "native" one on VisualForce UI.</div>
<br />
<div>
Please find below screen than explain how Custom Settings and VF page works together.<br />
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-MxJ0zKA44h4/VDLeIveyTUI/AAAAAAAAAkc/zQeRSE_WOZs/s1600/field%2Blabel%2Bcharacter%2Blimits.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img alt="Increase field label character limits" border="0" src="http://1.bp.blogspot.com/-MxJ0zKA44h4/VDLeIveyTUI/AAAAAAAAAkc/zQeRSE_WOZs/s1600/field%2Blabel%2Bcharacter%2Blimits.png" height="640" title="Increase field label character limits" width="444" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
Anonymousnoreply@blogger.com0tag:blogger.com,1999:blog-826409388179453463.post-37946694586155732342014-08-07T12:02:00.001-07:002015-02-05T11:39:06.592-08:00Salesforce Lead Conversion - How To Specify resulted Account/Contact/Opportunity Record TypesAs known when user converts a Lead, Salesforce creates new Account, Contact and Opportunity using the information from the Lead.<br />
<br />
There is uncomfortable feature that Salesforce users struggle for a long time: "the default record type of the user converting the Lead is assigned to records created during lead conversion".<br />
<br />
I.e. User (who's Profile has access to multiple Record Types) can not control with which Record Type Account, Contact and Optionally will be created. It always will be "Default Record Type" selected by Administrator for the User's profile. As a result Users will have to manually change Record Types for newly created records. Which is uncomfortable and takes valuable time.<br />
<br />
There are two workarounds to enhance user experience and overcome the issue. Please find below workarounds description with analysis about their <b>PROS </b>and <b>CONS</b>.<br />
<br />
<b>Note</b>: Updated 2015/02/05 with corrections related to solution #1<br />
<br />
<h4>
#1 Use Workflow Rules to update Record Type of created Account, Contact and Opportunity </h4>
Easy to implement solution which not requires advanced Salesfroce Development skills.<br />
<br />
<b><i>Step 1:</i></b> Create three picklist fields on Lead object (Expected Account Type, Expected Contact Type, Expected Opportunity Type) and populate them with Record Types names you have for Accounts, Contacts and Opportunities objects. These field will be visible to Users to let them to select Record Type they want for created after Lead Conversion records.<br />
<br />
<b><i>Step 2:</i></b> Create three text fields with the same names on Accounts (Expected Account Type), Contacts (Expected Contact Type) and Opportunity (Expected Opportunity Type). These fields could be invisible for Users an will be used by workflow rules.<br />
<br />
<div>
<b><i>Step 3:</i></b> Map new Leads fields to related fields on Accounts, Contacts and Opportunities objects. <br />
<br />
<b><i>Step 4:</i></b> You will have to create as many Workflow Rules and Actions for Accounts, Contacts and Opportunities as many Record Types you have in these objects. For instance if you have two Record Types for Account you will have to create two Workflows dedicated for each of them.<br />
<br />
Set every Workflow Rule with Evaluation Criteria "Evaluate the rule when a record is created" and Rule Criteria (for example) "Account: Expected Account Type EQUALS [YOUR RECORD TYPE NAME]. Add Action as field update to update the Record Type field to value you want to change in this particular Workflow. Repeat the same for every Account Record Type. Then do the same for Contacts and Opportunities Record Types.<br />
<br />
<b><i>Step 5:</i></b> Activate Workflows and test the solution.<br />
<br />
<b>PROS:</b> Pretty easy to implement. Does not require APEX coding and triggers development.<br />
<b>CONS:</b> Six custom fields in four objects. Multiple (depend on your SFDC solution - could be quite many) Workflow Rules/Actions to monitor and update if any changes required.<br />
<br />
<h4>
#2 Use APEX trigger Rules to update Record Type of created Account, Contact and Opportunity </h4>
A little more complicated from development point of view but more efficient and easy to support solution.<br />
<br />
Instead of the workflows this solution use a single APEX trigger with Helper class to handle all needed functionality.<br />
<br />
<b><i>Step 1: </i></b>The same as above. Create three picklist fields on Lead object (Expected Account Type, Expected Contact Type, Expected Opportunity Type) and populate them with Record Types names you have for Accounts, Contacts and Opportunities objects. These field will be visible to Users to let them to select Record Type they want for created after Lead Conversion records.<br />
<br />
<b><i>Step 2:</i></b> Create trigger and helper class. See code samples below.<br />
<br />
<b><i>Step 3: </i></b>Test and deploy the solution to Production environment.<br />
<br />
<b>PROS:</b> Easy to implement and support.<br />
<b>CONS:</b> None<br />
<br />
<b><i>APEX Trigger: </i></b>LeadConversionExpectedRecordTypes</div>
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : Websolo inc.
Date : 08/2014
Description :
Update History :
*/
trigger LeadConversionExpectedRecordTypes on Opportunity (after insert)
{
Boolean uiCR = false;
if(Test.isRunningTest())
{
uiCR = true;
}
else
{
if(Trigger.new.size() == 1)
{
uiCR = true;
}
}
if(uiCR == true)
{
for(Opportunity opp: Trigger.new)
{
LeadConversionExpectedRecordTypesHelper.UpdOpp(opp.id);
}
}
}
</code></pre>
<br />
<b><i>APEX helper class: </i></b>LeadConversionExpectedRecordTypesHelper<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : Websolo inc.
Date : 08/2014
Description : help class for LeadConversionExpectedRecordTypes trigger
Update History :
*/
public class LeadConversionExpectedRecordTypesHelper
{
@future(callout = true)
public static void UpdOpp(id oppObjID)
{
map<string id=""> mapRC = new map<string id="">();
for(RecordType a: [select Name, id from RecordType where SobjectType = 'Opportunity'])
{
mapRC.put(a.Name, a.id);
}
map<string id=""> mapRCacc = new map<string id="">();
for(RecordType a: [select Name, id from RecordType where SobjectType = 'Account'])
{
mapRCacc.put(a.Name, a.id);
}
Opportunity opp = [select id, OwnerId, RecordTypeId from Opportunity where id =: oppObjID];
List<lead> listLead = new List<lead>();
if(!test.isRunningTest())
{
listLead = [select id, Expected_Opportunity_Type__c, Expected_Account_Type__c, ConvertedAccountId from Lead where ConvertedOpportunityId =: opp.id AND isConverted = true];
}
else
{
listLead = [select id, Expected_Opportunity_Type__c, Expected_Account_Type__c, ConvertedAccountId from Lead];
}
if(listLead.size() > 0)
{
if(listLead[0].Expected_Opportunity_Type__c != null)
{
opp.RecordTypeId = mapRC.get(listLead[0].Expected_Opportunity_Type__c);
update opp;
}
if(listLead[0].ConvertedAccountId != null)
{
Account acc = [select id, RecordTypeId from Account where id=:listLead[0].ConvertedAccountId];
if(listLead[0].Expected_Account_Type__c != null)
{
acc.RecordTypeId = mapRCacc.get(listLead[0].Expected_Account_Type__c);
update acc;
}
}
}
}
}
</lead></lead></string></string></string></string></code></pre>
Anonymousnoreply@blogger.com4tag:blogger.com,1999:blog-826409388179453463.post-86160019003847058812014-07-24T10:26:00.000-07:002015-01-21T08:24:16.760-08:00Identify Ultimate Parent in an Account HierarchyTo build Account hierarchy Salesforce has native "Parent Account" lookup field in Account object. Unfortunately Salesfroce does not go further to provide native solution to automatically calculate "Ultimate Parent" or "Top Level Account" if you have more than two levels in Account hierarchy. As per our experience organizations have up to 5 (sometimes more) levels of Account hierarchy.<br />
<br />
Having such "Ultimate Parent" field will allow SFDC Users/Administrators to do a lot of advanced data exercises using grouping, summation or aggregation by the entire account tree.<br />
For instance:<br />
<ul>
<li>total sales to multi-level hierarchies</li>
<li>list of Contacts in entire account hierarchy</li>
<li>views, reports and dashboards for account families</li>
</ul>
There are a few workarounds to create such useful field. Please find below the most popular workarounds with analysis about their <b>PROS </b>and <b>CONS</b>.<br />
<br />
<h4>
#1 Use formula field to calculate Ultimate Parent Name or ID</h4>
Most obvious and popular solution. Using formula field you can determine the ultimate parent in the hierarchy. Formula below returns Ultimate Parent as a text formatted to hyperlink to use it in Account layout. Click to the link will open Ultimate Account page. This formula supports up to 10 levels of Accounts hierarchy.<br />
<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">IF(LEN(Parent.Name) < 1, HYPERLINK("/"&Id, Name,"_parent"),
IF(LEN(Parent.Parent.Name) <1, HYPERLINK("/"&Parent.Id,Parent.Name,"_parent"),
IF(LEN(Parent.Parent.Parent.Name) < 1, HYPERLINK("/"&Parent.Parent.Id,Parent.Parent.Name,"_parent"),
IF(LEN(Parent.Parent.Parent.Parent.Name) < 1, HYPERLINK("/"&Parent.Parent.Parent.Id,Parent.Parent.Parent.Name,"_parent"),
IF(LEN(Parent.Parent.Parent.Parent.Parent.Name) < 1, HYPERLINK("/"&Parent.Parent.Parent.Parent.Id,Parent.Parent.Parent.Parent.Name,"_parent"),
IF(LEN(Parent.Parent.Parent.Parent.Parent.Parent.Name) < 1 ,HYPERLINK("/"&Parent.Parent.Parent.Parent.Parent.Id,Parent.Parent.Parent.Parent.Parent.Name,"_parent"),
IF(LEN(Parent.Parent.Parent.Parent.Parent.Parent.Parent.Name) < 1 ,HYPERLINK("/"&Parent.Parent.Parent.Parent.Parent.Parent.Id,Parent.Parent.Parent.Parent.Parent.Parent.Name,"_parent"),
IF(LEN(Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Name) < 1 ,HYPERLINK("/"&Parent.Parent.Parent.Parent.Parent.Parent.Parent.Id,Parent.Parent.Parent.Parent.Parent.Parent.Parent.Name,"_parent"),
IF(LEN(Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Name) < 1 ,HYPERLINK("/"&Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Id,Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Name,"_parent"),
IF(LEN(Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Name) < 1 ,HYPERLINK("/"&Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Id,Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Name,"_parent"), "Ultimate Parent Beyond 10 Levels"))))))))))
</code></pre>
If needed formula could be updated to return text instead of hyperlink.<br />
<br />
<b>PROS</b>: Easy to implement<br />
<b>CONS</b>: Covers very limited number of use cases because the result of the formula is text. In move advanced cases you need Ultimate Account as a lookup field to use in reports, APEX code or in Roll-up formulas.<br />
<br />
<h4>
#2 Singe synchronous APEX trigger to calculate the Ultimate Parent when Account created or updated.</h4>
The trigger requires and has to calculate/re-calculate custom "Ultimate Parent" lookup field on Account object (read-only for users).<br />
<br />
Let's consider few scenarios using following Accounts hierarchy tree as example:<br />
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
<a href="http://3.bp.blogspot.com/-k6rN0IY_OsI/U8_2Q2s6sII/AAAAAAAAAjM/YQxhAnZOats/s1600/Image+2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Accounts hierarchy " border="0" src="http://3.bp.blogspot.com/-k6rN0IY_OsI/U8_2Q2s6sII/AAAAAAAAAjM/YQxhAnZOats/s1600/Image+2.png" title="Accounts hierarchy " /></a></div>
<br />
Accounts 1 and 8 are "Ultimate or Top Accounts" (i.e. do not have parent Accounts)<br />
<br />
<b>Case 1: </b>New Account created as a child of Account 3. The trigger has to calculate Account 1 as Ultimate Parent for new Account and update this Account with calculated value for "Ultimate Parent" field. This is easy recursive update.<br />
<br />
<b>Case 2</b>: Account 3 updated to be the child of Account 1 instead of Account 2 (i.e. Account 3 moved up along the Account 1 hierarchy tree branch).<br />
In this case Account 1 still Ultimate Parent for Account 3, 4 and trigger no need to update these two Accounts.<br />
<br />
<b>Case 3</b>: Account 2 updated to be on the top of the hierarchy tree (i.e. Account 2 will become Ultimate Account for all related child Accounts).<br />
In this case the trigger has to calculate Account 2 as Ultimate Parent for Accounts 3-7 and update these five Accounts with new Ultimate Parent. This is quite complicated recursive update. To find all impacted child Accounts the trigger will have to traverse down through Account 3 and Account 5 branches (layers) to identify all related to these branches Accounts and update them with new Ultimate Parent value.<br />
<br />
<b>Case 4</b>: Account 2 updated to be the child of Account 8 instead of Account 1 (i.e. Account 2 with all related child Accounts moved to another [Account 8] hierarchy tree branch).<br />
In this case trigger has to calculate Account 8 as Ultimate Parent for Accounts 2-7 and update these six Accounts with new Ultimate Parent value. This is also complicated recursive update which involve multiple parent to child account layers.<br />
<div>
<br /></div>
The most complicated part here is to traverse parent-child relationships from account to account. Unfortunately SFDC does not support such traverse so you can not do this in a single SOQL query. What you can - is to include parent.parent.parent.parent.parent.id in your SOQL select. Than loop through the result and if none of them are null - launch another SOQL query until you find a parent without parent. And then assign it as Ultimate Parent for involved Accounts.<br />
<br />
Sounds complicated even in theory. But after number of experiments and brainstorming we finally built such trigger and even used it for a while. Unfortunately the trigger code was pretty ugly and solution (as appeared) was fragile. In particular cases - when hierarchy has a lot of parent to child account layers - trigger hits SFDC's governor limits. In cases when the trigger was able to handle governor limits it took a lot of time to finish - up to 5 seconds - this was not acceptable from performance point of view.<br />
<br />
That's why we DO NOT recommend such approach and do not sharing here the trigger's code.<br />
<br />
<b>PROS</b>: Works in not complicated SFDC implementations where accounts hierarchy does not have a lot of parent to child account layers.<br />
<b>CONS</b>: Covers only limited use cases. Ugly and fragile code which apparently hits SFDC governor limits. Performance issues.<br />
<br />
<h4>
#3 Two components solution: synchronous APEX trigger and APEX batch</h4>
In this solution the trigger has to handle only inserts and updates along the single Account hierarchy tree branch (see Cases 1 and 2 above) and use overnight APEX batch to catch and calculate more complicated cases.<br />
<br />
APEX batch class has more wider governor limits, could be launched overnight and does not have strict performance requirements.<br />
<br />
The final solution consists of following components:<br />
<ul>
<li>custom "Ultimate Parent" lookup field on Account object (read-only for users).</li>
<li>an APEX Trigger that populates "Ultimate Parent" field for new created or updated Accounts</li>
<li>scheduled APEX batch class to recalculate all Accounts to catch cases not covered by trigger (see Cases 3 and 4 above).</li>
</ul>
<div>
<b>PROS</b>: The solution covers all possible use cases. Stable code which does not hit SFDC governor limits. No performance issues.<br />
<b>CONS</b>: In cases when Account moved to another branch (family) Ultimate Parent will be re-calculated only next night. But such scenarios happens quite rare.<br />
<br /></div>
<div>
<b><i>APEX Trigger: </i></b>SetUltimateParent</div>
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">trigger SetUltimateParent on Account (before insert, before update)
/*
Version : 2.0
Company : Websolo Inc.
Description : The trigger calculates Ultimate Parent ID ONLY for the updated/inserted Account with Hierarchy of parent Accounts to 5 levels up.
Scheduled APEX job will recalculate Ultimate Parent ID for all Account overnight to make sure they calculated properly in case if for example Account moved to another Account
*/
{
list<Account> ListAcc = new list<Account>();
set<id> idAcc = new set<id>();
for(Account a: Trigger.new)
{
if(a.ParentId != null)
{
ListAcc.add(a);
idAcc.add(a.id);
}
}
List<Account> AccUltP = [select id, Name,
ParentId,
Parent.ParentId,
Parent.Parent.ParentId,
Parent.Parent.Parent.ParentId,
Parent.Parent.Parent.Parent.ParentId
from
Account
where
ParentId != null
and
id IN: idAcc];
if(AccUltP.size() > 0)
{
for(Account a: ListAcc)
{
for(Account b: AccUltP)
{
if(a.id == b.id)
{
if(b.Parent.Parent.Parent.Parent.ParentId != null)
{
a.Ultimate_Parent__c = b.Parent.Parent.Parent.Parent.ParentId;
}
else
{
if(b.Parent.Parent.Parent.ParentId != null)
{
a.Ultimate_Parent__c = b.Parent.Parent.Parent.ParentId;
}
else
{
if(b.Parent.Parent.ParentId != null)
{
a.Ultimate_Parent__c = b.Parent.Parent.ParentId;
}
else
{
if(b.Parent.ParentId != null)
{
a.Ultimate_Parent__c = b.Parent.ParentId;
}
else
{
if(b.ParentId != null)
{
a.Ultimate_Parent__c = b.ParentId;
}
else
{
a.Ultimate_Parent__c = b.id;
}
}
}
}
}
}
}
}
}
}
</code></pre>
<br />
<b><i>APEX Batchable Class</i></b>: UltimateParentUpdateBatchable<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 2.0
Company : Websolo inc.
Date : 11/2013
Description : This batchable class calculates Ultimate Parent for Account with Hierarchy of parent Accounts to 5 levels up.
*/
global class UltimateParentUpdateBatchable implements Database.Batchable<SObject>, Database.Stateful
{
global Database.QueryLocator start(Database.BatchableContext bc)
{
return Database.getQueryLocator([select id, Name, ParentId, Parent.ParentId, Parent.Parent.ParentId, Parent.Parent.Parent.ParentId, Parent.Parent.Parent.Parent.ParentId from Account]);
}
global void execute(Database.BatchableContext bc, List<SObject> batch)
{
list<Account> ListAcc = new list<Account>();
set<id> idAcc = new set<id>();
for (Account a : (List<Account>) batch)
{
if(a.ParentId != null)
{
ListAcc.add(a);
idAcc.add(a.id);
}
}
List<Account> AccUltP = new List<Account>();
if(Test.isRunningTest())
{
Account Ac1 = new Account();
Ac1.id = ListAcc[0].id;
Ac1.Name = 'Test1';
AccUltP.add(Ac1);
}
else
{
AccUltP = [select id, Name,
ParentId,
Parent.ParentId,
Parent.Parent.ParentId,
Parent.Parent.Parent.ParentId,
Parent.Parent.Parent.Parent.ParentId
from
Account
where
ParentId != null
and
id IN: idAcc];
}
if(AccUltP.size() > 0)
{
for(Account a: ListAcc)
{
for(Account b: AccUltP)
{
if(a.id == b.id)
{
if(b.Parent.Parent.Parent.Parent.ParentId != null)
{
a.Ultimate_Parent__c = b.Parent.Parent.Parent.Parent.ParentId;
}
else
{
if(b.Parent.Parent.Parent.ParentId != null)
{
a.Ultimate_Parent__c = b.Parent.Parent.Parent.ParentId;
}
else
{
if(b.Parent.Parent.ParentId != null)
{
a.Ultimate_Parent__c = b.Parent.Parent.ParentId;
}
else
{
if(b.Parent.ParentId != null)
{
a.Ultimate_Parent__c = b.Parent.ParentId;
}
else
{
if(b.ParentId != null)
{
a.Ultimate_Parent__c = b.ParentId;
}
else
{
a.Ultimate_Parent__c = b.id;
}
}
}
}
}
}
}
}
update ListAcc;
}
}
global void finish(Database.BatchableContext bc)
{
}
private static testMethod void test()
{
Test.startTest();
Account Acc1 = new Account();
Acc1.Name = 'Test1';
insert Acc1;
Account Acc2 = new Account();
Acc2.Name = 'Test2';
Acc2.ParentId = Acc1.id;
insert Acc2;
Test.stopTest();
}
}
</code></pre>
<br />
<b><i>APEX Batchable Class</i></b>: UltimateParentUpdateBatchable<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">global class UltimateParentUpdateSchedulable implements Schedulable
{
global void execute(SchedulableContext ctx)
{
Database.executeBatch(new UltimateParentUpdateBatchable(), 200);
}
private static testMethod void test() {
Test.startTest();
System.schedule('UltimateParentUpdateBatchable', '0 0 0 * * ?', new UltimateParentUpdateSchedulable());
Test.stopTest();
}
}
</code></pre>
Anonymousnoreply@blogger.com10tag:blogger.com,1999:blog-826409388179453463.post-40190472057075602152014-07-14T09:39:00.000-07:002015-02-24T06:30:50.116-08:00Custom Validation For Lead ConversionTo increase data quality Sales Managers encourage their Sales Representatives to gather as much as possible information about Leads before Conversion it to Contact, Account and Opportunity.<br />
<br />
There are multiple techniques to enforce SFDC Users to enter values to all required fields before the Conversion. Let's discuss these techniques with implementation details and all its <b>PROS </b>and <b>CONS</b>.<br />
<br />
<h3>
How to enforce Sales Reps to enter values to required Lead fields before the Conversion.</h3>
<br />
<h4>
#1 Make fields required on Lead layout</h4>
The most obvious and straight forward solution. But not the best one. Salesforce Lead layout allows Administrator to make some fields required (mandatory). But it's hard for Sales representative to get values for all required fields at once. In most cases Sales person can enter some data initially and then revisit the Lead record to update the rest of the important fields later.<br />
<br />
<b>PROS</b>: Easy to implement<br />
<b>CONS</b>: Not convenient for Users. To make any update (edit the Lead) User have to enter values in all required fields at once.<br />
<b><br />
</b> <b>#2 Use a validation rule to fire when Lead is actually converted</b><br />
<b><br />
</b> Such validation rule will help to make certain fields required before converting the Lead but not require them to be entered when User edits the Lead's record.<br />
<br />
Here is example of Validation Rule (could be adjusted to meet particular business requirements)<br />
<br />
Rule Name: Lead_Conversion_Validation<br />
<br />
Error Condition Formula:<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">AND(IsConverted,
OR(
ISBLANK(Email),
ISBLANK(Phone),
ISBLANK(Website),
ISBLANK(TEXT(LeadSource)),
ISBLANK(TEXT(Range_of_Revenues__c)),
ISBLANK(State),
ISBLANK(Street),
ISBLANK(City),
ISBLANK(PostalCode),
ISBLANK(Country)
))
</code></pre>
<br />
Error Message:<br />
Please fill out following fields before conversion:<br />
- Email<br />
- Phone<br />
- Full Address<br />
- Website<br />
- Range of Revenues<br />
- Lead Source<br />
<br />
<b>PROS</b>: Pretty easy to implement<br />
<b>CONS</b>: Still not convenient for Users. The Validation Rule fires ONLY when user hit the Convert button on the Lead page, entered data on Conversion page and clicked Convert button. If something goes wrong (any required field is missed) - user will lose his time and will have to return back to the Lead page to enter missed data. Also Error Message is very general and does not really help the User to identify the missed field(s).<br />
<br />
<b>#3 Use workflow to update the Lead's Record Type/Layout when User enters all required for conversion fields</b><br />
<br />
To build this solution you will have to create and support two Lead Record Types and related identical Lead layouts:<br />
- NotReadyForConversion (default) Lead Record Type and related Lead layout <b>without </b>Convert button<br />
- ReadyForConversion Record Type and related Lead layout <b>with </b>Convert button<br />
<br />
Custom workflow have to check/monitor if all required fields have data.<br />
When the necessary fields are filled out workflow action will update the Lead's Record Type to ReadyForConversion and Users will see the Lead in new layout - <b>with </b>Convert button.<br />
<br />
<b>PROS</b>: Allows user to edit the Lead and Convert it only when all required fields are filled out.<br />
<b>CONS</b>: Required two Record Type and two Layouts. Difficult to maintain - especially if you have multiple Leads Record Types/Layouts/User Profiles. Still not convenient for Users. No Error message. User can't see/check out which fields are still missing.<br />
<br />
<b>#4 Override the standard 'Convert' button with custom button and related VF page/Controller</b><br />
<br />
SFDC Administrator/Developer can create a new custom 'Convert' button to be used on Lead layout instead of native one. Click to the button will call popup window with custom VF page and Controller to check if all required fields have data (based on particular business requirements).<br />
<ul>
<li>If all required fields are filled out - embedded in VF page JavaScript will auto close the pop up window and redirect User to Conversion page.</li>
<li>If any required field(s) not filled out yet - VF page will show User Error message with information about which field(s) have to be entered. By click to Close button / or lose pop up window focus / or in 10 seconds of inactivity - close/auto close pop up window. User still on the Lead page and can enter data in missed field.</li>
</ul>
<b>PROS</b>: Allows user to edit the Lead and Convert it only when all required fields are filled out. User can easy check out which fields are missing to let them to convert the Lead.<br />
<b>CONS</b>: Required some Force.com development. But final result worth the efforts - Sales Users are happy with the solution which help then save time and meet data quality criteria.<br />
<br />
Solution Code Sample: (please <a href="http://websolo.ca/contact_us/" target="_blank">contact us</a> if you have questions or want to modify this solution to meet your particular business requirements)<br />
<br />
<i><b>Custom Button</b></i><br />
<br />
Label: Convert<br />
Object Name: Lead<br />
Name: ConvertCustom<br />
Behavior: Execute JavaScript<br />
Display Type: Detail Page Button<br />
OnClick JavaScript<br />
<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">var url = "/apex/Lead_Conversion_Validation?id={!Lead.Id}";
var width = "350";
var height = "350";
window.open(url, '','scrollbars=no,resizable=no,status=no,toolbar=no,menubar=no, width=' + width + ',height=' + height + ',left=' + ((window.innerWidth - width)/2) + ',top=' + ((window.innerHeight - height)/2) );
</code></pre>
<br />
<b><i>Visualforce Page: </i></b>Lead_Conversion_Validation<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;"><!--
Version : 1.0
Company : Websolo Inc. (websolo.ca)
Date : 07/2014
Update History :
-->
<apex:page standardController="Lead" showHeader="false" sidebar="false" extensions="LeadConversionValidation">
<style>
h2
{
width: 300% !important;
}
</style>
<script>
window.onload = function(){
window.onblur = function(){window.close();}
if(document.getElementById('sd').innerHTML == "")
{
var ids = document.getElementById('ids').innerHTML;
window.opener.location.href="/lead/leadconvert.jsp?retURL=%2F" + ids + "&id=" + ids;
window.top.close();
}
else
{
setTimeout(function(){
window.close();
}, 10000);
}
};
</script>
<div id="sd" style="display: none;">{!reft}</div>
<div id="ids" style="display: none;">{!leadobjid}</div>
<apex:pageBlock title="Lead Conversion Validation">
<apex:pageBlockButtons location="bottom">
<button onclick="window.close();">Close</button>
</apex:pageBlockButtons>
<apex:pageMessage severity="error" strength="1">
<apex:outputText value="{!error}" escape="false" />
<apex:outputText value="{!sterrmsg}" escape="false" rendered="{!sterr}"/><br />
</apex:pageMessage>
</apex:pageBlock>
</apex:page></code>
</pre>
<br />
<br />
<b><i>Apex Class</i></b>: LeadConversionValidation<br />
(controller - code could be adjusted to meet particular business requirements)<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">/*
Version : 1.0
Company : Websolo Inc. (websolo.ca)
Date : 07/2014
Update History :
*/
public class LeadConversionValidation
{
public id leadobjid {get; set;}
public String error {get; set;}
public String reft {get; set;}
public Boolean sterr {get; set;}
public String sterrmsg {get; set;}
public LeadConversionValidation(ApexPages.StandardController controller)
{
sterr = false;
sterrmsg = 'To convert Lead please change Lead Status to Contacted';
error = 'Please fill out following fields before conversion:<br />';
reft = '';
leadobjid = ((Lead) controller.getRecord()).id;
Lead leadobj = [select Status, LastName, Keywords__c, Financing_Notes__c, Email,Phone,Website,Employees__c,Rapport__c,LeadSource,Range_of_Revenues__c,State,Street,City,PostalCode,Country from Lead where id =: leadobjid ];
if(leadobj.LastName == null ||
leadobj.Keywords__c == null ||
leadobj.Financing_Notes__c == null ||
leadobj.Email == null ||
leadobj.Phone == null ||
leadobj.Website == null ||
leadobj.Employees__c == null ||
leadobj.Rapport__c == null ||
leadobj.LeadSource == null ||
leadobj.Range_of_Revenues__c == null ||
leadobj.State == null ||
leadobj.Street == null ||
leadobj.City == null ||
leadobj.PostalCode == null ||
leadobj.Country == null)
{
reft = '1';
if(leadobj.Keywords__c == null){error = error + '- Keywords<br />';}
if(leadobj.Financing_Notes__c == null){error = error + '- Call Notes (at least 100 characters)<br />';}
if(leadobj.Email == null){error = error + '- Email<br />';}
if(leadobj.Phone == null){error = error + '- Phone<br />';}
if(leadobj.Website == null){error = error + '- Website<br />';}
if(leadobj.Employees__c == null){error = error + '- Employees Range<br />';}
if(leadobj.Rapport__c == null){error = error + '- Rapport<br />';}
if(leadobj.LeadSource == null){error = error + '- Lead Source<br />';}
if(leadobj.LastName == null){error = error + '- Lead Name<br />';}
if(leadobj.Range_of_Revenues__c == null){error = error + '- Range of Revenues<br />';}
if(leadobj.State == null || leadobj.Street == null || leadobj.City == null || leadobj.PostalCode == null || leadobj.Country == null){error = error + '- Full Address<br />';}
}
if(leadobj.Status != 'Contacted')
{
//sterr = true;
if(reft == '1')
{
sterr = true;
}
else
{
error = '';
reft = '1';
sterr = true;
}
}
}
}</code>
</pre>
<br />
<br />
<b><i>Apex Class: </i></b>TestLeadConversionValidation (test coverage)<br />
<pre style="background-color: #eeeeee; border: 1px dashed #999999; color: black; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;"><code style="color: black; word-wrap: normal;">Version : 1.0
Company : Websolo Inc. (websolo.ca)
Date : 07/2014
Update History :
*/
@isTest
private class TestLeadConversionValidation
{
static testMethod void myUnitTest()
{
Lead ld = new Lead();
ld.LastName = 'test';
ld.Company = 'test';
ld.Email = 'test@test.com';
ld.Phone = '1112223456';
insert ld;
LeadConversionValidation obj = new LeadConversionValidation(new ApexPages.StandardController(ld));
}
}</code>
</pre>Anonymousnoreply@blogger.com11