Monday, June 29, 2015

How Much Does It Cost To Create A Stand Alone Custom App With The Force.com Platform?

I've worked for a lot of big companies who have had a lot money to spend on SFDC licenses. Including the ones I used.  But recently I had a small client who needed a web app, and I knew SFDC would be a great solution, but I didn't know how much it would cost them.  They had never used SFDC before and therefore had no licenses.  How much would it cost them?  Mind you, they had a limited budget.  I had a devil of a time trying to figure it out.  I now share my experience with you, in the hopes that you won't have to waste as much time as I did...

If you can build the app for your client with 10 custom objects or less and don't use built-in objects (Cases, Leads, etc.) , here is your cheapest solution:

1 Salescloud Enterprise license - $1500/year
Chatter Only (aka Chatter Plus) licenses - $180/year/user

So if your client needs to have 4 people logging in to use the custom system you build, it will cost them $2,200/year (1 Salescloud Enterprise license + 4 Chatter Only licenses) in licensing. If it's just one person, it will cost them $1,680 (1 Salescloud Enterprise license + 1 Chatter Only licenses).

You will be using the Salescloud Enterprise license yourself as the developer to build them the system.  But even when you are done building the system, they will still need to pay for that license every year to keep the system going.

Q&A

Q: Can't I just buy the Force.com Enterprise App license instead of the Salescloud Enterprise License?
A: Nope.  Even though it doesn't say it explicitly, implicit in all the other license types is that you already have at least one Sales Cloud or Service Cloud license.  Even if you are not using any of the built-in objects, you still need this license to do development.

Q: I signed up for the Force.com/Sales Cloud/Service Cloud Trial and I don't see "Apex Classes" or "Sites" or [fill in the blank] under "Develop", are you sure I get those with a Sales Cloud Enterprise license?  (I really need them!)
A: Yep, you do get all of that.  I was told it's a glitch in the Trial version software why some of those options are missing.  When I did buy my Sales Cloud License, this is what I saw (6/29/2015):
Everything I needed!!!

Q: Do I really need the Salescloud Enterprise?  Group and Professional are so much cheaper!!
A: Sorry, you just CANNOT do custom development without an Enterprise license.

Q: What if my app needs more than 10 objects?
A: Then, I believe, you'll have to buy the Force.com App Bundle license at $960/user/year instead of the $180/user/year Chatter Plus licenses (yikes, I know, quite a jump.).  Keep in mind, you'll still need that 1 Sales Cloud Enterprise License at $1500/year too.    But remember, with your 10 object app, you'll have Record Types, and you might be able to squish a few "logical objects" into one "realized object" with many Record Types.

Q: Still $1,680/year for as long as the app it used seems like a lot, shouldn't it be cheaper?
A: Let me put it in perspective.  I've done a lot of .NET development in my life.  I have an EC2 server with Amazon webservices that runs a few web applications.  It uses IIS Server and SQL Server.  It costs me around $1,300/year for just the infrastructure.  And even though I'm a great .NET programmer, .NET development still takes me twice, maybe three times as long to get from concept to production than in SFDC.  Under the right circumstances, SFDC just makes lot more sense.

Tuesday, May 12, 2015

How can I show Field Dependency matrix in Excel

After trying other solutions that were not really working for me, I came up with this approach.  I created some javascript code in to be used in a javascript "bookmarklet".  The idea is, you go to the "Edit Field Dependency" screen in SFDC and click on the bookmarklet and it screenscrapes the page, formatting the data into a flatten csv which you can then paste into notepad and open with Excel.

