Friday, 25 August 2017

Auto-populate emails in "Additionalto,BCC" section of Conga Composer email sending window

Hello Awesome Developers,

Today I am coming with the interesting blog of ADMIN+DEVELOPMENT thing.

I hope all of you know about the document generation tool conga composer which
merge the data into templates for pixel-perfect proposals, quotes and more in Word,
PowerPoint, Excel, HTML emails & PDF's.

How conga composer work?
1) Configure template, email template, conga query etc with conga field format
2) Create custom button to launch the conga app along with required parameters
    like templateId etc

You can visit https://support.getconga.com for more details on this.

Our Scenario :
We have following custom object structure :
 Class__c                                                                   Participants__c
 Name                                                                        Contact - (Lookup to contact)
                                                                                   Class - (Lookup to Class__c)

So 1 class can have many participants.

Before the class begins the user needs to send the announcement email to participants.
For example, Salesforce Training is the class with 10 enrollments.Before class begins
user suppose to send announcement email to all of the participant's using configured
conga email template.

General Conga Configuration :
1. Configure conga email templates with conga fields.(Field coming from Class object (Merge Field))
2. Create a custom detail page button on Class__c object with content source as URL 
   and enter the URL (This you will find in conga documentation)
    For conga 8 it is :

/apex/APXTConga4__Conga_Composer?serverUrl={!API.Partner_Server_URL_290}&id={!Class__c.Id}

3. This will result as follows :




4. On-click of this button the conga composer window will launch and you will need
    to select the template and click "Merge and Email" producing the following result





Our Challenge : 

Auto-populate the "to" or "AdditionalTo" or "BCC" 
with Emails of Enrollments.

As per conga documentation, you can add following parameters in the custom button URL
1) EmailToId : Contact Or LeadId
2) EmailAdditionalTo : Email Address

Since the number of enrollments (with contact lookup) are not fixed so we can not use option 1.

In the option 2, we can give comma separated list of emails
(In our case List of Enrollment__r.Contact__r.Email)

Solution :
1. Create a long text field (Enrollment Emails) at a Class level which will store a comma
    separated list of emails captured from the enrollments once it's inserted, updated, deleted.

2. Trigger on Enrollment to update the class with the enrollment emails.

3. Change the conga button URL to :

/apex/APXTConga4__Conga_Composer?serverUrl={!API.Partner_Server_URL_290}&id={!SFDC_Class__c.Id}&EmailAdditionalTo={!Class__c.Enrollment_Emails__c}


Above will result in the following :



In case you want to emails in BCC then use EmailBCC parameter.


Trigger code you will find at this link : 

https://docs.google.com/document/d/1W85Z_Iqdcqq9Sq9W8Bq42vBdOmCUjBar5gQE0I1D6eQ/edit?usp=sharing

The above solution will work in lighting as well :)

Happy Coding !!!! 







Friday, 11 August 2017

Multi VF Page Redirects Using Single Controller

Hi Awesome Developers,

Have you ever thought of redirecting to multiple pages using a single controller, here is what I have come up with the scenarios of redirecting to different pages using same controller.

Scenario 1:
Main Page
<apex:page standardController="Account" extensions="PageRedirectDemo">
    <apex:form>
         <apex:inputtext value="{!strReceivedText}" /> <br/>
         <apex:inputtext value="{!objAccount.Id}" />
         <apex:commandbutton action="{!redirect}" value="Redirect"/>
   </apex:form>
</apex:page>

Controller :
public with sharing class PageRedirectDemo
{
   public string strReceivedText{get;set;}
   public Account objAccount{get;set;}

   public PageRedirectDemo(ApexPages.StandardController stdController){
         objAccount = (Account)stdController.getRecord();
   }

   public pagereference redirect(){
      PageReference ref = new PageReference('/apex/RedirectedPage'+'?id='+objAccount.Id);
      ref.setRedirect(false);
      return ref;
   }

}

