Monday, 26 October 2015

Calling Boomi process from Salesforce page and getting the data from SAP environment using HTTP Request


Salesforce
The topic which i am covering here to show the PDF on apex page, this is very simple but the main thing over here is that i need to fetch the data from SAP system through Boomi and than after it takes the data from Boomi in Salesforce than it would show as PDF. So first i am showing my apex page code here. The reason being I am using dynamic rendering is that if there would be no data to show than it should show the blank white page with appropriate message, if I would not use the condition than if it would not contain any data than it is showing blank BLACK pdf style without data.
<apex:page controller="GenerateInvoicePdf" sidebar="false">
     <apex:pageMessages />
<script>
        var isPDF = '{!bJSEnable}';
        if (isPDF=='true')
        {
           window.location.href = "data:application/pdf;base64,{!sData}";
        }
    </script>
</apex:page>
I am showing you the controller of this page, in this controller, I using HTTP call to pass the request envelop from Salesforce to Boomi (Integration tool) and get the response envelope from Boomi to saleforce. This below function returns HTTPResponse. First it is getting Endpoint from custom lable.
public static HttpResponse getInfoBoomi()
    {
        // Get the Endpoint URL from custom label     
        String EndPointUrl = Label.CGI_Invoice_Pdf_Boomi_Endpoint_Url; 
        Httprequest request=new Httprequest();
        request.setmethod('POST');
        request.setTimeout(120000);
        request.setEndpoint(EndPointUrl);    
        //set the request string for Boomi service connector containing Invoice No which will than pass to SAP to get the Invoice Base64 code
        string strInput='<?xml version="1.0" encoding="UTF-8"?>' +
                        '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' +
                        '   <soapenv:Body> <setOutput xmlns="https://secure.logmeinrescue.com/API/API.asmx"> <eOutput>XML</eOutput></setOutput>' +
                        '      <notifications xmlns="http://soap.sforce.com/2005/09/outbound">' +
                        '         <Notification> ' +
                        '               <InvoceId>'+ ApexPages.currentPage().getParameters().get(CGIConstant.Invoice_PDF_Query_String) +'</InvoceId>' +       
                        '         </Notification>' +
                        '      </notifications>' +
                        '   </soapenv:Body>' +
                        '</soapenv:Envelope>';
                                   
         //replace all & with &amp; to avoid xml encoding issue                     
                                 
         strInput=strInput.replaceAll('&','&amp;');
         request.setBody(strInput);
         Http httprequest=new Http();
         string responseMessage;      
         HttpResponse res;
         //send http request to boomi process
         res=httprequest.send(request);

         return res;
     }

The value of Label.CGI_Invoice_Pdf_Boomi_Endpoint_Url = https://test.connect.boomi.com/ ws/simple/getInvoice; boomi_auth=dGhlY2hhbWJlcmxhaW5ncm91cGluYy1US1pBRDY=. If you noticed that this contains service URL coming from Boomi Web service server operation URL path, see the below image red block, the operation also contains Request profile as well as response profile. Request profile contains the HTTP request envelope coming from Salesforce which is yellow highlighted area in above table. This request receive by Boomi through the below operation. Now the after Simple URL path in above Endpoint URL we need to put “;boomi_auth=” and after that it needs the Account Id and Token concatenate by Colon (:) and then user should convert the whole string into base64 like in our case the Account Id is “TestAccount” and Token is “HGFTH76-JGBVFF876-KJKBV23”. This account info can be get from Boomi Atom Management under Shared Web Server settings. So after getting this information, we need to concatenate both the things and put colon in between them like  TestAccount: HGFTH76-JGBVFF876-KJKBV23 than convert this string into the base 64 and then put that encoded value after boomi_auth in above URL. If you wee in above source code it is passing Invoice Id from apex controller to Boomi using Http request envelop and that Invoice Id is fetching from query string.
FIGURE 1


Now I am going to cover the Boomi Process. If you see below image which is showing whole Process which is actually covering the scenario.
FIGURE 2


On first step, I am using the Web Service Server connector which receive data from Salesforce and as you already seen its operation in figure 1. This operation has request and response part. Request XML contains the same XML meta data schema as Request envelope which I covered in first table containing yellow highlighted area. Response would be contains those XML tags which your SAP system would return. So you can make your response after seeing the response from SAP. In above figure 2, in map part I am mapping the request Invoice Id to SAP request profile, see the below image

FIGURE 3 


For SAP, I have used the BAPI, and consuming the BAPI which contains Request and Response profile.
FIGURE 4 