Here is the javascript that you would save to  your own webserver:
var firstColumn = 0;
        var lastColumn = 0;
        main();

        function main() {

            var curUrl = document.URL;

            if ((curUrl.indexOf('salesforce.com') != -1) || (curUrl.indexOf('localhost:') != -1)) {
                setColumnNumbers();
                var columnsName = getColumnNames();
                var FieldsWithDependencies = getFieldsWithDependencies(columnsName);
                var csvData = getCsv(FieldsWithDependencies);
                openPopup(csvData);
            }
            else {
                alert("Sorry, I don't recognize this page or its format :(");
                return;
            }
        }

        function setColumnNumbers() {
            var navElements = document.getElementsByClassName('navigationHeaderNormal');
            if (navElements[0] === undefined) {
                navElements = document.getElementsByClassName('navigationHeaderAll');
            }
            var navText = navElements[0].innerHTML;
            var startIndex = navText.indexOf('Showing Columns:') + 'Showing Columns:'.length;
            var endIndex = navText.indexOf('-', startIndex);
            var firstColumnStr = navText.substring(startIndex, endIndex);
            startIndex = endIndex + 1;
            endIndex = navText.indexOf('(of');
            var lastColumnStr = navText.substring(startIndex, endIndex);
            firstColumn = parseInt(firstColumnStr.trim()); // 11;
            lastColumn = parseInt(lastColumnStr.trim());;
            firstColumn = firstColumn - 1;
            lastColumn = lastColumn - 1;
        }

        function getCsv(fieldsWithDependencies) {
            var rtnCsv = '';
            var sepChar = ',';
            var crChar = '\r\n';
            for (i = 0; i < fieldsWithDependencies.length; i++) {
                var fieldName = fieldsWithDependencies[i][0];
                var dependencies = fieldsWithDependencies[i][1];
                for (j = 0; j < dependencies.length; j++) {
                    rtnCsv += '"' + fieldName + '"' + sepChar + '"' + dependencies[j] + '"' + crChar;
                }
            }
            return rtnCsv;
        }

        function getFieldsWithDependencies(columnsName) {
            var FieldsWithDependencies = [];
            for (i = 0; i < columnsName.length; i++) {
                var fieldValues = getEnabledRowsForColumn(i + firstColumn);
                var row = [columnsName[i], fieldValues];
                FieldsWithDependencies.push(row);
            }
            return FieldsWithDependencies;
        }

        function getColumnNames() {
            var colCount = firstColumn; // 0;
            var columnNameBase = 'th_r0c';
            var columnNames = [];
            while (colCount <= lastColumn) {
                var cellElement = document.getElementById(columnNameBase + colCount.toString());
                if (cellElement != null) {
                    var cellElementStrippedString = cellElement.innerHTML.replace('&nbsp;', '').replace('&nbsp;', '');
                    if (cellElementStrippedString.length > 0) {
                        columnNames.push(cellElementStrippedString);
                        colCount++;
                    }
                } 
            }
            return columnNames;
        }

        function getEnabledRowsForColumn(colNum) {
            var rowCount = 0;
            var fieldValues = [];
            var cellNameBase = 'te_r{0}c' + colNum;

            while (rowCount < 1000) {
                var cellId = cellNameBase.replace('{0}', rowCount.toString());
                var cellElement = document.getElementById(cellId);
                if (cellElement != null) {
                    if (cellElement.className == 'shownPickValue') {
                        var cellElementStrippedString = cellElement.innerHTML.replace('&nbsp;', '').replace('&nbsp;', '');
                        if (cellElementStrippedString.length > 0) {
                            fieldValues.push(cellElementStrippedString);
                        }
                    }
                    rowCount++;
                } else {
                    rowCount = 1001;
                }
            }
            return fieldValues;
        }

        function openPopup(csvData) {
            var popBox = document.createElement("div");
            popBox.style.backgroundColor = "#81da7a";
            popBox.style.position = "absolute";
            popBox.style.width = "700px";
            popBox.style.top = "50px";
            popBox.style.left = "200px";
            popBox.style.border = "solid 3px white";
            popBox.style.padding = "20px";
            popBox.id = "TTPopupBox";

            var closebutton = document.createElement("div");
            closebutton.id = "ttCloseButton";
            closebutton.style.backgroundColor = "white";
            closebutton.style.width = "70px";
            closebutton.onclick = removePopupBox;
            closebutton.appendChild(document.createTextNode("CLOSE"));
            popBox.appendChild(closebutton);

            var csvTextArea = document.createElement("textarea");
            csvTextArea.style.width = "500px";
            csvTextArea.style.height = "500px";
            csvTextArea.appendChild(document.createTextNode(csvData));
            popBox.appendChild(csvTextArea);

            document.body.appendChild(popBox);
        }


        function removePopupBox() {
            var popBox = document.getElementById("TTPopupBox");
            popBox.parentNode.removeChild(popBox);
        }

Here is the bookmarklet code you would use to call your hosted code:

javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','https://pathtoyourjscode.js?r='+Math.random()*99999999);document.body.appendChild(e)})());

Steps to use:

1. Create a bookmark in your browser with the following info (this example uses my hosted code, which you welcome to use):

Name: SFDC - Field Dep Scrape
URLjavascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','https://s3.amazonaws.com/datawin-wwwroot/SFDC/fieldDepScrape.js?r='+Math.random()*99999999);document.body.appendChild(e)})());