Redirected Page
<apex:page standardController="Account" extensions="PageRedirectDemo">
 <apex:outputLabel>Value received from Page 1</apex:outputLabel> <br/>
 <apex:outputText> {!strReceivedText } </apex:outputText>
</apex:page>

Output :











The above will work well if I keep the signature of second page same as first. Now I have changed the signature of the second page as below and added empty constructor in the controller.

<apex:page controller="PageRedirectDemo">
 <apex:outputLabel>Value received from Page 1</apex:outputLabel><br/>
 <apex:outputText> {!strReceivedText } </apex:outputText> <br/>
  <apex:outputtext value="{!objAccount.Id}" />
</apex:page>

public with sharing class PageRedirectDemo
{
   public string strReceivedText{get;set;}
   public Account objAccount{get;set;}

   public PageRedirectDemo(){
   }
   public PageRedirectDemo(ApexPages.StandardController stdController){
         objAccount = (Account)stdController.getRecord();
   }

   public pagereference redirect(){
      PageReference ref = new PageReference('/apex/RedirectedPage'+'?id='+objAccount.Id);
      ref.setRedirect(false);
      return ref;
   }

}

You can see the values are not set due to change in signature,










Scenario 3 :
MainPage :
<apex:page controller="PageRedirectDemo">
    <apex:form>
         <apex:inputtext value="{!strReceivedText}" /> <br/>
         <apex:commandbutton action="{!redirect}" value="Redirect"/>
</apex:form>
</apex:page>

public with sharing class PageRedirectDemo
{
   public string strReceivedText{get;set;}
   public PageRedirectDemo(){
   }

   public pagereference redirect(){
      PageReference ref = new PageReference('/apex/RedirectedPage');
      ref.setRedirect(false);
      return ref;
   }

}

RedirectedPage
<apex:page controller="PageRedirectDemo">
 <apex:outputLabel>Value received from Page 1</apex:outputLabel><br/>
 <apex:outputText> {!strReceivedText } </apex:outputText> <br/>
</apex:page>

Output :













*IMP : Observe the URL in each scenario :)

Happy Redirecting ;)


Make custom button calling web-service static method working in Lightning

Hi Folks,

Recently we switched our classic Salesforce edition into Lightning edition.

The custom button calling visual-force page worked fine in lightning but the custom button executing Javascript or calling the Webservice method are not even displayed on the page layout as it's a known thing in Lightning.

We have a custom button calling Webservice method and we have two options to make it working in lightning.
1. Create a lighting component and call it using quick action. (For this we need to enable custom domain)

2.We can call a visual-force page and execute our method using the action attribute.
   (No need to enable custom domain)

Let us see how point 2 will actually work :

Suppose we have following code in the custom button
--------------------------------------------------------------------------------------------------------------------
{!REQUIRESCRIPT("/soap/ajax/29.0/connection.js")}
{!REQUIRESCRIPT("/soap/ajax/29.0/apex.js")}

sforce.apex.execute("SurveySender","sendSurvey",{OppId:"{! Opportunity.Id}"});
alert("Survey Sending Started");


Apex Class :
global class SurveySender {
    webservice static void sendSurvey(Id OppId) {
       system.debug('>>>>>>>>>Method Executed>>>>>>>>>>>'+pOppId);            
    }
}
---------------------------------------------------------------------------------------------------------------------

To work it in lightning :

1. Convert global class as below :

global class SurveySender {

   public Opportunity objOpp;

   global SurveySender(ApexPages.StandardController stdController){
        objOpp = (Opportunity)stdController.getRecord();
   }
   
    public pagereference sendEmail(){
      sendSurvey(objOpp.Id);
      return new pagereference(url.getsalesforcebaseurl().toexternalform()+'/'+objOpp.Id);
    }

   webservice static void sendSurvey(Id OppId) {
       system.debug('>>>>>>>>>Method Executed>>>>>>>>>>>'+OppId);            
    }
}


2. Create a Visualforce page with a standard controller :

<apex:page standardController="Opportunity" extensions="SurveySender"
   action="{!sendEmail}">
</apex:page>

