PDF Services too expensive? — 11/06/2017

PDF Services too expensive?

Recently, the license for one of the apps we use to generate documents in PDF format expired. And as expected, the service provider raised the subscription price for renewal, so I decided to ‘get rid of’ them and build our own custom solution. (Pretty standard as an ops engineer hey?)

The main purpose of us having this app is to ship an invoice to our partners when certain actions happen (workflow). Our partners will receive emails with a pdf file attached.

I thought it would be super easy to set up a visualforce page and simply set the following to render the page as a PDF file:

<apex:page renderAs="pdf"></apex:page>

However, I was wrong, all stylings in the visualforce page are dismissed when salesforce converts it to PDF, the resulting file looks like an image taken in 1880s…

Therefore I had to dig further to see if there is another way of solving it. After few minutes of reading, I realised javascript button can be used to trigger a page PRINT action, this might be the alternative I am after.

After few attempts, I got the solution to work. The final product is simply a visualforce page running on a salesforce sites (so users, our partners, don’t need licenses to access files), and on the bottom of the page, I place a print button like so.

<input type="button" value="Print" onclick="window.print()" />

Here is the flow:

  1. Whatever actions trigger emails to partners;
  2. Within the email, there is a link similar to xxx.force.com?id= that opens the ‘record’ on a salesforce site with a desired layout;
  3. Users open the link and review the page and print or save to local storage if needed.

This challenge shows how we can leverage salesforce sites and other technologies outside of salesfore to achieve business needs with a minimum cost.

Salesforce API – Creating Objects — 22/04/2017

Salesforce API – Creating Objects

Last year, we talked about how to get an access token to make api request to Salesforce.

Now, we are going to use this token and create a something with it. Again, the method is written in Python. You may, of course, write it in a different language, but I found python to be a very handy programming language when I try to get things up and running quickly.

def createRecord(instance, sessionId, recordApiName, fields):
    url = 'https://'+instance+'.salesforce.com/services/data/v37.0/sobjects/'+recordApiName+'/'
    headers = {
        'Content-Type': "application/json",
        'Sforce-Auto-Assign': "FALSE",
        'Authorization': "Bearer " + sessionId
    }
    payload = json.dumps(fields, ensure_ascii=False)
    response = requests.request("POST", url, data=payload, headers=headers)
    return response.text

Notice the method takes four inputs,
– instance is simply the org you are using, ‘cs6’ for example;
– sessionId is what we got from the previous article;
– recordApiName is basically the api name for the object you are updating, e.g. Account
– fields, this is a JSON formatted string that contains the key value pairs of the data you want to insert/update in Salesforce.

There you have it, a simple and useful way to manipulate data in your Salesforce org.

Salesforce API – Getting Access Token — 15/01/2017

Salesforce API – Getting Access Token

In the article before, I described how to set up Heroku for integration. In this post, I will start with the basic — obtaining the access token.

Just a bit of the background. I recently run into a problem where customers are emailing enquiries to whichever company email address they found in their inboxes. Meaning a technical support agent might be getting an enquiry about a product feature, and the agent will have manually assign the case back to the correct queue.

It felt like a very common problem, however, I couldn’t find any solutions online whatsoever. Therefore, I (again) decided to design a solution for it myself. Due to the system limitations of apex programming, I don’t think it’s even possible to build a classifier inside the Salesforce platform. I had to build it externally and push the result back to Salesforce.

So the complete flow is:

  1. Salesforce creats cases from emails
  2. Salesforce sends case descriptions along with the Case IDs to
  3. The classified cases based on historical data and send results back to Salesforce

Here, we are actually only going to talk about the last step — send data back to Salesforce.

The external application is written in Python, a very powerful programming language. The reason why I chose python is not only because it’s fast to build a web application on Django, but also because python comes with heaps of free libraries we can use e.g. sklearn.

There are many ways to achieve this, I am going to introduce the soap api, just because it only requires your Salesforce user login email and password.

Here is the method in Python:

def login(instance, userName, password):
    url = "https://" + instance + ".salesforce.com/services/Soap/u/37.0"
    payload = u"""<?xml version="1.0" encoding="utf-8" ?>
    <env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
    <env:Body>
    <n1:login xmlns:n1="urn:partner.soap.sforce.com">
    <n1:username>""" + userName + """</n1:username>
    <n1:password>""" + password + """</n1:password>
    </n1:login>
    </env:Body>
    </env:Envelope>"""
    headers = {
        'content-type': "text/xml; charset=UTF-8",
        'soapaction': "login"
    }
    response = requests.request("POST", url, data=payload, headers=headers)
    loginXmlRoot = ET.fromstring(response.text)
    sessionId = loginXmlRoot[0][0][0][4].text
    return sessionId

When you call this method, it calls Salesforce API and obtains the session id (the token), which can be later used to ‘do some damages’.

In the next blog, I will post how to use this token to create objects in Salesforce via api.

Tracking the actual time spent on a record — 09/01/2017

Tracking the actual time spent on a record

There is no transparency to identify the total time taken for an agent to work on a particular case and which type of inbound enquiries has the highest rate of neglect from a single agent.

Simply subtracting the case open date from the case closed date won’t provide us with the information we are after because this is limited to the lifetime of a case from opened to closed. In addition, it will not indicate the number of users who opened the case.

I contacted Salesforce support to see if I whether there was a component that could achieve what I was after before investing time into building this, however to no avail.

The build

There are two requirements to this:

  • A custom object ‘time log’ to capture the duration
  • A Visualforce component that logs the start and end time each time a record is viewed

We firstly create a custom object with lookup fields to the record you wish to track the duration for, in this instance we will use the Case standard object. Next we create a number field in the new custom object to store the duration.

We will use JavaScript in conjunction with Visualforce to log the duration. Below is what the Visualforce page looks like.

<apex:page standardController="Case">

<script src="/soap/ajax/37.0/connection.js"></script>
<script src="/soap/ajax/37.0/apex.js"></script> 

<script type="text/javascript">
var timerStart = Date.now();

window.onbeforeunload = function(){
    var timeObj = new sforce.SObject("Time_Log__c");
    timeObj.case__c = "{!Case.id}";
    timeObj.Sec__c = (Date.now()-timerStart)/1000;
    var tmp = [timeObj];
    sforce.connection.create(tmp);
}

</script>
</apex:page>

As simple as that! In the Visualforce page, we define a start time when a user opens the page, and create the custom object Time Log when the window/tab is closed by the user.

However, this is only compatible with standard page, it would not work properly in the service console, because we are technically not closing a ‘window’, but a sub-tab within a window.

In the next article, I will explain how it can be solved by using the integration tool provided by Salesforce and how we can report on agent performance with this object.

Setting Up Heroku – Django — 03/01/2017

Setting Up Heroku – Django

I think programming in another language is pretty important when it comes down to solving real world challenges, the reason is that Salesforce has a lot of system limitations and the fact that Apex isn’t flexible enough to handle all sorts of problems.

Today, we are going to set up a Django app in Heroku and make the first deployment.

Firstly, you will need to be familiar with python and how the Django framework works. And I strongly recommend you to use virtual environment to avoid making changes to system files.

In order to use Heroku, you are gong to need a free Heroku account, Heroku is owned by Salesforce by the way. Once you have it, you can download the Heroku CLI from their website, the Mac Version can be found here.

Next, we will need to create an app on Heroku, you can do this via Heroku website or by running “heroku create” on your local project folder (your git repository).

“””When you create an app, a git remote (called heroku) is also created and associated with your local git repository.”””

By running “git push heroku master“, you can easily push changes to Heroku. Salesforce/Heroku handles the deployment process so easily, you won’t even need to worry about things such as dependencies, as long as you declare everything in your files (for Django web applications).

I think this should be enough for you to get the app up and running provided you already know how to run the website on your local machine.

In the next article, I will talk more about how to integrate your python app with your Salesforce instance.

More information on how to set up a Heroku app can be found at https://devcenter.heroku.com/articles/getting-started-with-python#introduction, and happy new year.

Happy New Year — 01/01/2017
Resend Approval Email — 18/12/2016

Resend Approval Email

Hello! Welcome to another interesting ‘world-class’ problem – resending approval emails that accept approvers’ replies. (I was, again, surprised that Salesforce doesn’t support this functionality natively.)