2. Navigate to the "Edit Field Dependency" in SFDC (click on Edit in the Field Dependencies area of the picklist properties).

3. Click the bookmark you created in Step 1.

4. A Popup screen will display the flattened data with column 1 contains the master fields and column 2 the dependent fields.

This will work whether you are viewing all columns or sets of 5.

Hope this helps!

Kenny

Thursday, January 29, 2015

Avoid Multiple Clicks on Longer Loading VisualForce Page

For this you'll need a Static asset like this (in my case it's a "waiting/loading" animated gif):


You'll wrap some divs around your buttons and your animate gif:
<apex:pageBlockButtons >
    <div class="waitingGifDiv" >
        <apex:image id="WaitingGif" value="{!URLFOR($Resource.WaitingGif)}" width="50" height="50" style="float:center-right; "/>
    </div>
    <div class="SubmitButtonDiv">
        <apex:commandButton id="SubmitButton" styleClass="SubmitButton" value="Submit Case" action="{!submitCase}" />
    </div>
</apex:pageBlockButtons>

Then you'll need just a little javascript (in this case with a little jquery help) to first hide the animated gif when the page loads.  Then when the user clicks the button, hide the button and unhide the animated gif.  Simple, right?
  
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script >
      
    j$ = jQuery.noConflict();
    j$(document).ready(function() {
       //code when page is ready 
       j$('.waitingGifDiv').hide();
       
       j$('.SubmitButton').on('click',function() {
           j$('.waitingGifDiv').show();
           j$('.SubmitButtonDiv').hide();
        });
    });
</script>
    

Tuesday, January 20, 2015

Questions for Client BEFORE accepting an engagement.

How many projects?

Do you have a SFDC admin?

Do you have a project manager for each project?  Do you have a project manager at all?  Who is it?

Do you have a business analyst? How many? Who? Dedicated to which project(s)?

Who decides priority on the projects?

Do you have change control?  How does it work?

Do you have a deployment policy?  What is it?

Do you have a documentation policy?  What is it?

Do you have a QA team?

If I am to be the SFDC developer, and admin, and business analyst, and QA, and project manager...do you understand that it multiplies the amount of time it will take me to get things done?

Will SFDC need to integrate with other systems?  What systems?  How is the manager, BA, admin, developer for those systems? It there QA environments for those systems?

Who was the previous SFDC admin/dev/ba/pm?  Why are they no longer on the project?



NOTE: Also, go to http://www.glassdoor.com/ to research the company. Read reviews of what it's like working for the company.  Get the main gist of the complaints and tactfully address those during the meeting.

Thursday, January 15, 2015

Upload Pictures to Case with Apex

Uploading a picture to a Case programatically is a multi-stage process.

First, you'll need a custom field on the Case with a Data Type of Rich Text Area.  In this example, we have one called Photo__c.

You'll also need the "apex:inputfile" component in you Visual Force code, like so:
  

<table>
  <tr>
    <th>Photo #1 (Optional):</th>
    <td><apex:inputfile filename="{!photoForUploadName}" id="photoForUpload" value="{!photoForUpload}"></apex:inputfile></td>
  </tr>
  <tr>
    <th>Photo #2 (Optional):</th>
    <td><apex:inputfile filename="{!photoForUploadName2}" id="photoForUpload2" value="{!photoForUpload2}"></apex:inputfile></td>
  </tr>
</table>

You will also need a Document Folder to use for storing the images (in this example, we use "QAPhotos").

Please note in the code the comments regarding the imageURL

Your apex code will first up save the image to your chosen folder in your Documents object.  It will then based on the newly generated id and create an html string using references to your images as the scr attribute in an img tag:
  
