Approval Email with Attachments — 04/12/2016

Approval Email with Attachments

Welcome to another Salesforce automation tutorial.

As usual, I will explain the challenge and  ‘criticise’ Salesforce for not having a built-in solution. Of cause, in the end, a useful custom solution for this problem will be provided.

Let’s get started.

Challenge: Salesforce comes with a pretty cool approval tool – approval process, it handles most of the business needs, however, I feel like its missing one of the most important features – attaching files with the approval email.

Since there is no way to hack the standard feature, I decided to explore “Salesforce Files” instead of “Salesforce Attachments”. (Yes, they are different)

What I have noticed is that Salesforce files can be shared externally via public facing links, maybe we can include this in the approval email to allow approvers to view/download files similar to how people download email attachments.

To achieve this, we are going to need:

  1. A custom field to hold the links
  2. An HTML email template to include the custom field
  3. An apex trigger to:
    1. create an external link for each attachment
    2. update the custom field with the new links
    3. remove links when files are deleted

A custom field is really easy, just create a custom field under the object with type = rich text area.

screen-shot-2016-12-03-at-5-31-32-pm

Next step, we create an email template that includes this new field. (Too easy, not going to include any screenshots…)

Now the fun part, apex trigger.

After going through the documentation, I realised it’s not as simple as I imagined. This apex trigger not only takes care of generating and removing public links but also writes the links back to our custom field in HTML format.


List<ID> ContentDocumentIDs = new List<ID>();
List<ID> yourObjectIDs = new List<ID>();

/* ====== creating public facing urls for files after link insert ====== */
if (Trigger.isInsert){
  String tmp;
  // adding qualified files to the list - YourObject
  for (ContentDocumentLink cdl : Trigger.New){
    tmp = cdl.LinkedEntityId;
    // need to use Left to build links only for related objects (e.g. opportunity)
    if (tmp != null && tmp.left(3) == 'xxx'){
      ContentDocumentIDs.add(cdl.ContentDocumentId);
      yourObjectIDs.add(tmp);
    }
  }
  // list of content documents
  List<ContentDocument> cdList = [
    select id, Title, LatestPublishedVersionId
    from ContentDocument
    where id in: ContentDocumentIDs
  ];

  // list of public links
  List<ContentDistribution> cds = new List<ContentDistribution>();
  for (ContentDocument c : cdList){
    ContentDistribution cd = new ContentDistribution(
        name = c.Title + ' - ' + date.today().format(),
        ContentVersionId = c.LatestPublishedVersionId
    );
    cdList.add(cd);
  }
  // creating links
  insert cdList;
}
else if (Trigger.isDelete){
  System.debug('YourObject Content Sharing Trigger Starts');
  String tmp;
  // adding qualified files to the list - YourObject
  for (ContentDocumentLink cdl : Trigger.Old){
    tmp = cdl.LinkedEntityId;
    if (tmp != null && tmp.left(3) == 'a1V'){
        ContentDocumentIDs.add(cdl.ContentDocumentId);
        yourObjectIDs.add(tmp);
    }
  }
  delete [
    select id, Title, LatestPublishedVersionId
    from ContentDocument
    where id in: ContentDocumentIDs
  ];
}

/* ====== Now writing links back to Your Objects ====== */