Start with the problem:

  • Approvers sometimes forget to reply to approval emails nor be bothered to login into salesforce to approve the record.
  • Once an item is submitted for approval, there is no way to remind the approver unless the raiser recalls the submission and resubmits.

This is causing a lot of headaches because manual emails and human interactions will be involved, and mistakes happen when there are back and forth communications.

Here is the real challenge we have, the executive team never want to have another login, we therefore need to make their life as easy as possible, and simply set up a workflow email alert won’t help much because it wouldn’t “read” the reply from the approvers.

In fact we just want to replicate the “Submit for Approval” button but only send the reminder email to the latest pending approver.

Solution design, to achieve this, we need:

  • an Email Template similar to the initial submission email that contains the record ID (which is later used to find the record)
  • an Apex class that implements Messaging.InboundEmailHandler to read the inbound email content
  • a Salesforce Email service to send and receive emails
  • a custom button on record detail page to send the email from the email service we set up (using URL Hack I wrote before)

Let’s get started!

Both email template and the button design is easy, I will just skip them, just don’t forget to include the detail link to the related record e.g. [instance].salesforce.com/[your-record-id], we are going to use this URL to allocate the record in salesforce.

Now in step two, we need to write some codes.

The Apex class will have to implement Messaging.InboundEmailHandler which contains a global method called handleInboundEmail that takes two inputs and returns Messaging.InboundEmailResult.

global class ObjectInboundEmailHandler implements Messaging.InboundEmailHandler {
    global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope envelope) {
    }
}

We are going to mainly focus on Messaging.InboundEmail email and try to dig some information from it.

Before that, let me introduce regex for those who aren’t familiar with it, here is the link to Wikipedia (please donate some spare changes to support them 🙂 ), basically we are going to use regex to extract the record ID.

Let’s assume your object ID always starts with “abc”, then you can use the following code to find record id inside the email body (assuming you did not forget to include the full URL of the record in the email body as mentioned above, which makes sense).

Pattern p = Pattern.compile('abc.{12}');
Matcher pm = p.matcher(email.plainTextBody.trim());
String ObjectId = null;
if (pm.find()){ // Object id found
    System.debug(pm.group(0));
    ObjectId = pm.group(0);
}
System.debug('ObjectId ' + ObjectId);

It’s not difficult to read the response from the first line and second line of the email (we do this because we want to replicate the standard button, however, feel free to add more logics on top of that).

Once we have the record ID and the response, we can then apply it to the following SOQL query to find the latest approval step for the record and action it on behalf of the email sender (the approver) based on user response. I’ve also added a simple validation to make sure the email sender is a valid salesforce user.

String res = email.plainTextBody.trim().split('\n')[0];
if (res != null){
    String action = null;
    List<ProcessInstance> pis = [
        SELECT Id, (SELECT Id, ActorId, ProcessInstanceId FROM Workitems)
        FROM ProcessInstance
        where TargetObjectId =: ObjectId
    ];
    // check sender email to see if sender is a salesforce user
    System.debug('email.fromAddress ' + email.fromAddress);
    List<User> users = [select id from user where email =: email.fromAddress];
    User user = users != null && !users.isEmpty() ? users[0] : null;
    System.debug('user ' + user);
    if (user == null) {
        /* your own error handling */
        return null;
    }
    // attach the email to Object
    insert new Task(
        ownerid = user.id,
        Description =  email.plainTextBody,
        Priority = 'Normal',
        Status = 'Complete',
        subject = email.subject,

        whatId = ObjectId
    );

    // now standardise the action
    if (
        res.trim().toLowerCase() == 'yes' ||
        res.trim().toLowerCase() == 'approve' ||
        res.trim().toLowerCase() == 'approved'
    ) {
        action = 'Approve';
    }
    else if (
        res.trim().toLowerCase() == 'no' ||
        res.trim().toLowerCase() == 'reject' ||
        res.trim().toLowerCase() == 'rejected'
    ) {
        action = 'Reject';
    }
    if (action != null){
        for (ProcessInstance pi : pis){
            if (pi.Workitems != null && !pi.Workitems.isEmpty()){
                ID tmp = pi.Workitems[0].id;
                Approval.ProcessWorkitemRequest req = new Approval.ProcessWorkitemRequest();
                req.setComments(comment);
                req.setAction(action);
                req.setWorkitemId(tmp);
                Approval.ProcessResult ProcessResult = Approval.process(req);
            }
        }
    }
    else{
        /* your own error handling */
        return null;
    }
}