3. Convert custom button behavior to Display in existing window without sidebar or header
    Select content source to the Visualforce page created in step 2.

4. Now the button will behave same like the webservice method.


Happy Coding !!!!



Monday, 26 December 2016

It's always shallow copy :)

Hi Awesome Folks,

All of us are aware of the clone method of the sobject class.

I got a requirement to clone the object of the class. I thought the clone will be deep copy but to my surprise its always shallow copy.

public class Demo
{
 
    List<Account> lstAcc;

    public Demo(){
        lstAcc = new List<Account>();
        lstAcc.add(new Account(Name='Ajay'));
    }
 
    public void doclone(){
 
      system.debug('>>>>>>>>this'+this.lstAcc[0]);
   
      Demo objClone = this.clone();
      system.debug('>>>>>>>>clone'+objClone);
 
      objClone.lstAcc [0].Name = 'Vijay';
      system.debug('>>>>>>>>this.clone'+objClone.lstAcc[0]);
      system.debug('>>>>>>>>this'+this.lstAcc[0]);
    }
}

Output :
>>>>>>>>thisAccount:{Name=Ajay}

>>>>>>>>cloneDemo:[lstAcc=(Account:{Name=Ajay})]

>>>>>>>>this.cloneAccount:{Name=Vijay}

>>>>>>>>thisAccount:{Name=Vijay}


From output you can see change in the clone object modifying the main object also.

Keep Coding !!!!

Tuesday, 11 October 2016

Building Dynamic Triggers and Fieldset !!

Hi Folks,

Isn't it awesome if you are able to create trigger and fieldset dynamically :)

1) Create Trigger Dynamically :
map<string,string> mapinfo= new map<string,string>();
mapinfo.put('Name','AccountTrigger');
mapinfo.put('TableEnumOrId','Account');
mapinfo.put('Body','trigger AccountTrigger on Account(after insert){}');
String serialized = JSON.serialize(mapinfo);
         
Httprequest req = new HttpRequest();
req.setEndpoint('https://ajaytrailhead-dev-ed.my.salesforce.com/services/data/v27.0/sobjects/ApexTrigger'); // Enter the org.
req.setMethod('POST');
req.setHeader('Content-Type','application/json');
req.setHeader('Authorization','Bearer '+UserInfo.getSessionID());
req.setBody(serialized);

Http httpReq = new Http();
HttpResponse res = httpReq.send(req);
System.debug(res.getBody());

2) Create FieldSet Dynamically :

To make it possible you need to add the following class to your org.

https://github.com/financialforcedev/apex-mdapi/blob/master/apex-mdapi/src/classes/MetadataService.cls

Use below code to create fieldset dynamically :

public with sharing class DynamicFieldSetCreator
{
public static MetadataService.MetadataPort createService()
{
 MetadataService.MetadataPort service = new MetadataService.MetadataPort();
 service.SessionHeader = new MetadataService.SessionHeader_element();
 service.SessionHeader.sessionId = UserInfo.getSessionId();
 return service;
}
   
public static void createFieldSet()
{
        MetadataService.MetadataPort service = createService();

MetadataService.FieldSet fieldSet = new MetadataService.FieldSet();
        fieldSet.fullName = 'Account.MyFieldSet'; // You can use custom object also.                                                                                                             //Mycustomobject__c.MyFieldSet
        fieldSet.label = 'My FieldSet';
        fieldSet.description = 'Demo fieldset';

        MetadataService.FieldSetItem myAvailableField = new MetadataService.FieldSetItem();
        myAvailableField.field = 'Name'; // Add standard/custom fields to be included in fieldset
        myAvailableField.isFieldManaged = true;
        myAvailableField.isRequired = true;

        fieldSet.availableFields = new List<MetadataService.FieldSetItem>();
        fieldSet.availableFields.add(myAvailableField);

        List<MetadataService.SaveResult> results =
            service.createMetadata(new MetadataService.Metadata[] { fieldSet });
        handleSaveResults(results[0]);
}

public static void handleSaveResults(MetadataService.SaveResult saveResult)
    {
        if(saveResult==null || saveResult.success)
            return;

        // Handle Errors
        if(saveResult.errors!=null)
        {
            List<String> Erros = new List<String>();
            Erros.add(
                (saveResult.errors.size()==1 ? 'Error ' : 'Errors ') +
                    'occured processing component ' + saveResult.fullName + '.');
            for(MetadataService.Error error : saveResult.errors)
                Erros.add(
                    error.message + ' (' + error.statusCode + ').' +
                    ( error.fields!=null && error.fields.size()>0 ?
                        ' Fields ' + String.join(error.fields, ',') + '.' : '' ) );
            if(Erros.size()>0)
            {
                system.debug('>>>>>>>>>>>>>Here');
               
             }
        }
        if(!saveResult.success)
        {
            system.debug('>>>>>>>>>>>>>HereAlso');
        }
    }
}