The important thing here is that how to return the response envelop from Boomi to Salesforce so I have used the Return Document, which actually returns the response to that source which actually send the request. I also wanted to cover the message properties. I am using the service response envelope message into the message node in which I am passing the whole encoded base 64 Invoice PDF from SAP in between the Ack tag please see the below image.



Actually I have a Generate Invoice button in Invoice detail page which open this above apex page as a pop passing Invoice Number and this Invoice Number than passed to the SAP through Boomi. So when user click this button and get the proper response than we get the status code 200 which is Http success response. There are few more generic responses which might user get, so developer should maintain the proper error handling for end user. I am pasting the whole controller code here which will guide you through the end.
public with sharing class GenerateInvoicePdf{
    transient Blob bString;
    public string sData  {get; set;}
    public Boolean bJSEnable  {get; set;}
    public static HttpResponse getInfoBoomi()
    {
        // Get the Endpoint URL from custom label     
        String EndPointUrl = Label.CGI_Invoice_Pdf_Boomi_Endpoint_Url; 
        Httprequest request=new Httprequest();
        request.setmethod('POST');
        request.setTimeout(120000);
      
        request.setEndpoint(EndPointUrl);    
        //set the request string for Boomi service connector containing Invoice No which will than pass to SAP to get the Invoice Base64 code
        string strInput='<?xml version="1.0" encoding="UTF-8"?>' +
                        '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' +
                        '   <soapenv:Body> <setOutput xmlns="https://secure.logmeinrescue.com/API/API.asmx"> <eOutput>XML</eOutput></setOutput>' +
                        '      <notifications xmlns="http://soap.sforce.com/2005/09/outbound">' +
                        '         <Notification> ' +
                        '               <InvoceId>'+ ApexPages.currentPage().getParameters().get(CGIConstant.Invoice_PDF_Query_String) +'</InvoceId>' +       
                        '         </Notification>' +
                        '      </notifications>' +
                        '   </soapenv:Body>' +
                        '</soapenv:Envelope>';
                                   
         //replace all & with &amp; to avoid xml encoding issue                     
                                 
         strInput=strInput.replaceAll('&','&amp;');
         request.setBody(strInput);
         Http httprequest=new Http();
         string responseMessage;      
         HttpResponse res;
         //send http request to boomi process
         res=httprequest.send(request);

         return res;
        
    }
    public void Initializedata()
     {
         HttpResponse res = getInfoBoomi();
         sData = res.getbody();
         Boolean result = String.isEmpty(sData);
         string extraNodes = '';
         if (res.getStatusCode() == integer.valueof(Label.HTTP_SUCCESS_REQUEST))    
         {
             if (sData.contains(CGIConstant.HTTP_Boomi_Response_Ack_Having_Null)|| sData == '')
             {
                 bJSEnable = false;
                 ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING ,Label.ERR_INV_DOESNOT_EXISTS));
             }
             else
             {
                 if ((sData.IndexOf(CGIConstant.HTTP_Boomi_Response_Ack_Start_Tag) < 0) && (sData.IndexOf(CGIConstant.HTTP_Boomi_Response_Ack_End_Tag) < 0))
                 {
                    bJSEnable = false;
                    ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING ,Label.ERR_INV_DOESNOT_EXISTS));
                 }
                 sData = sData.substring(sData.IndexOf(CGIConstant.HTTP_Boomi_Response_Ack_Start_Tag) + 5);
                 extraNodes = sData.Substring(sData.IndexOf(CGIConstant.HTTP_Boomi_Response_Ack_End_Tag));
                 sData =sData.replace(extraNodes ,'');
                
             }
            
         }
         else if(res.getStatusCode() == integer.valueof(Label.ERR_HTTP_BAD_REQUEST))
             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING , Label.ERR_HTTP_BAD_REQUEST_MESSAGE));
         else if(res.getStatusCode() == integer.valueof(Label.ERR_HTTP_UNAUTHORIZE_REQUEST))
             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING , Label.ERR_HTTP_UNAUTHORIZE_REQUEST_MESSAGE));
         else if(res.getStatusCode() == integer.valueof(Label.ERR_HTTP_NOT_FOUND_REQUEST))
             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING , Label.ERR_HTTP_NOT_FOUND_REQUEST_MESSAGE));
         else if(res.getStatusCode() == integer.valueof(Label.ERR_HTTP_INTERNAL_REQUEST))
             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING , Label.ERR_HTTP_INTERNAL_REQUEST_MESSAGE));
         else if(res.getStatusCode() == integer.valueof(Label.ERR_HTTP_UNAVAILABLE_REQUEST))
             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING , Label.ERR_HTTP_UNAVAILABLE_REQUEST_MESSAGE));
    }
 
    public GenerateInvoicePdf()
    {
       bJSEnable = true;
       Initializedata();
    }
   
 }