(Note that the actual approver is always going to be you — the inbound email handler runner, that’s why I saved an email object as a reference, you may also want to append the actual approver’s name in the comment section.)

Now step three, the email service. It’s actually very simple:

  1. Go to setup page (our favourite as a Salesforce Admin/Dev)
  2. In the search box type in “email services” and click on it
  3. Click on “New Email Service”
  4. Give it a name and put “ObjectInboundEmailHandler” (or your custom class name) under Apex Class
  5. Put your email under Route Error Emails to This Email Address (optional)

You will also need to verify this newly created email address as an Organization-Wide Email Address in order to send reminder email from it.

And …

There you have it!

The final product is a detail page button that sends a reminder email from a custom email service that has the ability to read approvers’ responses and approve/reject the current step for you, automatically.

screen-shot-2016-12-18-at-12-19-01-pm

CSAT in Salesforce?? — 11/12/2016

CSAT in Salesforce??

Salesforce does not provide any ways to capture customers’ feedbacks as a standard feature, most companies will have to rely on pricey appexchange apps to achieve this.

Considering what we need is a simple way to rate resolved cases, buying something that cost $x per user per month just sounds incredibly expensive.

Fortunately, there is still hopes. Salesforce gives us the ability to create something called Salesforce Site, we can use this to gather end users feedbacks via email.

This is how it’s done:

  1. Salesforce automatically/manually sends an email to the customer
  2. The customer clicks on a link in the email (maybe behind a happy/sad face)
  3. A customer satisfaction feedback can be created in Salesforce and linked to the case.

Sounds extremely useful hey? Let’s get started. (I won’t be posting any code for this tutorial btw)

Firstly, we need to create a custom object to hold the feedback value. This should be easy for all of us, just make sure there are at least three fields in the custom object:

  1. Case__c
  2. Score__c
  3. Comment__c

These names are pretty self-explanatory, I won’t dig too much into it.

Then, we need a controller to handle user selections. Since it’s a link inside an email template, they can only be GET requests. We use the standard method in Apex to read the request that looks like this .salesforce.com/?score=x&caseid=

ApexPages.currentPage().getParameters().get('score');

And then, the Apex class will parse this request and create a feedback with score = x and attach it to the case with .

The next step is to create a visualforce page that is used for customers to fill in feedback comments. This should be very easy, again, I am using the Google Material Design to beautify the page. (and don’t forget the submit button)

Almost there, we now need to create a Salesforce site, we do this by going into Setup -> Develop-> Sites. (If you haven’t created one before, be sure to get a domain name for your site. ) Click on New to create a new site. Put on the label and a preferred URL, and put the name of the visualforce page we just created under “Active Site Home Page”. Save it, and test it make sure it’s working by clicking on the link under “Custom URLs”.

This should open an external page that contains a dropdown to pick a score and a free text area to type in comments. Of cause this page won’t work, simply append the URL with any random Salesforce case id, you will see a feedback has been created under that case and the “created by user” is called something site user.

Lastly, we need an email template, to keep it straightforward and to see the result immediately, we quickly create a “Custom Email” template that contains the following three links in the email footer.

<a href="<domain>.salesforce.com/yrpage?caseid=<case id>&score=1>Happy</a>
<a href="<domain>.salesforce.com/yrpage?caseid=<case id>&score=0>I'm just fine</a>
<a href="<domain>.salesforce.com/yrpage?caseid=<case id>&score=-1>Not Happy</a>

Notice the case id is dynamic and the score is static, so customers won’t create feedbacks for the same case.

To finish up the article, I suggest you add few more validation rules around feedback creation, for example, only one feedback can be created within 24 hour period for the same case. Or you may want to use a Smiley face in your email templates instead of having just some links.

Later on, we will be able to simply run a feedback with case report and see which agent (case closed by) got the highest CSAT rating 🙂

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.

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