Keep Coding !!!!



Tuesday, 9 August 2016

Stuck with Salesforce Limited Feed Tracking.....Here you go with UNLIMITED Feed Tracking.......

Hi Awesome Salesforce Developers,

Recently I got a requirement where a client wants feed tracking for every change that Users are doing.Salesforce Limits Feed Tracking to 20 fields per object.
I have used FieldSet and After Update trigger on object (for which we want to do feed tracking) to overcome the 20 field limit.

What is feed tracking ? 
The below salesforce documentation link will explain in detail : 
https://help.salesforce.com/apex/HTViewHelpDoc?id=collab_feed_tracking_overview.htm&language=en

Example : Account Feed tracking 

IMP : Enable Feed Tracking by : 
setup -> Chatter -> FeedTracking -> Select Account -> Click on Enable Feed Tracking

Step 1) Create field set on Account called AccountChatter. Add the fields you want to track in feed tracking.

Step 2) Create the following after update trigger on Account : 

trigger AccountChatter on Account (after update) 
{
 List<Schema.FieldSetMember> lstTrackedFields = SObjectType.Account.FieldSets.AccountChatter.getFields();

 if (lstTrackedFields.isEmpty()) return; 
    
 List<FeedItem> lstFieldChanges = new List<FeedItem>();
    
 if(!trigger.isUpdate) return;
    
for (Account objNewAccount : trigger.new) 
{

 final Account oldAccount = trigger.oldmap.get(objNewAccount.Id);
    // Iterate over all fields in Fieldset 
 for (Schema.FieldSetMember objField : lstTrackedFields) 
 {
String fieldName  = objField.getFieldPath();
String fieldLabel = objField.getLabel();

if (objNewAccount.get(fieldName) == oldAccount.get(fieldName))
continue;

String oldValue = String.valueOf(oldAccount.get(fieldName));
String newValue = String.valueOf(objNewAccount.get(fieldName));

if (oldValue != null && oldValue.length()>255) 
oldValue = oldValue.substring(0,255);

if (newValue != null && newValue.length()>255) 
newValue = newValue.substring(0,255); 

FeedItem post = new FeedItem();
post.ParentId = objNewAccount.Id; // RecordId
post.Body = UserInfo.getName()+' changed '+fieldLabel+' from '+oldValue +' to '+newValue ;

lstFieldChanges.add(post);
  }
}

if (!lstFieldChanges.isEmpty()) insert lstFieldChanges;

}

Output : 
Add fields in FieldSet :















Create Record in Account with Name : Ajay test chatter 

Now update the record with Name : Ajay test verified





















The above solution implemented by taking idea from below post :
(To overcome the limit of History Tracking)
http://salesforce.stackexchange.com/questions/39956/what-is-the-best-workaround-for-the-20-field-history-tracking-cap

Happy Coding !!!

Cheers !!



Tuesday, 26 July 2016

Integrating Significant Ecommerce and Helpful Platforms with Salesforce

Hi Awesome Salesforce Developers,

Recently I did custom integrations with shopify,bill.com,LiveChat,Helpscout so sharing with you authentication request/response formats.