In above code if you see, I have covered all the Http request and show them as proper message for the user.

Wednesday, 8 July 2015

Add download button in Add form which download the file

Blackbaud CRM 
I had a scenario in which download functionality needs to be added on Add form. This download button takes File Name as input from the user. The same functionality like we have in normal windows application in which user open a save file dialogue box and then user put the name of file and then save the file. So we were facing issues to actually map the same sort of functionality in BlackBaud. So what I did to make a View form instead of Add form, reason being to actually no need for Save button and in Add form we don’t have any functionality to hide the OOB save button. So, I made a view form like below.



In above picture, File name is an input field, normally we have view only fields in view form so I changed this in HTML page in UI Model and replace the span with input tag. In VB class of UI Model, I needed to actually fetch the data from some data form through DataFormLoadRequest() and put that data into the text file. So, in UI Model, I coded the below lines behind the Export button event

Private Sub _export_InvokeAction(sender As Object, e As InvokeActionEventArgs) Handles _export.InvokeAction
        If String.IsNullOrEmpty(Me.MYEXPORTFILE.Value) Then 'Checking if MYEXPORTFILE Field is empty
            Dim prompt As New UIPrompt()
            prompt.ButtonStyle = UIPromptButtonStyle.Ok
            prompt.Text = "Empyt File Name is not allowed."
            prompt.ImageStyle = UIPromptImageStyle.Information
            Me.Prompts.Add(prompt)
            Exit Sub
        End If

        If Not Me.MYEXPORTFILE.Value.Contains(".") Then
            Dim prompt As New UIPrompt()
            prompt.ButtonStyle = UIPromptButtonStyle.Ok
            prompt.Text = "File Name without Extension not allowed."
            prompt.ImageStyle = UIPromptImageStyle.Information
            Me.Prompts.Add(prompt)
            Exit Sub
        End If


        Dim Filename_Array As String() = Me.MYEXPORTFILE.Value.Trim.Split(New Char() {"."c}) 'spliting MYEXPORTFILE Field value & creating Array of string
        Dim counter As Int32 = 1 'Counter to skip first part of Array in loop

        For Each fern As String In Filename_Array 'Loop on Array to get Extension part in Array
            If Not counter = 1 Then 'skiping first part of Array
                If Not (fern = "ext" Or fern = "txt") Then 'Checing extension of File
                    Dim prompt As New UIPrompt()
                    prompt.ButtonStyle = UIPromptButtonStyle.Ok
                    prompt.Text = "File Extension '" + fern + "' is not Allowed."
                    prompt.ImageStyle = UIPromptImageStyle.Information
                    Me.Prompts.Add(prompt)
                Else
                    Me.DownloadCustomPrompt("MYEXPORTKEY", Me.MYEXPORTFILE.Value)
                End If
            End If
            counter += 1
        Next
    End Sub

The above yellow highlighted line will call the below File download event of UI Model which will actually start the downloading and writes the data into the text file, see the below event source code

Private Sub ExportOMerViewDataFormUIModel_FileDownloadCustom(sender As Object, e As FileDownloadCustomEventArgs) Handles Me.FileDownloadCustom
        If e.Key = "MYEXPORTKEY" Then

            '''''' Lines are commented for OM project business

            Dim omerID As String

            'omerID = Me.ID.Value.ToString
            omerID = Me.RecordId


            If String.IsNullOrEmpty(omerID) Then
                Throw New Exception("OMer Not Found")
            End If
            Dim fileText = String.Empty

            Dim request As New DataFormLoadRequest()
            request.FormID = New Guid(OMEREXPORT_FORMID)
            request.RecordID = omerID
            request.SecurityContext = Me.GetRequestSecurityContext()

            Dim reply As DataFormLoadReply = Nothing
            Try
                reply = DataFormLoad(request, Me.GetRequestContext())
            Catch ex As Exception
                reply = Nothing
            End Try

            If (reply IsNot Nothing) Then
                Dim fieldValue As DataFormFieldValue = Nothing
                If reply.DataFormItem.TryGetValue("FILETEXT", fieldValue) Then
                    Try
                        If fieldValue.Value IsNot Nothing AndAlso fieldValue.Value.ToString().Length > 0 Then
                            fileText = fieldValue.Value.ToString()
                        End If
                    Catch
                    End Try
                End If
            End If
            fileText = fileText.Replace(vbCrLf, vbLf)

            Dim file = fileText.Split(vbCrLf)
            If (Not fileText.Contains(vbCrLf)) Then
                file = fileText.Split(vbLf)
            End If
            Using sr As New IO.StreamWriter(e.Stream)
                For Each x In file
                    sr.WriteLine(x)
                Next
            End Using
        End If
    End Sub

