MS Access – Print Individual PDFs of a Report

Here’s another common request.

Say you have a contacts report and like to print each client’s individual report separately as a PDF. How can this be done exactly? A concrete example might be a billing database in which you have an Invoice report (which would be comprised of all the records) and you’d like to print each invoice separately. Or just the outstanding invoices… you get the idea.

Generating PDFs on the Hard Drive

Approach 1 – Individual Reports

The General Concept

The general concept is pretty straightforward:

  1. Defined a recordsource that you’re going to use to filter the report to get those records you seek
  2. Open the report filtered based on the above
  3. Print the report as a PDF
  4. Close the report
  5. Repeat for each record in 1

A Concrete Example

Enough with the blah blah, below is some sample code to inspire yourself from

Private Sub Command0_Click()
    Dim rs                    As DAO.Recordset
    Dim sFolder               As String
    Dim sFile                 As String
    Const sReportName = "NameOfYourReport"

    On Error GoTo Error_Handler

    'The folder in which to save the PDFs
    sFolder = Application.CurrentProject.Path & "\"

    'Define the Records that you will use to filtered the report with
    Set rs = CurrentDb.OpenRecordset("SELECT ContactID, FirstName FROM Contacts;", dbOpenSnapshot)
    With rs
        If .RecordCount <> 0 Then 'Make sure we have record to generate PDF with
            .MoveFirst
            Do While Not .EOF
                'Build the PDF filename we are going to use to save the PDF with
                sFile = sFolder & Nz(![FirstName], "") & ".pdf"
                'Open the report filtered to the specific record or criteria we want in hidden mode
                DoCmd.OpenReport sReportName, acViewPreview, , "[ContactID]=" & ![ContactID], acHidden
                'Print it out as a PDF
                DoCmd.OutputTo acOutputReport, sReportName, acFormatPDF, sFile, , , , acExportQualityPrint
                'Close the report now that we're done with this criteria
                DoCmd.Close acReport, sReportName
                'If you wanted to create an e-mail and include an individual report, you would do so now
                .MoveNext
            Loop
        End If
    End With

    'Open the folder housing the PDF files (Optional)
    Application.FollowHyperlink sFolder

Error_Handler_Exit:
    On Error Resume Next
    If Not rs Is Nothing Then
        rs.Close
        Set rs = Nothing
    End If
    Exit Sub

Error_Handler:
    If Err.Number <> 2501 Then    'Let's ignore user cancellation of this action!
        MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
               "Error Number: " & Err.Number & vbCrLf & _
               "Error Source: Command0_Click" & vbCrLf & _
               "Error Description: " & Err.Description & _
               Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
               , vbOKOnly + vbCritical, "An Error has Occurred!"
    End If
    Resume Error_Handler_Exit
End Sub

Approach 2 – A Single Filtered Report

The General Concept

The general concept is pretty straightforward:

  1. Defined a recordsource that you’re going to use to filter the report to get those records you seek
  2. Open the report to include all the records
  3. Apply a filter to limit the report
  4. Print the report as a PDF
  5. Repeat 3 & 4 for each record in 1
  6. Close the report

A Concrete Example

Private Sub Command1_Click()
    Dim rs                    As DAO.Recordset
    Dim rpt                   As Access.Report
    Dim sFolder               As String
    Dim sFile                 As String
    Const sReportName = "NameOfYourReport"

    On Error GoTo Error_Handler

    'The folder in which to save the PDFs
    sFolder = Application.CurrentProject.Path & "\"

    Set rs = CurrentDb.OpenRecordset("SELECT ContactID, FirstName FROM Contacts;", dbOpenSnapshot)
    With rs
        If .RecordCount <> 0 Then    'Make sure we have record to generate PDF with
            'Open the Report
            DoCmd.OpenReport sReportName, acViewPreview, , , acHidden
            'Define a report object so we can manipulate it below
            Set rpt = Reports(sReportName).Report
            .MoveFirst
            Do While Not .EOF
                'Build the PDF filename we are going to use to save the PDF with
                sFile = Nz(![firstname], "") & ".pdf"
                sFile = sFolder & sFile
                'filter the report to the specific record or criteria we want
                rpt.Filter = "[ContactID]=" & ![ContactID]
                rpt.FilterOn = True
                DoEvents 'This is critical or else the filter isn't applied!!!!
                'Print it out as a PDF
                DoCmd.OutputTo acOutputReport, sReportName, acFormatPDF, sFile, , , , acExportQualityPrint
                'If you wanted to create an e-mail and include an individual report, you would do so now
                .MoveNext
            Loop
            'Close the report now that we're done with this criteria
            DoCmd.Close acReport, sReportName
        End If
    End With

    'Open the folder housing the PDF files (Optional)
    Application.FollowHyperlink sFolder