Integration with Shopify.com 
(Using private app in Shopify)
Shopify is a complete e-commerce solution that allows you to set up an online store to sell your goods. It lets you organize your products, customize your storefront, accept credit card payments, track and respond to orders — all with a few clicks of the mouse.

Firstly you need to create private app in Shopify.com which will generate the API key , Password,Shared secret

You need to use those keys for authentication purpose and while sending REST requests from salesforce : 

Shopify API Documentation : https://docs.shopify.com/api

Example : Create customer in Shopify.com from salesforce 
API : https://help.shopify.com/api/reference/customer#create

POST Request from salesforce  :

JSONGenerator gen = JSON.createGenerator(true);
gen.writeStartObject();
 gen.writeFieldName('customer');
 gen.writeStartObject();
   gen.writeStringField('email', Contact.Email);        
   gen.writeStringField('first_name', Contact.FirstName);
   gen.writeStringField('last_name', Contact.LastName);             
 gen.writeEndObject();
gen.writeEndObject();

string strRequest = gen.getAsString();

Http objHttp = new Http();

HttpRequest request = new HttpRequest(); 
request.setMethod('POST'); 
Blob headerValue = Blob.valueOf('your apikey: your password');
String authorizationHeader = 'Basic ' + EncodingUtil.base64Encode(headerValue);
request.setHeader('Authorization', authorizationHeader);
request.setHeader('Content-Type', 'application/json');
request.setBody(strRequest);
request.setEndpoint('https://apikey:password@nameofyourshop.myshopify.com/admin/customers.json'); 

HttpResponse res = objHttp.send(request);  

Map<String, Object> mapResponse =                                                                                   (Map<String,Object>)JSON.deserializeUntyped(res.getBody());

Map<String, Object> mapCustomerData =
                       (Map<String, Object>)mapResponse.get('customer');

if(mapCustomerData .containskey('id'))
system.debug('>>>Shopify='+string.valueof(mapCustomerData.get('id'));)



Integration with Bill.com
Bill.com is a US-based cash flow management software system, provided as a software as a service that integrates with accounting and banking systems. It is intended to serve as a command and control dashboard for cash flow, by businesses, accounting firms and banks.

Documentation : http://developer.bill.com/api-documentation/api/bill

Every API request needs a valid session id, which is obtained by issuing a Login request. Login API takes in an organization id, developer (or application) key, and user credentials (or Token), and returns a valid session id, as well as an end-point URL, which should be used for any subsequent API requests. This session id will remain valid for up to 35 minutes without activity. If the session becomes inactive, you will have to issue a new Login request and obtain a new session ID. You can explicitly end the session by issuing a Logout request.

You need Bill.com -> username,password,OrganizationId,Devkey which can obtain from bill.com account / support.

Login Request :
String strLoginRequest = 'userName='+'Bill_com_Username'+'&password='+'Bill_com_Password'
                   +'&orgId='+'Bill_com_OrgId'+'&devKey='+'Bill_com_Devkey'+';

HttpRequest objHttpReq = new HttpRequest();
objHttpReq.setHeader('Content-Type','application/x-www-form-urlencoded');
objHttpReq.setEndpoint('EndPOINTURL'+'/Login.json');
objHttpReq.setMethod('POST');
objHttpReq.setbody(strLoginRequest);

Http http = new Http();
HTTPResponse objHttpResp = http.send(objHttpReq);     

if (objHttpResp.getStatusCode()==200)
{
  Map<String,object> mapLoginResp = (Map<String,object>)JSON.deserializeUntyped(objHttpResp.getBody());
   
 if(mapLoginResp.containsKey('response_status') &&   Integer.valueOf(mapLoginResp.get('response_status'))==0)
 {
  if (mapLoginResp.containsKey('response_data') && 
                    mapLoginResp.get('response_data') instanceof map<string,object>)
   {   
   Map<String,Object> mapLoginRespData =  (Map<String,Object>)mapLoginResp.get('response_data');
   
   if(mapLoginRespData.containskey('apiEndPoint') )
   { 
    system.debug('>apiEndPoint='+string.valueof(mapLoginRespData.get('apiEndPoint')));
     system.debug('>sessionId='+string.valueof(mapLoginRespData.get('sessionId')));
     system.debug('>orgId='+string.valueof(mapLoginRespData.get('orgId')));
     system.debug('>usersId='+string.valueof(mapLoginRespData.get('usersId')));
   }
   }  
 }
}


