1

I have a list of Accounts (think possibly dozens or low hundreds) and some end user who wants to just click one button to have all the reports of a certain format generated for them instead of needing to navigate to a different webpage for each item to then click some "generate X" button.

So, for example I have a list of Payments (Payments__c), I want to make it so that my end user can say "I want all the payments to be generated as a PDF", then click on some "Generate PDFs" button, which will then produce all the PDFs without requiring they move to a new page (all the processing happens in the background?)

Does anyone have any resources? Googling around seemed to lead me to batch sending of emails.

I ask if it's possible because currently I have some leftover code from the last person who worked on the individual generation and what the person did was pull data from the fields of the current page the user is on.

What I've tried

I had an idea to create the page object, then pass in the arguments I want (hopefully making the page constructor get called) but unfortunately it seems that the page is only created upon visiting it so that method won't work

  • What do you mean by "all the reports"? Do you mean a standard Salesforce report with a filter on each account? – David Cheng Oct 1 '17 at 16:00
  • I've edited it so the second paragraph details what I mean. Do let me know if you need more details – user49458 Oct 1 '17 at 16:53
  • Have you considered using a document merge app like Conga Composer or Drawloop? See the AppExchange. – David Cheng Oct 2 '17 at 0:03
1

Printing pretty PDFs in Visualforce is a kind of art. Takes a while to produce something pleasant to look that will also behave OK. For example your single print of Account record might take 3 pages. If you bulk print several accounts - you don't want last one to display "page 10 of 12, 11 of 12, 12 of 12". You want the counter to reset, still say "1,2,3", as if the PDF wasn't collated (if that's the right word). Styling it (picking right font that works for non-English characters, page orientation, breaking tables not in a middle of the row so upper half of the words is on previous page...) takes some experience ;)

You might want to read up on "Standard Set Controllers" (SSC)... What you see below might be an overkill for you, you might be able to do what you need without Apex controller, with pure SSC. Still - have a look around.

Class:

// This class has several constructors. Can be used in single-record prints, mass prints (from StandardSetController)
// or without any base object at  all - in which case it'll try to read Ids from the URL.
global with sharing class Stack194167 {

    global transient String id {get; set;}      // One record Id or a semicolon-separated list of Ids to query for
    global transient List<Account> accounts;    // StandardSetController's getSelected() result, passed when list view button is clicked

    transient Set<String> ids;

    // Paremeterless constructor. Needed so we can use this class as component controller in email template.
    global Stack194167(){
        PageReference pr = ApexPages.currentPage();
        if(pr != null){
            id = pr.getParameters().get('id');
        }
    } 

    global Stack194167(ApexPages.StandardController sc){
        this();
    }

    global Stack194167(ApexPages.StandardSetController ssc){
        this();
        if(ssc != null){
            accounts = ssc.getSelected();
        }
    }

    global List<Account> getAccounts(){
        if(ids != null && !ids.isEmpty()){
            return null;
        }
        ids = accounts == null ? new Set<String>() : new Map<String, Account>(accounts).keyset().clone();
        if(String.isNotBlank(id)){
            ids.addAll(id.split('[\\,\\:;\\s]'));
        }
        System.debug(ids);

        accounts = [SELECT Id, Name, Description
            FROM Account
            WHERE Id IN :ids
            ORDER BY Name];
        return accounts;
    }
}

Page

<apex:page standardController="Account" extensions="Stack194167" recordSetVar="records"
    sidebar="false" showHeader="false" applyHtmlTag="false" standardStylesheets="false"
    readonly="true"
    renderAs="pdf">

<html>
    <head>
    <style type="text/css">
    table {
        margin: 10px 0px 10px 0px;
        width:100%;
        table-layout: fixed;
    }
    tr {
        page-break-inside: avoid;   /* Don't split tables if possible. But if it's needed - at least inject page breaks on table row border rather than in middle of the row's content. */
    }
    table, td, th {
        border: 1px solid black;
        border-collapse: collapse;
    }
    td, th{
        text-align:center;
        padding:40px;               /* This is extreme, just to illustrate what should happen if it doesn't fit on one page*/
        white-space: pre-line;      /* Preserve line breaks in data from textareas exactly as they were entered. */
    }
    .pageBreak {
        page-break-before: always;
        -fs-page-sequence: start;
    }
    @page {
        size: A4;
        @bottom-left {
            font-size: 8pt;
            content: "<apex:outputText value="{0, date, dd.MM.yyyy HH:mm:ss}"><apex:param value="{!NOW()}" /></apex:outputText>";
        }
        @bottom-right {
            font-size: 8pt;
            content: counter(page) " of " counter(pages);
        }
    }
    </style>
    </head>

    <body>
    <apex:repeat value="{!accounts}" var="a">
        <div class="account">
            <table>
                <caption>{!a.Name}</caption>
                <thead>
                    <tr><th>Field</th><th>Value</th></tr>
                </thead>
                <tbody>
                    <tr><td>{!$ObjectType.Account.fields.Id.label}</td><td>{!a.Id}</td></tr>
                    <tr><td>{!$ObjectType.Account.fields.Name.label}</td><td>{!a.Name}</td></tr>
                    <tr><td>{!$ObjectType.Account.fields.Description.label}</td><td>{!a.Description}</td></tr>
                </tbody>
            </table>

            <table>
                <tr><td>Some other stupid table, just to pad the data a bit...</td></tr>
                <tr><td>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</td></tr>
            </table>
        </div>
        <!-- Looks bit funny but it's easy way to start each order on fresh page and yet not have last page blank. -->
        <apex:outputPanel layout="block" styleClass="pageBreak" rendered="{!a.Id != accounts[accounts.size-1].Id}" />
    </apex:repeat>
    </body>
</html>
</apex:page>

Usage

  • From Account listview button (make sure to have it display checkboxes)
  • From single record: /apex/Stack194167?id=0017000000vXh3m
  • Custom URL to I don't know, collate all children of one parent account? /apex/Stack194167?id=0017000000vXh3m,0017000001TSmA8,0017000000Lg8WY,0017000000Lg8Wf
0

I gather this is a Visualforce-based PDF template. Do you want the result PDFs to get attached to some record? Do you want them to be e-mailed? Do you want existing ones to get re-created/duplicated upon that action?

Do you want them all to end up in the same PDF file, or separate?

In any case it is possible. Depending on your answers to the questions it may have to be bulkified in some way or other.

  • Yes it is! The idea I had was to create the page format such that it takes in an object parameter, then populates the fields. I want the PDFs to be downloaded. I want all of them to end up in separate files (was gonna use the some fields from the object parameter I passed in to name the files). If possible I would really love some resources to look it up / a simple example. Googling really didn't leave me with much but probably doesn't help that I'm not 100% sure what I'm looking for as I'm new to this – user49458 Oct 1 '17 at 17:13
  • Hmm. How would you propose a bulk download... in a zip file? As Apex can't create those. Also Apex has a heap limit of only a few MB per transaction. In theory you could have a Visualforce page that launches a batch job to generate the PDFs, polls repeatedly until it's complete, downloads each individual file into the browser's memory and uses a front-end JavaScript ZIP library to deliver them as one file. – Charles T Oct 1 '17 at 17:17
  • I honestly have no idea... I'm more concerned about the bulk generation first. I figure I'll get to that after I'm done with this – user49458 Oct 1 '17 at 17:28
  • So you can totally have a loop that instantiates new copies of the VF PDF page, fetches the content, and saves them to a Salesforce File. The only limit is that these are considered "callouts" and you may reach either callout limits or timeout limits doing a large volume. Hence the need to potentially do it in Batchable Apex. – Charles T Oct 1 '17 at 18:01
  • awesome! Thanks, but do you have any resources for actually instantiating the new VF PDF pages without rendering or navigating to them as per the original question? I just want to be able to pass in an object, then have it set everything up in the constructor side so that I can download it. I'll worry about the batching later – user49458 Oct 1 '17 at 18:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.