if (!yourObjectIDs.isEmpty()){
  System.debug('Writting back to Your Objects');
  // reset attachment list in Your Objects
  List<ContentDocumentLink> ContentDocumentLinks = [
    select id, LinkedEntityId, ContentDocumentId
    from ContentDocumentLink
    where LinkedEntityId in :yourObjectIDs
  ];
  System.debug('ContentDocumentLinks: ' + ContentDocumentLinks);
  // Your Object ID -> ContentDocument IDs
  Map<ID, List<ID>> ContentDocumentMap = new Map<ID, List<ID>>();
  List<ID> ContentDocumentIds = new List<ID>();
  for (ContentDocumentLink ContentDocumentLink : ContentDocumentLinks){
    ContentDocumentIds.add(ContentDocumentLink.ContentDocumentId);
    if (ContentDocumentMap.containsKey(ContentDocumentLink.LinkedEntityId)){
      ContentDocumentMap.get(ContentDocumentLink.LinkedEntityId).add(ContentDocumentLink.ContentDocumentId);
    }
    else{
        ContentDocumentMap.put(ContentDocumentLink.LinkedEntityId, new List<ID>{ContentDocumentLink.ContentDocumentId});
    }
}

List<ContentDocument> ContentDocuments = [
  select id, Title, LatestPublishedVersionId
  from ContentDocument
  where id in: ContentDocumentIds
];
System.debug('ContentDocuments: ' + ContentDocuments);
List<ID> LatestPublishedVersionIds = new List<ID>();
for (ContentDocument ContentDocument : ContentDocuments){
    LatestPublishedVersionIds.add(ContentDocument.LatestPublishedVersionId);
}

List<ContentDistribution> ContentDistributions = [
  select id, Name, DistributionPublicUrl, ContentDocumentId, ContentVersionId
  from ContentDistribution
  where ContentVersionId in: LatestPublishedVersionIds
];
System.debug('ContentDistributions: ' + ContentDistributions);
// ContentDocumentId -> ContentDistribution
Map<ID, ContentDistribution> ContentDistributionMap = new Map<ID, ContentDistribution>();
for (ContentDistribution ContentDistribution : ContentDistributions){
    ID tmpDocId = ContentDistribution.ContentDocumentId != null ? ContentDistribution.ContentDocumentId : ContentDistribution.ContentVersionId;
    ContentDistributionMap.put(tmpDocId, ContentDistribution);
}
// Your Object ID -> List<ContentDistribution>
Map<ID, List<ContentDistribution>> pMap = new Map<ID, List<ContentDistribution>>();
List<Purchase_Order__c> Your Objects = [
  select id, Attachment_List__c
  from purchase_order__c
  where id in: yourObjectIDs
];
System.debug('Your Objects: ' + Your Objects);
String attachmentString = '';
For (Purchase_Order__c YourObject : Your Objects){
    if (ContentDocumentMap != null && ContentDocumentMap.size() > 0){
        for (ID ContentDocumentId : ContentDocumentMap.get(YourObject.id)){
            attachmentString += '
	<li><a href="' + ContentDistributionMap.get(ContentDocumentId).DistributionPublicUrl + '">' + ContentDistributionMap.get(ContentDocumentId).Name + '</a></li>
';
        }
    }
    YourObject.Attachment_List__c = attachmentString;
}

update Your Objects;

 /* ====== Now writing links back to Your Objects ====== */

if (!yourObjectIDs.isEmpty()){
  System.debug('Writting back to Your Objects');
  // reset attachment list in Your Objects
  List<ContentDocumentLink> ContentDocumentLinks = [
    select id, LinkedEntityId, ContentDocumentId
    from ContentDocumentLink
    where LinkedEntityId in :yourObjectIDs
  ];
  System.debug('ContentDocumentLinks: ' + ContentDocumentLinks);
  // Your Object ID -> ContentDocument IDs
  Map<ID, List<ID>> ContentDocumentMap = new Map<ID, List<ID>>();
  List<ID> ContentDocumentIds = new List<ID>();
  for (ContentDocumentLink ContentDocumentLink : ContentDocumentLinks){
    ContentDocumentIds.add(ContentDocumentLink.ContentDocumentId);
      if (ContentDocumentMap.containsKey(ContentDocumentLink.LinkedEntityId)){
        ContentDocumentMap.get(ContentDocumentLink.LinkedEntityId).add(ContentDocumentLink.ContentDocumentId);
      }
      else{
        ContentDocumentMap.put(ContentDocumentLink.LinkedEntityId, new List<ID>{ContentDocumentLink.ContentDocumentId});
      }
  }

  List<ContentDocument> ContentDocuments = [
    select id, Title, LatestPublishedVersionId
    from ContentDocument
    where id in: ContentDocumentIds
  ];
  System.debug('ContentDocuments: ' + ContentDocuments);
  List<ID> LatestPublishedVersionIds = new List<ID>();
  for (ContentDocument ContentDocument : ContentDocuments){
    LatestPublishedVersionIds.add(ContentDocument.LatestPublishedVersionId);
  }

  List<ContentDistribution> ContentDistributions = [
    select id, Name, DistributionPublicUrl, ContentDocumentId, ContentVersionId
      from ContentDistribution
      where ContentVersionId in: LatestPublishedVersionIds
  ];
  System.debug('ContentDistributions: ' + ContentDistributions);
  // ContentDocumentId -> ContentDistribution
  Map<ID, ContentDistribution> ContentDistributionMap = new Map<ID, ContentDistribution>();
  for (ContentDistribution ContentDistribution : ContentDistributions){
    ID tmpDocId = ContentDistribution.ContentDocumentId != null ? ContentDistribution.ContentDocumentId : ContentDistribution.ContentVersionId;
    ContentDistributionMap.put(tmpDocId, ContentDistribution);
  }
  // Your Object ID -> List<ContentDistribution>
  Map<ID, List<ContentDistribution>> pMap = new Map<ID, List<ContentDistribution>>();
  List<Purchase_Order__c> Your Objects = [
    select id, Attachment_List__c
    from purchase_order__c
    where id in: yourObjectIDs
  ];
  System.debug('Your Objects: ' + Your Objects);
  String attachmentString = '';
  For (Purchase_Order__c YourObject : Your Objects){
    if (ContentDocumentMap != null && ContentDocumentMap.size() > 0){
      for (ID ContentDocumentId : ContentDocumentMap.get(YourObject.id)){
        attachmentString += '
	<li><a href="' + ContentDistributionMap.get(ContentDocumentId).DistributionPublicUrl + '">' + ContentDistributionMap.get(ContentDocumentId).Name + '</a></li>
';
      }
    }
  YourObject.Attachment_List__c = attachmentString;
}

update Your Objects;

There you have it, a long, complicated, hard-to-read apex code. The first bit creates and deletes  public links. The second long part adds the newly created links to our custom field created above – Attachment_List__c.

Finally, we add the File related list to the page layout. (Remove Attachments if you wish)

screen-shot-2016-12-04-at-11-50-07-am

Here is what it looks like when a file is uploaded.

screen-shot-2016-12-04-at-11-51-32-am

(File uploading page)

screen-shot-2016-12-04-at-11-52-30-am

The Attachment List custom field looks like an unordered list, which is exactly what it looks like in the approval email. Approvers will now be able to click on the link and access the file without logging into Salesforce, how convenient is that.

Screen Shot 2016-12-04 at 11.54.16 am.png

The external link also provides a useful download button if the file cannot be rendered by your browser.

That’s all for today, hope you enjoy it.

Advertisements
Outbound Email-to- Case — 27/11/2016

Outbound Email-to- Case

So it turns out that writing blog articles isn’t as easy as I initially thought. There are so many attractive activities and distractions over the weekend! The weather on Sunday morning was cloudy, so a perfect opportunity to smash through all of my certificate maintenance exams – yay!

screen-shot-2016-11-27-at-3-23-48-pm

Alright – back to the purpose of this blog post. Salesforce has an in-the-box solution to convert inbound emails to cases. We utilize this to receive customer enquiries every day.

What about outbound emails? The most efficient way we have discovered is:

  1. Create a Salesforce case
  2. Attach the case to a contact and an account
  3. Send an email from this case

Pretty slow hey? Wouldn’t it be nicer if you could send an email and have the case automatically created with predefined values?

 

Fortunately, there is a solution – visualforce page + apex controller + list view button. Continue reading

URL Hack — 19/11/2016

URL Hack

Speed and efficiency are very important to a business.

In Salesforce, if a user wants to send a quick email to a lead via a predefined template, here is the long list of steps:

  1. Open lead page
  2. Click on “send an email” button
  3. In the new page, click “select template” button
  4. Pick the desired email template in a new pop up
  5. Possibly also need to change from user to an organisational email address

Is there a way to shorten the process and cut step 3 – 5 ?

Certainly, we can build a custom button on the record detail page which automatically fills all necessary fields in send email page for us.

Here is how it’s done:

Firstly, find the email template id from the URL https://<instance>.salesforce.com/<your email id>.

Then, (I use Google chrome browser) go to the standard send email page and right click on the from field, choose “inspect”.screen-shot-2016-11-19-at-3-35-20-pm

Exam the highlighted id in the browser console (in this case we have ID p26). screen-shot-2016-11-19-at-3-37-10-pm

Expand the selection and find the desired email address and copy the whole value for that option. screen-shot-2016-11-19-at-3-37-19-pm

By now we have got all the building blocks we need to hack the URL.

The last step is to build the custom URL for our button. We can build this by modifying the current page URL (send email):

Simply change https://<instance&gt;.salesforce.com/_ui/core/email/author/EmailAuthor to https://<instance&gt;.salesforce.com/_ui/core/email/author/EmailAuthor?p26=<the value you collect above>&template_id=<template id you copied from your template url> .

Done.

Try hitting enter, see the same page reloads with required fielded prefilled?

We can also extend the power of this method by including more ‘ids’ in the url, for example to auto CC someone in your company (&p4=henry@epic.com for example).

That’s it for today, hope this tutorial inspires you.

 

Lead Convert — 13/11/2016

Lead Convert

Today, let’s talk about lead conversion.

Say what? Lead conversion is a standard feature, is there something new? Affirmative.

  • Salesforce comes with an out of the box solution for field mapping, but what about lead’s related objects mapping?
  • Salesforce provides standard object generation after lead convert, but what if we want to create more than the standard accounts, contacts, and opportunities?

The answer is Apex Trigger.

The problem we want to solve:

  1. if a tick box in the lead page layout is ticked, some other type of opportunities should also be created in addition to the standard opportunity.
  2. all lead’s related objects need to be attached to the newly generated account.

Let’s go straight into the solution:

List<Opportunity> newOpps = new List<Opportunity>();
Map<ID, ID> leadToAccIdMap = new Map<ID, ID>();

for (Lead l : Trigger.New){
Lead oldLead = Trigger.oldMap.get(l.ID);
// only care about converted leads
if (l.isConverted == false) {continue;}

if (l.some_check_box__c == true){
Opportunity newOpp = new Opportunity(
recordtypeid = 'Some ID',
AccountId = l.ConvertedAccountId,
StageName = 'Closed Won',
CloseDate = Date.Today(),
name = 'Some Name',
);
newOpps.add(newOpp);
}
leadToAccIdMap.put(l.id, l.ConvertedAccountId);
}
insert newOpps;

// now the related objects
List<someObject__c> es = [
select id, leadId, accountId
from someObject__c
where leadId in: leadToAccIdMap.keySet()
];

for (someObject__c e : es){
e.accountid = leadToOppIdMap.get(e.leadId);
}
update es;

Here we have the working solution, but what about the test class? How do we easily test a lead convert operation?

It’s great that Salesforce does provide this for us already:

Database.LeadConvertResult lcr = Database.convertLead(lc);

That’s it, again hope this tutorial is helpful for you.

Start with something simple — 11/11/2016

Start with something simple

The very first ‘technical’ blog doesn’t need to be something complicated right?

I think I might just start with one of the very first problems I had – displaying an account’s contact details in the page layout for a custom object.

Few things I considered before making any changes to the org:

  • What’s the relationship like?
  • Anything salesforce already provided?
  • Visualforce page for sure
  • Controller needed ?

I have tried several approaches including writing complicated SOQL queries to find related contacts to the account (join queries).

However, halfway through the implementation, I realized this could be a common problem for other companies as well and decided to ask our best friend, Google .

Let’s skip the talk and go straight into the solution:

<apex:relatedList list="Contacts" subject="{!xxx__c.account__c.Id}>
<apex:relatedList>

That’s it! I never knew the solution can be as elegant as only 2 lines of code.

This gives the ideal result, but when clicking on the related list inside the page, contact page will actually be opened within the little window within the current page.

That’s a bit annoying hey? Luckily, this can be easily resolved by adding

<base target="_blank" />

to the page.

That’s all for today, hope this article is a good starting point and is also helpful.

First blog post ever — 09/11/2016