The view data form Spec contains the below mentioned code

                <SPDataForm SPName="USR_USP_DATAFORMTEMPLATE_VIEW_EXPORTOMER">
                                <common:CreateProcedureSQL>
                                                <![CDATA[
create procedure dbo.USR_USP_DATAFORMTEMPLATE_VIEW_EXPORTOMER
(
                @ID uniqueidentifier,
                @DATALOADED bit = 0 output,
  @MYEXPORTFILE varchar(max) = null output
)
as
                set nocount on;
               
                -- be sure to set this, in case the select returns no rows
                set @DATALOADED = 0;
               
                -- populate the output parameters, which correspond to fields on the form.  Note that
                -- we set @DATALOADED = 1 to indicate that the load was successful.  Otherwise, the system
                -- will display a "no data loaded" message.
                select @DATALOADED = 1
   
   
               
                return 0;
                                                ]]>
                                </common:CreateProcedureSQL>
                </SPDataForm>

                <!-- describe fields on the form, which correspond to parameters on the SP.  Note that system paramters
                like the context @ID, @DATALOADED, and @CURRENTAPPUSERID need not be listed. -->
                <common:FormMetaData>
                                <common:FormFields>
      <common:FormField FieldID="ID" Caption="ID" DataType="Guid" Hidden="true" />
      <common:FormField FieldID="MYEXPORTFILE" Caption="File Name" DataType="String" />
                                </common:FormFields>

        <common:WebUIComponent>
            <common:UIModel AssemblyName="Blackbaud.CustomFx.OMer.UIModel.dll" ClassName="Blackbaud.CustomFx.OMer.UIModel.ExportOMerViewDataFormUIModel" />
            <common:WebUI>
                <common:ExternalResource Url="browser/htmlforms/custom/OMER/ExportOMerViewDataForm.html" />
            </common:WebUI>
        </common:WebUIComponent>


    <common:UIActions>
      <common:UIAction ActionID="EXPORT" Caption="Export" />
    </common:UIActions>
  </common:FormMetaData>



Friday, 3 July 2015

Custom PTGL process

Blackbaud CRM 
I had a scenario from one my client that in custom Post to GL process, they want those rows not included in the csv file which contains Zero amount. This can be easily achievable if we have custom class running behind PTGL process which would be calling via Javascript file. But my client only had custom Query view and nothing else and they downloaded their Post to GL file using OOB download file. So that was the problem which i was facing. So, i ran the SQL pro-filer and found one OOB SQL function which solved my issue. That function is called "UFN_POSTTOGLPROCESS_CUSTOMIZE". The original body of this function is

CREATE function [dbo].[UFN_POSTTOGLPROCESS_CUSTOMIZE]   
    (@InRecords UDT_GENERICIDANDBIT readonly, @ID uniqueidentifier)   
    returns @WORKTABLE table   
     (   
     GLTRANSACTIONID uniqueidentifier   
     )      
    with execute as caller   
    as   
    begin   
    /* Products - DO NOT EDIT THIS FUNCTION!  It is only for users to customize their post process*/   
    /* This is a default that just returns the input records */   
       
    insert into @WORKTABLE (GLTRANSACTIONID)   
    select ID from @InRecords   
   
    return   
    end    

So if you analyze above SQL function which is OOB, it only selects all the transaction and return those via table. So I picked that SQL function and alter this SQL function and written my custom logic

ALTER  function [dbo].[UFN_POSTTOGLPROCESS_CUSTOMIZE] 
    (@InRecords UDT_GENERICIDANDBIT readonly, @ID uniqueidentifier) 
    returns @WORKTABLE table 
     ( 
     GLTRANSACTIONID uniqueidentifier 
     )    
    with execute as caller 
    as 
    begin 
    /* Products - DO NOT EDIT THIS FUNCTION!  It is only for users to customize their post process*/ 
    /* This is a default that just returns the input records */ 
   
                insert into @WORKTABLE (GLTRANSACTIONID) 
             select ID from @InRecords  R
                where R.ID in (select ID from GLTRANSACTION where ID = R.ID and AMOUNT <> 0)
                 
 
    return 
    end  


I had filtered only those rows in which amount is not equal to 0 that discarded those rows which contains Zero amount. Because we don’t have any control only if we make a query view, other solution would offcourse a custom class library and filtered all those rows which contains amount equal to zero