The session Id,API end point to need to use for making next requests:

For example :Create Bill request : 

HttpRequest objHttpReq = new HttpRequest();
objHttpReq.setEndpoint('ENDPOINTURL');
objHttpReq.setMethod('POST');
objHttpReq.setbody('devKey='+'DEVKEY'+'&sessionId='+'SESSION ID RECEIVED from LOGIN'+'&orgId='+'Bill_com_OrgId'+'&data='+'RequestBODY');
objHttpReq.setHeader('Content-Type','application/x-www-form-urlencoded');       
Http http = new Http();
HTTPResponse objHttpResp = http.send(objHttpReq); 



Integration with LiveChat :
LiveChat is an online customer service software with live support, help desk software and web analytics capabilities. It was first launched in 2002 and since then it is developed and offered in SaaS (software as a service) business model by LiveChat Software.

Documentation : https://developers.livechatinc.com/rest-api/

Request to get all chats :
Livechat give the response in pages so intially we need to make request with page=1 and get a total number of pages. Do the requests till you reach the last page.

    integer intPageCountFromBatch = 1;

    Http objHttp = new Http();
    HttpRequest request = new HttpRequest(); 
    request.setMethod('GET'); 
    Blob headerValue = Blob.valueOf('Livechatusername/email:apikey');
    String authorizationHeader = 'Basic ' +    EncodingUtil.base64Encode(headerValue);
    request.setHeader('Authorization', authorizationHeader);
    request.setHeader('Content-Type', 'application/json');
    request.setHeader('X-API-Version','2');
    request.setEndpoint('https://api.livechatinc.com/chats?page='+intPageCountFromBatch);
    request.setTimeout(120000); 
    HttpResponse res = objHttp.send(request);  

if (res.getStatus() == 'OK')
{
  Map<String, Object> mapResponse =
          (Map<String, Object>)JSON.deserializeUntyped(res.getBody());

  if (mapResponse.containsKey('pages'))
  intTotalPageCount = String.valueOf(mapResponse.get('pages')) != null ? Integer.valueOf(String.valueOf(mapResponse.get('pages'))) : 1 ;
      
List<Object> lstChatData =(List<Object>)mapResponse.get('chats');
   
}

IMP : You need to make callout till you reach the last page number.



Integration with HelpScout :
Help Scout is a simple help desk designed for small businesses. 

Documentation : http://developer.helpscout.net/help-desk-api/

integer intPageCountFromBatch = 1;

Http objHttp = new Http();
HttpRequest request = new HttpRequest(); 
request.setMethod('GET'); 
Blob headerValue = Blob.valueOf('APIKey:Password');
String authorizationHeader = 'Basic ' + EncodingUtil.base64Encode(headerValue);
request.setHeader('Authorization', authorizationHeader);
request.setHeader('Content-Type', 'application/json');
request.setEndpoint('https://api.helpscout.net/v1/mailboxes/YourMailboxId/conversations.json?page='+intPageCountFromBatch); 

HttpResponse res = objHttp.send(request);
request.setTimeout(120000); 

if (res.getStatus() == 'OK')
{
Map<String, Object> mapResponse = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());

if (mapResponse.containsKey('pages'))
  intTotalPageCount = String.valueOf(mapResponse.get('pages')) != null ? Integer.valueOf(String.valueOf(mapResponse.get('pages'))) : 1 ;

 List<Object> lstChatData = (List<Object>)mapResponse.get('items');
}



IMP : You need to make callout till you reach the last page number.


For both LiveChat & HelpScount you need to implement the batch chain logic.

Feel free to contact for detailed code : ajay.ghuge09@gmail.com

Cheers,
Ajay