Error_Handler_Exit:
    On Error Resume Next
    If Not rpt Is Nothing Then Set rpt = Nothing
    If Not rs Is Nothing Then
        rs.Close
        Set rs = Nothing
    End If
    Exit Sub

Error_Handler:
    If Err.Number <> 2501 Then    'Let's ignore user cancellation of this action!
        MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
               "Error Number: " & Err.Number & vbCrLf & _
               "Error Source: Command1_Click" & vbCrLf & _
               "Error Description: " & Err.Description & _
               Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
               , vbOKOnly + vbCritical, "An Error has Occurred!"
    End If
    Resume Error_Handler_Exit
End Sub

I haven’t had a chance to validate which approach is fastest. I suspect the 2nd approach would be best.

Some Important Things to Notes

What’s important to understand is that by controlling the line

Set rs = CurrentDb.OpenRecordset("SELECT ContactID, FirstName FROM Contacts;", dbOpenSnapshot)

You control which pages of the report you generate as PDF.

Say you wanted only the Active Contacts you might do

Set rs = CurrentDb.OpenRecordset("SELECT ContactID, FirstName FROM Contacts WHERE Active=True;", dbOpenSnapshot)

Or you wanted just the Canadian client

Set rs = CurrentDb.OpenRecordset("SELECT ContactID, FirstName FROM Contacts WHERE Country='Canada';", dbOpenSnapshot)

You get the idea.

Important
I was part of a recent discussion in which a user was having extreme performance issues generating such PDFs and in the end we discovered that the base SQL Statement was the root cause of the issue. So it is important to best optimize the SQL Statement you are using otherwise your code may uselessly perform repetitive work for no reason. So be sure to validate the SQL Statement and consider using DISTINCT and/or GROUP BY and/or WHERE clauses to limit the records to be processed to a minimum.
 

Using This Technique With SendObject

You can adapt the above to even work with SendObject. By iterating over the recordset, we can then simply change the caption for each e-mail.

Sub CreateFilteredPDFsWithName()
    On Error GoTo Error_Handler
    Dim rs                    As DAO.Recordset
    Const sReportName = "NameOfYourReport"
 
    'Define the Records that you will use to filtered the report with
    Set rs = CurrentDb.OpenRecordset("SELECT ID, FirstName, LastName FROM Contacts;", dbOpenSnapshot)
    With rs
        If .RecordCount <> 0 Then 'Make sure we have record to generate PDF with
            .MoveFirst
            Do While Not .EOF
                'Open the report filtered to the specific record or criteria we want in hidden mode
                DoCmd.OpenReport sReportName, acViewPreview, , "[ContactID]=" & ![ContactID], acHidden
                'Change the Report's Caption (which is used as the FileName by SendObject)
                Reports(sReportName).Report.Caption = Nz(![FirstName], "") & "_" & Nz(![LastName], "") & "_" & Format(Now(), "yyyymmddhhnnss")
                'Generate The Email
                DoCmd.SendObject acSendReport, sReportName, acFormatPDF, , , , , , True
                'Close the report now that we're done with this criteria
                DoCmd.Close acReport, sReportName
                .MoveNext
            Loop
        End If
    End With
 
Error_Handler_Exit:
    On Error Resume Next
    If Not rs Is Nothing Then
        rs.Close
        Set rs = Nothing
    End If
    Exit Sub

Error_Handler:
    If Err.Number = 2501 Then
        'ignore error proceed with the next record
        Resume Next
        'Or exit nicely