<br>
    public Blob photoForUpload { get; set; }
    public String photoForUploadName { get; set; }
    
    public Blob photoForUpload2 { get; set; }
    public String photoForUploadName2 { get; set; }

    public PageReference createCase() {

               Folder QAPhotosFolder = [Select Id From Folder where Name = 'QAPhotos'];
        
               if(photoForUpload != null){
                    System.Debug('photoForUploadName: ' + photoForUploadName);
                    String photoExtension = photoForUploadName.substring( photoForUploadName.lastindexof('.')+1);
                    System.Debug('photoExtension : ' + photoExtension );
                    Document d= new Document();
                    d.name = 'Photo: ' + photoForUploadName ;
                    d.body=photoForUpload ; // body field in document object which holds the file.
                    d.folderid= QAPhotosFolder.Id;  //folderid where the document will be stored insert d;
                    d.IsPublic = true;
                    d.contenttype='image/' + photoExtension ;
                    d.type= photoExtension ;
                    insert d;
                    /* 
                       Label.Global_DocBaseURL is a label being used as a Global Variable
                       The value of oid will depend on your instance.
                       Open an image in you Documents, to see what your url and oid should be
                       We are using the value: https://c.cs9.content.force.com/servlet/servlet.ImageServer?id={DocumentId}&oid=00DX000000X51xX
                    */
                    String imageURL = Label.Global_DocBaseURL.replace('{DocumentId}',d.id);
                    c.Photo__c = 'Photo 1: <br/><img src="' + imageURL + '"></img>' ;
                }
                
                if(photoForUpload != null){
                    String photoExtension = photoForUploadName2.substring( photoForUploadName2.lastindexof('.')+1);
                    Document d= new Document();
                    d.name = 'Photo: ' + photoForUploadName2 ;
                    d.body=photoForUpload2 ; // body field in document object which holds the file.
                    d.folderid= QAPhotosFolder.Id;  //folderid where the document will be stored insert d;
                    d.IsPublic = true;
                    d.contenttype='image/' + photoExtension ;
                    d.type= photoExtension ;
                    insert d;
                    
                    String imageURL = Label.Global_DocBaseURL.replace('{DocumentId}',d.id);
                    c.Photo__c = c.Photo__c + '<br/><br/>Photo 2: <br/><img src="' + imageURL + '"></img>' ;
                }

                // Insert the case
                INSERT c;
        
    }

Create Knowledge Base Article, Publish It (Or Not), and Attach It To A Case

One best practice for Case Management (Service Cloud) is to attach a Knowledge Base Article when closing a case.

This code is for a circumstance where we want an external VF page to display the Case info and allow the user to enter the Knowledge Base Information that resolves the Case.  The KB info gets saved to SFDC.  Then the association is made between the KB Article and the case.

In our case, we did not want the Article published right away.  This allowed a second set of eyes to review the article, make modifications if necessary, and then publish it.  But the code below shows how to programatically publish the article.
  

public Case c { get; set; }

// QA_Corrective_Action__kav is an ArticleType.  Remember, each Knowledge Base Article gets its own object based on the ArticleType
public QA_Corrective_Action__kav CorrectiveAction { get; set; }

public PageReference updateCase() {
        
        //Clean up the URL a bit
        String UrlName = CorrectiveAction.title.replace(' ','-');
        UrlName = UrlName.replace('#','');
        UrlName = UrlName.replace('&','');
        UrlName = UrlName.replace('?','');
        CorrectiveAction.UrlName = UrlName ;

        //1st Create the article
        insert CorrectiveAction;

        KnowledgeArticleVersion kav = [Select Id, KnowledgeArticleId from KnowledgeArticleVersion  where PublishStatus = 'Draft' and Id=: CorrectiveAction.Id];
        
        //2nd Publish the article (if you want)
        //Comment Out Line Below if you do NOT want the Article published yet.
        KbManagement.PublishingService.publishArticle(kav.KnowledgeArticleId , true);

        
        CaseArticle cArticle = new CaseArticle();
        cArticle.CaseId = c.Id;
        cArticle.KnowledgeArticleId = kav.KnowledgeArticleId ;

        //3rd Create the association between the Article and the Case
        insert cArticle;
        
        
        return new PageReference('/apex/QACaseUpdateThankYou');
        
    }

Saturday, December 20, 2014

Custom Labels as Global Variables

I haven't found the official Salesforce way for creating global variables or configuration properties yet, but this is what I found at one of my clients that I liked.

Say you want to create a global variable to store a public key with a value of "1234xyz".  Just create a new Custom Label with Name = "Global_PublicKey" and Value="1234xyz"



So in Apex you just reference the value as Label.Global_PublicKey:

  
private boolean isAuthorized(){
        String qPublickKey = ApexPages.currentPage().getParameters().get('publicKey');
        return qPublickKey.contains(Label.Global_PublicKey);
}


In a VisualForce page you refence it {!$Label.Global_PublicKey}:

  
<apex:page showheader="false" sidebar="false">
   <apex:pageblock title="Test">
   Global Variable PublicKey = {!$Label.Global_PublicKey}
   </apex:pageblock>
</apex:page>