'        Resume Error_Handler_Exit
    Else
        MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
            "Error Source: CreateFilteredPDFsWithName2" & vbCrLf & _
            "Error Number: " & Err.Number & vbCrLf & _
            "Error Description: " & Err.Description & _
            Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
            , vbOKOnly + vbCritical, "An Error has Occurred!"
        Resume Error_Handler_Exit
    End If
End Sub

11 responses on “MS Access – Print Individual PDFs of a Report

  1. Van Rumste Geert

    THX for the code.
    I want every distinct pdf to have a specific name ex. 1.pdf , 2.pdf, etc.
    The solution I came up with is
    DoCmd.CopyObject , M_distinct_name, acReport, M_original_report_name.
    DoCmd.OpenReport M_distinct_name, acViewPreview, , M_where
    Is there an other solution?

    Second question: I would like to store the pdf in a SQL-database. Anybody who has experience with this?

    Kind regards,
    Geert.

  2. Klaus Oberdalhoff

    Hi,

    codecabinett.com means to that:

    open the report first (hidden) then print as pdf

    Public Sub ExportFilteredReportToPDF()

    Dim reportName As String
    Dim fileName As String
    Dim criteria As String

    reportName = “rptYourReportName”
    fileName = “C:\tmp\report_export_file.pdf”
    criteria = “SomeTextField = ‘ABC’ AND SomeNumberField = 123”

    DoCmd.OpenReport reportName, acViewPreview, , criteria, acHidden
    DoCmd.OutputTo acOutputReport, reportName, acFormatPDF, fileName
    DoCmd.Close acReport, reportName, acSaveNo

    End Sub
    mfg Klaus

  3. Alberto

    You are great! I’ve resolved my problem with this script that works very well. Thank you very much!

    1. Daniel Pineault Post author

      It’s kind of hard to give you an answer without context here, but you should be able to simply alter the SQL statement to include whatever WHERE clause filters your data as you need it to be.

      SELECT ContactID, FirstName FROM Contacts WHERE [ActivationDate] BETWEEN #2000-01-01 00:00:00# AND #2022-08-12 14:30:00#

      Basically, build a normal query that gives you the results you are after and then simply use it in the function.

  4. Norman Smith

    Daniel,

    Thank you for the response, let me see if I can explain it better.
    I currently have a report and in the query I have it asking for the date range criteria “Between [Start Date] And [End Date]” in the “HQ Posted” field. This works when you run the repot normally but when I plug the report into the code above and run it that way it asks for the criteria on every loop.
    I’d like to set the criteria within the code instead of the query and have it run the loops retaining the single input range.

    Here is the current code

    Private Sub NavigationButton31_Click()
    
    Dim rs                    As DAO.Recordset
        Dim sFolder               As String
        Dim sFile                 As String
        Const sReportName = "Rpt_FeesInvoice"
     
        On Error GoTo Error_Handler
     
        'The folder in which to save the PDFs
        sFolder = Application.CurrentProject.Path & "\Invoices\"
     
        'Define the Records that you will use to filtered the report with
        Set rs = CurrentDb.OpenRecordset("SELECT BranchID, BranchName FROM AllACHCC;", dbOpenSnapshot)
        With rs
            If .RecordCount  0 Then 'Make sure we have record to generate PDF with
                .MoveFirst
                Do While Not .EOF
                    'Build the PDF filename we are going to use to save the PDF with
                    sFile = sFolder & Nz(![BranchName], "") & ".pdf"
                    'Open the report filtered to the specific record or criteria we want in hidden mode
                    DoCmd.OpenReport sReportName, acViewPreview, , "[BranchID]=" & ![BranchID], acHidden
                    'Print it out as a PDF
                    DoCmd.OutputTo acOutputReport, sReportName, acFormatPDF, sFile, , , , acExportQualityPrint
                    'Close the report now that we're done with this criteria
                    DoCmd.Close acReport, sReportName
                    'If you wanted to create an e-mail and include an individual report, you would do so now
                    .MoveNext
                Loop
            End If
        End With
     
        'Open the folder housing the PDF files (Optional)
        Application.FollowHyperlink sFolder
     
    Error_Handler_Exit:
        On Error Resume Next
        If Not rs Is Nothing Then
            rs.Close
            Set rs = Nothing
        End If
        Exit Sub
     
    Error_Handler:
        If Err.Number  2501 Then    'Let's ignore user cancellation of this action!
            MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
                   "Error Number: " & Err.Number & vbCrLf & _
                   "Error Source: Command0_Click" & vbCrLf & _
                   "Error Description: " & Err.Description & _
                   Switch(Erl = 0, "", Erl  0, vbCrLf & "Line No: " & Erl) _
                   , vbOKOnly + vbCritical, "An Error has Occurred!"
        End If
        Resume Error_Handler_Exit
    
    End Sub
        1. Norman Smith

          Now to learn how to do that next, Sadly I know enough VBA to be dangerous.

          Do I run that in the current sub before the rs or within it?

          1. Daniel Pineault Post author

            Try something like:

            Private Sub NavigationButton31_Click()
                Dim db                    As DAO.Database
                Dim qdf                   As DAO.QueryDef
                Dim prm                   As DAO.Parameter
                Dim rs                    As DAO.Recordset
                Dim sFolder               As String
                Dim sFile                 As String
                Const sReportName = "Rpt_FeesInvoice"
            
                On Error GoTo Error_Handler
            
                'The folder in which to save the PDFs
                sFolder = Application.CurrentProject.Path & "Invoices"
            
                'Define the Records that you will use to filtered the report with
                Set db = CurrentDb
            
                Set qdf = db.QueryDefs("AllACHCC")
                For Each prm In qdf.Parameters
                    prm = Eval(prm.Name)
                Next prm
            
                Set rs = qdf.OpenRecordset
                '    Set rs = CurrentDb.OpenRecordset("SELECT BranchID, BranchName FROM AllACHCC;", dbOpenSnapshot)
                With rs
                    If .RecordCount <> 0 Then  'Make sure we have record to generate PDF with
                        .MoveFirst
                        Do While Not .EOF
                            'Build the PDF filename we are going to use to save the PDF with
                            sFile = sFolder & Nz(![BranchName], "") & ".pdf"
                            'Open the report filtered to the specific record or criteria we want in hidden mode
                            DoCmd.OpenReport sReportName, acViewPreview, , "[BranchID]=" & ![BranchID], acHidden
                            'Print it out as a PDF
                            DoCmd.OutputTo acOutputReport, sReportName, acFormatPDF, sFile, , , , acExportQualityPrint
                            'Close the report now that we're done with this criteria
                            DoCmd.Close acReport, sReportName
                            'If you wanted to create an e-mail and include an individual report, you would do so now
                            .MoveNext
                        Loop
                    End If
                End With
            
                'Open the folder housing the PDF files (Optional)
                Application.FollowHyperlink sFolder
            
            Error_Handler_Exit:
                On Error Resume Next
                If Not rs Is Nothing Then
                    rs.Close
                    Set rs = Nothing
                End If
                If Not prm Is Nothing Then Set prm = Nothing
                If Not qdf Is Nothing Then Set qdf = Nothing
                If Not db Is Nothing Then Set db = Nothing
                Exit Sub
            
            Error_Handler:
                If Err.Number <> 2501 Then    'Let's ignore user cancellation of this action!
                    MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
                           "Error Number: " & Err.Number & vbCrLf & _
                           "Error Source: NavigationButton31_Click" & vbCrLf & _
                           "Error Description: " & Err.Description & _
                           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
                           , vbOKOnly + vbCritical, "An Error has Occurred!"
                End If
                Resume Error_Handler_Exit
            End Sub

            or, your other choice would be to alter the underlying query itself, so it use Eval() directly eliminating the need to do this in VBA.

            Good luck, I’ve gotta run.

  5. Chris

    Thanks for posting that sample code. I was able to get things working in my access database with only a couple of minor tweaks.
    I was initially having some troubles getting the filter to work, but when I switched from using a text value over to an integer number value for the record ID, that seem to make all the difference.

    I am having a little bit of trouble still with getting the sub reports to show up in the PDF files that get pushed out to my network drive.
    My main report is seven pages long, and there are sub reports on each of the individual pages that display or hide based on a variable on the main report.
    It all works properly when I preview the print job in Access, but now when I’m pushing them out as individual PDF files, the sub reports don’t show up.