Wednesday, March 6, 2019

Connect to SQL Server with VBA for Pass Through Queries

I recently built a program that connects to a remote SQL Server database. I discovered that the cursor type makes a huge difference in how fast the program runs. Initially, I used the adOpenStatic cursor. In some cases, the program took over 30 seconds to run seven simple queries and to render the results to some text boxes on an Access form. I changed the cursor type to adOpenForwardOnly, and the program took less than a second to run from then on. I have a "cleantext" function that scrubs the value entered by the user before it is used in the queries. Below is a simplified version of the code:

 Dim cnx As ADODB.Connection
Set cnx = New ADODB.Connection
Dim serverName As String
Dim databaseName As String
Dim userId As String
Dim pwd As String
Dim strSQL as string
     
' DATABASE CONNECTION
serverName = "TheServerName"
databaseName = "TheDatabaseName"
userId = "TheUserId"
pwd = "ThePassword"
Set cnx = New ADODB.Connection
cnx.Open "Driver={SQL Server};Server=" & serverName & ";Database=" & databaseName & ";Uid=" & userId & ";Pwd=" & pwd & ";"
    
Dim rsADO As ADODB.Recordset
Set rsADO = New ADODB.Recordset

strSQL="SELECT a.field1, a.field2, a.field3" & _
 " FROM someTable a" & _
 " WHERE a.field1 = '" & cleantext(me.txtId.value) & "'"

rsADO.Open strSQL, cnx, adOpenForwardOnly
Do While Not rsADO.EOF
  strDisplay = strDisplay & rsADO!field2 & " | " & rsADO!field3 &     vbCrLf
rsADO.MoveNext
Loop
rsADO.Close

' Display the values in a scrollable text box
Me.txtHDHPosJobCodes.Value = strDisplay 
strDisplay = ""

'CLOSE THE REMOTE DATABASE CONNECTION
Set rsADO = Nothing
cnx.Close
Set cnx = Nothing



Friday, June 15, 2018

Save an Excel file as a PDF using VBA

In this example, I'm opening an Excel workbook from another Office program (such as Access) and saving the workbook as a PDF:

    Dim xlApp As Excel.Application
    Dim xlWB As Excel.Workbook
    Dim xlWS As Excel.Worksheet        

    Set xlApp = CreateObject("Excel.Application")
    Set xlWB = xlApp.Workbooks.Open("template.xlsx", , False)
    Set xlWS = xlWB.Sheets(1)
    xlWS.Activate          

    With xlWS
        filePathAndName = curPath & "\Reports\ReportName.pdf"
             
        .PageSetup.FitToPagesTall = 1
        .PageSetup.FitToPagesWide = 1

        .ExportAsFixedFormat _
        Type:=xlTypePDF, _
        FileName:=
filePathAndName, _
        Quality:=xlQualityStandard, _
        IncludeDocProperties:=True, _
        IgnorePrintAreas:=False, _
        OpenAfterPublish:=False
    End With

    xlWB.Close
    Set xlWB = Nothing
    Set xlWS = Nothing
    xlApp.Quit
    Set xlApp = Nothing

Format cell borders in Excel using VBA

This example creates a header row with a heavy, black border and a light blue background.

        With xlWS.Range("A1:A100")
            .Interior.Color = RGB(194, 220, 255)
            .Borders(xlEdgeRight).LineStyle = xlContinuous
            .Borders(xlEdgeRight).Weight = xlThick
            .Borders(xlEdgeRight).Color = RGB(255, 255, 255)
            .Borders(xlEdgeLeft).LineStyle = xlContinuous
            .Borders(xlEdgeLeft).Weight = xlThick
            .Borders(xlEdgeLeft).Color = RGB(255, 255, 255)
            .Borders(xlEdgeTop).LineStyle = xlContinuous
            .Borders(xlEdgeTop).Weight = xlThick
            .Borders(xlEdgeRight).Color = RGB(255, 255, 255)
            .Borders(xlEdgeBottom).LineStyle = xlContinuous
            .Borders(xlEdgeBottom).Weight = xlThick
            .Borders(xlEdgeBottom).Color = RGB(255, 255, 255)
        End With

Format cell text in Excel using VBA

    Dim wsName As String
    Dim xlApp As Excel.Application
    Dim xlWB As Excel.Workbook
    Dim xlWS As Excel.Worksheet        

    Set xlApp = CreateObject("Excel.Application")
    Set xlWB = xlApp.Workbooks.Open(curPath & "\Template\template.xlsx", , False)
    Set xlWS = xlWB.Sheets(1)
    xlWS.Activate        

    With xlWS.Cells(row, col)
        .Font.Name = "Cambria"
        .Font.Bold = True
        .Font.Size = 14
        .Value = "Total Compensation Statement"
        .VerticalAlignment = xlVAlignTop
        .HorizontalAlignment = xlCenterAcrossSelection
    End With

Use VBA to create a chart in Excel

    Dim xlApp As Excel.Application
    Dim xlWB As Excel.Workbook
    Dim xlWS As Excel.Worksheet

    Set xlWS = xlWB.Sheets(1)
    xlWB.Sheets("Sheet1").Activate

    Dim chartObj As ChartObject
    Set chartObj = xlWS.ChartObjects.Add _
        (Left:=242, Width:=245, Top:=260, Height:=240)
    chartObj.Chart.SetSourceData Source:=xlWB.Sheets("Sheet2").Range("A1:C2")
    chartObj.Chart.ChartType = xl3DPie
    chartObj.Chart.Elevation = 45
    chartObj.Chart.ChartArea.Border.LineStyle = xlDash
    chartObj.Chart.ChartArea.Border.LineStyle = xlNone

    chartObj.Chart.SeriesCollection(1).ApplyDataLabels
    chartObj.Chart.SeriesCollection(1).DataLabels.ShowPercentage = True
    chartObj.Chart.SeriesCollection(1).DataLabels.ShowValue = False
    chartObj.Chart.SeriesCollection(1).DataLabels.ShowCategoryName = True
    chartObj.Chart.SeriesCollection(1).DataLabels.Font.Size = 8
    chartObj.Chart.SeriesCollection(1).DataLabels.Font.Name = "Cambria"
    chartObj.Chart.SeriesCollection(1).DataLabels.Font.Bold = False
    chartObj.Chart.SeriesCollection(1).DataLabels.ShowLegendKey = -1
    chartObj.Chart.SeriesCollection(1).DataLabels.Position = xlLabelPositionBestFit
    chartObj.Chart.SetElement (msoElementLegendNone)

    xlWB.SaveAs (curPath & "\Reports\ReportName.xlsx")

    xlWB.Close
    Set xlWB = Nothing
    Set xlApp = Nothing

Use VBA to work with Microsoft Publisher

First, set a reference to the Microsoft Publisher Object Library in the VBA editor.

Create Publisher object variables:
    Dim pubApp As Publisher.Application
    Dim pubDoc As Publisher.Document

Open a Publisher document to use as a template:    
    Set pubApp = CreateObject("Publisher.Application")
    Set pubDoc = pubApp.Open(FileName:=curPath & "\Templates\" _
    & templateName, ReadOnly:=False, addtorecentfiles:=False, _
    savechanges:=pbDoNotSaveChanges)

Find and replace text in Publisher:
Private Sub replaceText(match As String, replace As String, ByRef pubDoc As Publisher.Document)
    With pubDoc.Find
    .Clear
    .FindText = match
    .ReplaceWithText = replace
    .ReplaceScope = pbReplaceScopeOne
    .Execute
    End With
End Sub

Call replaceText("<<Street>>", theStreet, pubDoc)

Add a table to a Publisher document:
pubDoc.Pages(1).Shapes.AddTable(NumRows:=5, _ NumColumns:=5, Left:=72, Top:=300, Width:=400, Height:=100) .Table.Columns(3).Cells(3).Fill.ForeColor.RGB = RGB _ (Red:=255, Green:=0, Blue:=0)

Add, format and populate rows in a Publisher table:    
Dim rowNew as Row
    Set rowNew = pubDoc.Pages(1).Shapes(1).Table.Rows.Add
    rowNew.Cells(1).TextRange.Font.Name = "Montserrat"
    rowNew.Cells(1).TextRange.Font.Size = 9
    rowNew.Cells(1).TextRange.Text = someValue

Save the Publisher document as a PDF:    
    pubDoc.ExportAsFixedFormat pbFixedFormatTypePDF, _
    docPathAndName, pbIntentStandard, False

Clean up Publisher objects:
    pubDoc.Close
    pubApp.Quit
    Set pubApp = Nothing

Thursday, June 14, 2018

Use VBA and Access Tables to Dynamically Populate a Publisher Template

I have a Publisher document that I need to send to a list of clients with information about their work comp rates for the upcoming year. The clients are categorized into several different types depending on a variety of factors which I won't get into here. A standard mail merge won't work because of the complexity of the process and the logic involved. I've omitted the business logic that organizes the clients and work comp rates into various categories.

The code creates a Publisher object, opens a pre-formatted Publisher document/template, iterates through an Access table and populates a table within Publisher from the Access data. A table with just the header already exists in the Publisher template. The code adds table rows and has to format the added rows so they are different from the header.

Once the document is complete, the code converts the Publisher doc to a PDF and closes Publisher without saving. A new document is created for each client in the table. Setting the "savechanges" parameter of the Publisher Open function to "pbDoNotSaveChanges" is critical, because it allows the Publisher template to be reused ad infinitum. Properly closing/cleaning up the Publisher doc and App is also critical. If your code does not properly open, save, and close the Publisher document, you will get all kinds of warnings and prompts from Publisher as the program runs.

The Optional parameter in the buildRatePage() function allowed me to test the function with a single client rather than waiting for an entire batch to run.

The first function below is called when a button is clicked. It calls the buildRatePage() function that does the actual work of building the work comp rate letters. A custom replaceText() function is called to find and replace text in the Publisher template which I've set up to look like merge fields in Word.

The key steps for working with Publisher in VBA are outlined below: 

1) Set a reference to the Microsoft Publisher Object Library in the VBA editor.

2) Set up the variables:
    Dim pubApp As Publisher.Application
    Dim pubDoc As Publisher.Document

3) Instantiate the variables:    
    Set pubApp = CreateObject("Publisher.Application")
    Set pubDoc = pubApp.Open(FileName:=curPath & "\Templates\" & templateName, ReadOnly:=False, addtorecentfiles:=False, savechanges:=pbDoNotSaveChanges)

4) Add, format and populate rows in a pre-existing table in Publisher:    
    Set rowNew = pubDoc.Pages(1).Shapes(1).Table.Rows.Add
    rowNew.Cells(1).TextRange.Font.Name = "Montserrat"
    rowNew.Cells(1).TextRange.Font.Size = 9
    rowNew.Cells(1).TextRange.Text = someValue

5) Save the Publisher document as a PDF:    
    pubDoc.ExportAsFixedFormat pbFixedFormatTypePDF, docPathAndName, pbIntentStandard, False

6) Clean up:
    pubDoc.Close
    pubApp.Quit
    Set pubApp = Nothing

Read on if you would like to see the details of how the program works:

'------------------------------------------------------
Private Sub cmdCreateRPLetters_Click()

    Me.lblLetterProc.Visible = True
    Me.Repaint

    Dim curPath As String
    curPath = CurrentProject.Path
 
    DoCmd.SetWarnings False
    On Error Resume Next
        Kill curPath & "\Letters\*.pdf"
    On Error GoTo 0
 
' Testing
'    Call buildRatePage("UtahOnly", "NoMod", "InsuredNoModTemplate.pub", "12-1315")
'    Call buildRatePage("UtahOnly", "Mod", "InsuredModTemplate.pub", "13-1623")
'    Call buildRatePage("MultiState", "Mod", "InsuredModTemplate.pub", "13-1540")
'    Call buildRatePage("MultiState", "NoMod", "InsuredNoModTemplate.pub", "13-2291")
 
    Call buildRatePage("UtahOnly", "NoMod", "InsuredNoModTemplate.pub")
    Call buildRatePage("UtahOnly", "Mod", "InsuredModTemplate.pub")
    Call buildRatePage("MultiState", "Mod", "InsuredModTemplate.pub")
    Call buildRatePage("MultiState", "NoMod", "InsuredNoModTemplate.pub")
    Call buildRatePage("MultiClient", "Mod", "InsuredModTemplate.pub")
    Call buildRatePage("MultiClient", "NoMod", "InsuredNoModTemplate.pub")
    Call buildRatePage("Idaho", "NoMod", "InsuredNoModTemplate.pub")
    Call buildRatePage("Idaho", "Mod", "InsuredNoModTemplate.pub")

    Me.lblLetterProc.Visible = False
    Me.Repaint
 
    MsgBox "Letters created. The Letters folder will be cleared the next time this program runs. Please save any letters" _
        & " you want to keep to a new location."
 
End Sub

'-----------------------------------------------------
Private Sub buildRatePage(clientType As String, modType As String, templateName As String, Optional parentID As String)

    Dim sql As String
    Dim db As DAO.Database
    Set db = CurrentDb
    Dim rs As DAO.Recordset
    Dim rs2 As DAO.Recordset
    DoCmd.SetWarnings False


    Dim pubApp As Publisher.Application
    Dim pubDoc As Publisher.Document
    Dim curPath As String
    curPath = CurrentProject.Path
    If Nz(parentID, "") <> "" Then
        pID = " and parent_id = '" & parentID & "'"
    Else
        pID = ""
    End If
 

    sql = "SELECT DISTINCT parent_id, insured_name, type, mod_type FROM Rate_Page_Letters WHERE type = '" & clientType & "' and mod_type = '" & modType & "'" & pID
    Set rs = db.OpenRecordset(sql)

    Do While Not rs.EOF
 
        If modType = "Mod" Then
            sql = "SELECT DISTINCT * FROM Rate_Page_Letters WHERE parent_id = '" & rs!parent_id & "' and type = '" _
            & rs!Type & "' and mod_type = '" & rs!mod_type & "' and mod2 <> 1"
        Else
            sql = "SELECT DISTINCT * FROM Rate_Page_Letters WHERE parent_id = '" & rs!parent_id & "' and type = '" & rs!Type & "' and mod_type = '" & rs!mod_type & "'"
        End If


        Set rs2 = db.OpenRecordset(sql)
     
        Set pubApp = CreateObject("Publisher.Application")
        Set pubDoc = pubApp.Open(FileName:=curPath & "\Templates\" & templateName, ReadOnly:=False, addtorecentfiles:=False, savechanges:=pbDoNotSaveChanges)

        Do While Not rs2.EOF

            Call replaceText("<<InsuredName>>", rs2!insured_name, pubDoc)
            Call replaceText("<<PolicyNumber1>>", polNum, pubDoc)

            ' Populate the Publisher document
            If templateName = "InsuredModTemplate.pub" Then
                Call replaceText("<<Emod>>", rs2!mod2, pubDoc)
                Set rowNew = pubDoc.Pages(1).Shapes(1).Table.Rows.Add
                rowNew.Cells(1).TextRange.Font.Name = "Montserrat"
                rowNew.Cells(1).TextRange.Font.Size = 9
                rowNew.Cells(1).TextRange.Text = rs2!comp_code
                rowNew.Cells(2).TextRange.Font.Name = "Montserrat"
                rowNew.Cells(2).TextRange.Font.Size = 9
                rowNew.Cells(2).TextRange.Text = rs2!code_desc
                rowNew.Cells(3).TextRange.Font.Name = "Montserrat"
                rowNew.Cells(3).TextRange.Font.Size = 9
                rowNew.Cells(3).TextRange.Text = rs2!cur_yr_final_rate
                rowNew.Cells(4).TextRange.Font.Name = "Montserrat"
                rowNew.Cells(4).TextRange.Font.Size = 9
                rowNew.Cells(4).TextRange.Text = rs2!cur_yr_mod_rate
            Else
                Set rowNew = pubDoc.Pages(1).Shapes(1).Table.Rows.Add
                rowNew.Cells(1).TextRange.Font.Name = "Montserrat"
                rowNew.Cells(1).TextRange.Font.Size = 9
                rowNew.Cells(1).TextRange.Text = rs2!comp_code
                rowNew.Cells(2).TextRange.Font.Name = "Montserrat"
                rowNew.Cells(2).TextRange.Font.Size = 9
                rowNew.Cells(2).TextRange.Text = rs2!code_desc
                rowNew.Cells(3).TextRange.Font.Name = "Montserrat"
                rowNew.Cells(3).TextRange.Font.Size = 9
                rowNew.Cells(3).TextRange.Text = rs2!cur_yr_final_rate
            End If
         
            rs2.MoveNext
        Loop
     
        'Terrorism insurance rows at the end of the table
        Set rowNew = pubDoc.Pages(1).Shapes(1).Table.Rows.Add
        rowNew.Cells(1).Merge MergeTo:=rowNew.Cells(2)
        rowNew.Cells(2).TextRange.Font.Name = "Montserrat"
        rowNew.Cells(2).TextRange.Font.Size = 7
        rowNew.Cells(2).TextRange.Text = "Terrorism Risk Insurance Act of 2002:"
        rowNew.Cells(1).TextRange.Font.Name = "Montserrat"
        rowNew.Cells(1).TextRange.Font.Size = 7
        rowNew.Cells(1).TextRange.Text = ".01%"
     
        Set rowNew = pubDoc.Pages(1).Shapes(1).Table.Rows.Add
        rowNew.Cells(1).TextRange.Font.Name = "Montserrat"
        rowNew.Cells(1).TextRange.Font.Size = 7
        rowNew.Cells(1).TextRange.Text = "Domestic Terrorism, Earthquakes, and Catastrophic Industrial Accidents: "
        rowNew.Cells(2).TextRange.Font.Name = "Montserrat"
        rowNew.Cells(2).TextRange.Font.Size = 7
        rowNew.Cells(2).TextRange.Text = ".01%"

        rs2.Close
     
        'Save as PDF
        docName = curPath & "\Letters\" & clientType & "_" & modType & "-" & replace(cleantext(rs!insured_name), "/", "") & ".pdf"
        pubDoc.ExportAsFixedFormat pbFixedFormatTypePDF, docName, pbIntentStandard, False

        pubDoc.Close
        pubApp.Quit
        Set pubApp = Nothing
     
        rs.MoveNext
    Loop
    rs.Close
 
End Sub


'-----------------------------------------------
Private Sub replaceText(match As String, replace As String, ByRef pubDoc As Publisher.Document)
    With pubDoc.Find
    .Clear
    .FindText = match
    .ReplaceWithText = replace
    .ReplaceScope = pbReplaceScopeOne
    .Execute
    End With
End Sub


Tuesday, February 14, 2017

Auto-populate a text box using a query

I have a small data warehouse I've built in MS Access 2016. I load new data in the warehouse every month, and I want to dynamically show users of the data warehouse the date range available for reports.

I created a label that says the following: "*Data loaded for January 1, 2016 through the end of".

Next, I created a textbox named txtMaxDate. I gave the textbox a transparent border and a font that matches the label above. I placed the textbox right after the label.

In the onLoad event of the form, I wrote the following code:

Private Sub Form_Load()
    Dim sql As String
    Dim rs As DAO.Recordset
    Dim db As DAO.Database
    Dim maxLoadDate As Date
    Dim theYear As String
    Dim theMonth As String
    Set db = CurrentDb
    sql = "SELECT MAX(PayYearMonth) as MaxDate FROM tblPaySummary"
    Set rs = db.OpenRecordset(sql)

    rs.MoveFirst
    maxLoadDate = rs!MaxDate
   
    theYear = year(maxLoadDate)
    theMonth = MonthName(month(maxLoadDate))
    Me.txtMaxDate.Value = theMonth & " " & theYear
End Sub

The code gets the most recent date from the database and displays the date in the text box.




Thursday, April 16, 2015

VBA Function to Remove Non-Alphanumeric Characters

Occasionally, you'll run into data that has been entered with inadvertent carriage returns or other non-alphanumeric characters. Sometimes, these invisible characters can cause mysterious errors in your code. The solution is to use a function to remove any superfluous characters from the data.

The following function removes everything except for the characters indicated between the brackets. You can tweak the accepted character set as needed. The function works by looping through each character in the string and testing each character to see if it matches one of the characters listed between the square brackets.

Function cleanText(strText As String) As String

    Dim valid as String

    Dim test As String
    valid = ""
    test = ""

    For i = 1 To Len(strText)
        test = Mid(strText, i, 1)
        'Only allow characters and spaces, not carriage returns or any other code
        If test Like "[A-Z,a-z,0-9, ,.,/,~,@,#,$,%,^,&,*,(,),_,-,+,= ]" Then
            valid = valid & test
        End If
    Next i
 
    'Return the valid characters
    cleanText = valid

End Function


The cleanText function uses the Visual Basic (VBA) "Like" operator. The Like operator is similar to using Regular Expressions and also has some similarities to the SQL Like operator. You can read more about the VBA Like operator on this page.

VBA Regular Expression Phone Number Validator

This phone number validator validates numbers with or without dashes and with or without dots between the segments.


Public Function validatePhoneNo(phoneno As String) As Boolean

    If phoneno <> "" Then
        Dim re As RegExp
        Dim matches As MatchCollection
        Set re = New RegExp
        re.IgnoreCase = True
        re.Global = True
        re.Pattern = "^\(?\d{3}-?\.?\)?\s?\d{3}-?\.?\d{4}$"
        Set matches = re.Execute(phoneno)
        If matches.Count <> 1 Then
            validatePhoneNo = False
            Exit Function
        End If
    Else
        validatePhoneNo = False
        Exit Function
    End If
 
    validatePhoneNo = True
 
End Function

Use VBA to Automatically Set Dates in Form Fields

In this case, I have a report that requires a Begin Date and an End Date.













To make things easier for the user, I pre-fill the date fields with the date range most commonly used in the report. I use the Form_Load() event to automatically set the dates in the form fields.

Private Sub Form_Load()
    Me.txtBegDt.Value = "1/1/" & Year(Date)
    Me.txtEndDt.Value = Date
End Sub



The first line sets the Begin Date (txtBegDt) field to January 1 of the current year. The second line sets the End Date (txtEndDt) to the current date. Easy as that!

Shortcut to Open the VBA Editor

Using keyboard shortcuts saves a lot of time for things you do frequently. I use this shortcut every time I need to open the VBA editor in an Office Program:

Alt + F11

That's it. Practice using this shortcut a few times until it becomes second nature.

Wednesday, May 21, 2014

Use Query Definitions to Create Dynamic Queries and Reports

The Begin and End dates in this query come from text boxes formatted as dates. The client list comes from a multi-select list box. The "qClaims" query is associated with the Claims Report. The query definition is fed a dynamic SQL statement built from the values selected by the user. The query definition makes it easy to generate a report from a dynamic query.


  



















 If Nz(Me.txtBegDt.Value, "") = "" Then
        MsgBox "Please select a begin date."
        Exit Sub
    End If
    If Nz(Me.txtEndDt.Value, "") = "" Then
        MsgBox "Please select an end date."
        Exit Sub
    End If
    
    DoCmd.Close acQuery, "qClaims", acSaveNo
    DoCmd.Close acReport, "Claims Report"
    
    Dim strList as String
    Dim strsql As String
    Dim qdf As DAO.QueryDef
    Set db = CurrentDb
    
    Set qdf = db.QueryDefs("qClaims")
    
    strList = ""
    
    If Me.lstClients.ItemsSelected.Count > 0 Then
        For Each client In Me.lstClients.ItemsSelected
            strList = strList & "'" & Me.lstClients.ItemData(client) & "',"
        Next client
        strList = CStr(Trim(Left(strList, Len(strList) - 1)))   ' Remove trailing comma
        strList = " AND i.client_id IN (" & strList & ")"
    Else
        strList = ""
    End If

    strsql = "SELECT i.client_id, i.client_name, i.fname, i.lname, i.ssn," _
    & " i.sep_reason, c.charge_qu, c.charge_yr, c.charge_amount" _
    & " FROM tCharges c RIGHT JOIN tSeparation_Info i ON c.sep_ID = i.id" _
    & " WHERE i.mail_date BETWEEN #" & [Forms]![Claims Report]![txtBegDt] _
    & "# And #" & [Forms]![Claims Report]![txtEndDt] & "# " & strList _
    & " ORDER BY i.client_name"
            
    qdf.sql = strsql   ' Set the sql of the query definition
    
    DoCmd.OpenReport "Claims Report", acViewReport
    
    Set db = Nothing
    Set qdf = Nothing

Monday, February 3, 2014

Dynamic SQL statements

Dynamic SQL statements are great when you want to give end users maximum flexibility in creating their own  reports. To build dynamic SQL statements, first declare variables for each clause of a SQL statement. Here we have variables for the SELECT, FROM, WHERE, and GROUP BY clauses. The allSQL variable will hold the completed SQL statement.

  Dim selSQL As String 
  Dim fromSQL As String
  Dim whereSQL As String
  Dim groupSQL As String
  Dim allSQL As String
  Dim func As String

Next, we create a query definition object that will be used to run the dynamic SQL statement. "CustomReport" is a query I created in the visual query builder. It's just a container that will ultimately be used to run the dynamic SQL statement.

 Dim qdf As DAO.QueryDef
 Dim db As DAO.Database
 Set db = CurrentDb
 Set qdf = db.QueryDefs("CustomReport")

Next, initialize the SQL query strings.

selSQL = "SELECT m.PayYear"
fromSQL = " FROM client_list c INNER JOIN metrics m ON c.apexID = m.apexID"
groupSQL = " GROUP BY m.PayYear"
whereSQL = " WHERE 1=1"

Next, we will build the SELECT and GROUP BY portions of the SQL statement from values selected on a form:


In this case, we will Sum the Admin Fees and Gross Pay amounts and group those amounts by Salesforce Client, Quarter, and Salesforce Industry. We will be creating our report from a mini data warehouse we've built from data exported out of Salesforce.com and from another system called Summit Apex.

The following statements iterate through the different Display Value fields to retrieve the selected values and apply them to the SQL sections. Notice how the aggregate function is set in a variable called "func." If the user chooses "none" from the Aggregate Function radio group, then we will simply set the groupSQL string equal to "" near the end of the subprocedure.

    ' Time Detail Level
    If Me.optTime.Value = 1 Then
        selSQL = selSQL & ", m.PayMonth"
        groupSQL = groupSQL & ", m.PayMonth"
    ElseIf Me.optTime.Value = 2 Then
        selSQL = selSQL & ", m.quarter"
        groupSQL = groupSQL & ", m.quarter"
    End If
    
    ' Client Level
    If Me.optClLevel.Value = 1 Then
        selSQL = selSQL & ", c.ApexID, c.ApexName"
        groupSQL = groupSQL & ", c.ApexID, c.ApexName"
    ElseIf Me.optClLevel.Value = 2 Then
        selSQL = selSQL & ", c.ApexSFIDCaseSens, c.SalesforceName"
        groupSQL = groupSQL & ", c.ApexSFIDCaseSens, c.SalesforceName"
    End If
    
    ' Aggregate Function
    If Me.optFunc.Value = 1 Then
        func = "SUM"
    ElseIf Me.optFunc.Value = 2 Then
        func = "AVG"
    ElseIf Me.optFunc.Value = 3 Then
        func = "MAX"
    ElseIf Me.optFunc.Value = 4 Then
        func = "MIN"
    ElseIf Me.optFunc.Value = 5 Then
        func = ""
    End If
    
    ' Metrics
    For Each indx In Me.lstMetrics.ItemsSelected
            If func = "" Then
                selSQL = selSQL & ", m." & Me.lstMetrics.ItemData(indx)
            Else
                If func <> "SUM" Then
                    selSQL = selSQL & ", " & func & "(m." & Me.lstMetrics.ItemData(indx) & ") as " & Me.lstMetrics.ItemData(indx) & func & "PerMonth"
                Else
                    selSQL = selSQL & ", " & func & "(m." & Me.lstMetrics.ItemData(indx) & ") as " & Me.lstMetrics.ItemData(indx) & "_" & func
                End If
            End If

    Next indx
    
    ' Attributes
    For Each indx In Me.lstAttr.ItemsSelected
        selSQL = selSQL & ", c." & Me.lstAttr.ItemData(indx)
        groupSQL = groupSQL & ", c." & Me.lstAttr.ItemData(indx)
    Next indx 

Next, we choose from a set of Attribute Filters to start building the WHERE clause.



In this case, we are limiting the results to the year 2013 and to the state of Utah. Here's the code to handle all of the Attribute filters:

   ' Date Range
    If Nz(Me.txtBegDt.Value, "") <> "" And Nz(Me.txtEndDt.Value, "") <> "" Then
        whereSQL = whereSQL & " AND m.MonthEndDate BETWEEN #" & Me.txtBegDt.Value & "# AND #" & Me.txtEndDt.Value & "#"
    End If
    
    'Company Number
    If Nz(Me.txtCoNum.Value, "") <> "" Then
          whereSQL = whereSQL & " AND c.ApexCompany = '" & Me.txtCoNum.Value & "'"
    End If
    
    ' Industry Filter In
    Dim industryList As String
    industryList = ""
    For Each indx In Me.lstIndustry.ItemsSelected
        industryList = industryList & "'" & Me.lstIndustry.ItemData(indx) & "',"
    Next indx
    If industryList <> "" Then
        whereSQL = whereSQL & " AND c.SFIndustry IN (" & Mid(industryList, 1, Len(industryList) - 1) & ")"
    End If
    
    ' State Filter In
    Dim stateList As String
    stateList = ""
    For Each indx In Me.lstState.ItemsSelected
        stateList = stateList & "'" & Me.lstState.ItemData(indx) & "',"
    Next indx
    If stateList <> "" Then
        whereSQL = whereSQL & " AND c.State IN (" & Mid(stateList, 1, Len(stateList) - 1) & ")"
    End If
    
    ' City Filter In
    Dim cityList As String
    cityList = ""
    For Each indx In Me.lstCity.ItemsSelected
        cityList = cityList & "'" & Me.lstCity.ItemData(indx) & "',"
    Next indx
    If cityList <> "" Then
        whereSQL = whereSQL & " AND c.city IN (" & Mid(cityList, 1, Len(cityList) - 1) & ")"
    End If
    
    ' Zip Filter In
    Dim zipList As String
    zipList = ""
    For Each indx In Me.lstZip.ItemsSelected
        zipList = zipList & "'" & Me.lstZip.ItemData(indx) & "',"
    Next indx
    If zipList <> "" Then
        whereSQL = whereSQL & " AND c.zip IN (" & Mid(zipList, 1, Len(zipList) - 1) & ")"
    End If
    
    ' Apex Client Filter In
    Dim apexClList As String
    apexClList = ""
    For Each indx In Me.lstApexClients.ItemsSelected
        apexClList = apexClList & "'" & Me.lstApexClients.ItemData(indx) & "',"
    Next indx
    If apexClList <> "" Then
        whereSQL = whereSQL & " AND c.ApexID IN (" & Mid(apexClList, 1, Len(apexClList) - 1) & ")"
    End If
    
    ' Salesforce Client Filter In
    Dim sfClList As String
    sfClList = ""
    For Each indx In Me.lstSFClients.ItemsSelected
        sfClList = sfClList & "'" & Me.lstSFClients.ItemData(indx) & "',"
    Next indx
    If sfClList <> "" Then
        whereSQL = whereSQL & " AND c.ApexSFIDCaseSens IN (" & Mid(sfClList, 1, Len(sfClList) - 1) & ")"
    End If

By breaking the SQL statement down into its component parts, we can easily add additional filters and attributes to the form to give the end user maximum flexibility in building their own custom reports.

The following code concatenates each SQL section together to create the final statement:

    allSQL = selSQL & fromSQL & whereSQL & groupSQL

I used debug.print to print the allSQL variable to the Immediate window in the VBA editor. Here's what the final SQL statement looks like:

SELECT m.PayYear, m.quarter, c.ApexSFIDCaseSens, c.SalesforceName, SUM(m.AdminFees) as AdminFees_SUM, SUM(m.GrossPay) as GrossPay_SUM, c.SFIndustry
FROM client_list c INNER JOIN metrics m ON c.apexID = m.apexID
WHERE 1=1 AND m.MonthEndDate BETWEEN #1/1/2013# AND #12/31/2013# AND c.State IN ('UT') GROUP BY m.PayYear, m.quarter, c.ApexSFIDCaseSens, c.SalesforceName, c.SFIndustry

In order to run the SQL statement and show the report, we feed the SQL statement to the Query Definition object:

    qdf.SQL = allSQL

Finally, we open the report and display the output:

DoCmd.OpenQuery "CustomReport", acViewNormal

We could just as easily export the report to Excel as follows:

DoCmd.TransferSpreadsheet acExport, acSpreadsheetTypeExcel12Xml, "CustomReport", curPath & "\Excel Exports\CustomReport.xlsx"

Wednesday, November 20, 2013

How to make a household budget calculator with Excel


  • Open a new Excel workbook. 
  • Name the first worksheet Expenditures. 
  • Name the second worksheet Budget Calculations. 
  • Delete any remaining worksheets.





  • On the first worksheet, add the text Expenditures to cell A1. 
  • Merge and center A1 across three columns and two rows. 
  • Add the following text to cells A3, B3, and C3: Category, Amount, Description. 







  • Set up the Budget Calculations worksheet as follows:







  • On the Expenditures worksheet, add the following data validation to cell A4. Apply the same validation to 100 or so cells in the same column. (Click the bottom right corner of cell A4 and drag down to apply the validation to the column.) The data validation will add a dropdown list in column A of the Expenditures worksheet that is comprised of the categories from column A in the Budget Calculations worksheet. 



















  • Add the following formula to cell C4 of the Budget Calculations worksheet:
    • =SUMIF(Expenditures!$A$4:$B$151,A4,Expenditures!$B$4:$B$151)
  • The above formula adds up the expenditures for each category entered into column A of the Budget Calculations spreadsheet. Drag the formula down the column. 
  • Add categories to column A of the Budget Calculations according to the way you want to categorize expenditures. These can be changed as needed. 
  • Add the amounts you want to budget for each category to column B of the Budget Calculations. 



  • Add the following formula to cell D4 to calculate the Remaining amount: =B4 - C4
    • Drag the formula down.
  • Add totals to the bottom row to show the total Budgeted amount, the total Actual amount spent, and the total Remaining.
  • Click here to download the file: https://sites.google.com/site/benberginsfiles/Budget%20Calculator.xlsx
  • You can save the file as a template so you can create a fresh copy each month. Just go to Save As and choose Excel Template. 
    • I usually name each file after the year and month I'm tracking: 2013-12_Budget.xlsx for example. 





Saturday, November 9, 2013

How to secure your Access application using Active Directory

If your network is part of an Active Directory domain, then you can easily use domain groups as a means of securing your Microsoft Access application.

The IsMember function determines if a logged in user is a member of a specific Active Directory group or not.

Function IsMember(strDomain As String, strGroup As String, strMember As String) As Boolean
    Dim adGrp As Object
    Dim strPath As String
    
    strPath = "WinNT://" & strDomain & "/"
    Set adGrp = GetObject(strPath & strGroup & ",group")
    IsMember = adGrp.IsMember(strPath & strMember)
    
End Function

If IsMember("mydomain.com", "Accounting", Environ("Username")) Then
    ' Allow access to sensitive information
Else
    ' Deny access to sensitive information 
End If

You can use the IsMember function to show or hide forms, to show or hide buttons, to add filters to SQL code, etc. Here are some code samples of ways you might secure your application using Active Directory groups:

This code hides and disables a button when the form loads:

Private Sub Form_Load()
    If IsMember("mydomain.com", "Domain Admins", Environ("Username")) Then
        Me.cmdLoadData.Visible = True
        Me.cmdLoadData.Enabled = True
    Else
        Me.cmdLoadData.Visible = False
        Me.cmdLoadData.Enabled = False
    End If
End Sub

You can also use the IsMember function as you build a dynamic SQL Statement. The following code allows Executives to see anyone's salary information but only allows others to see their own salary information.

    If IsMember("mydomain.com", "Executives", Environ("Username")) Then
        strSQL = "SELECT * FROM SalaryInfo"
    Else
         strSQL = "SELECT * FROM SalaryInfo" _
                        & " WHERE Username = '" & Environ("Username") & "'"
    End If









Monday, November 4, 2013

How to save a Word document as a PDF using Visual Basic (VBA)

Set a reference to the Microsoft Word object library.






















Dim wordApp As Word.Application
Dim wordDoc As Word.Document
Dim pdfName As String
Dim curPath as String
curPath = CurrentProject.Path  

Set wordApp = CreateObject("Word.Application")
Set wordDoc = wordApp.Documents.Open(curPath & "\Templates\Some Doc.docx")

pdfName= curPath & "\Some Folder\Converted Doc.pdf"

wordDoc.ExportAsFixedFormat OutputFileName:=pdfName, ExportFormat:= _
wdExportFormatPDF, OpenAfterExport:=False, OptimizeFor:=wdExportOptimizeForPrint, Range:=wdExportAllDocument, _
Item:=wdExportDocumentContent, IncludeDocProps:=True

wordDoc.Close saveChanges:=False

How to delete files with Visual Basic (VBA)

dim folderName as String
folderName = "\\Server Name\Share Name\Some Folder\Old Files\"    

On Error Resume Next
        Kill folderName & "*.*" 
On Error GoTo 0


To delete a specific file type, replace the second "*" wildcard with the file extension of the files you want to delete:

On Error Resume Next
        Kill folderName & "*.xlsx" 
On Error GoTo 0

Be careful with the Kill function as you can inadvertently delete valuable files. Make sure you specify the folder from which you want to delete files. And it's best to specify the file type or the file name itself. Be as specific as possible with the Kill command.

VBA Functions for working with Pervasive SQL (PSQL)

The following function will set up your connection to a Pervasive SQL database. You will need to set a reference to the Microsoft Active X Data Objects library. This function creates a DSN-less connection that will allow you to execute pass through queries using raw SQL. Pass through queries require more knowledge of SQL than using linked tables and the Access query builder. But linked tables can be a hassle to deal with and create numerous maintenance headaches. And pass-through queries are more versatile and run more efficiently than linked table queries.

Public Function psqlConn(dbName as String) As ADODB.Connection

    Dim conn As New ADODB.Connection
    Dim strConn As String
    strConn = "Driver={Pervasive ODBC Client Interface}; ServerName=SomeServer.1583;ServerDSN=" & dbName & "; UID=Uname; PWD=Pass; ArrayFetchOn=1; ArrayBufferSize=8; TransportHint=TCP:SPX;"
    conn.Open strConn
    Set psqlConn = conn
 
End Function



The next function is for SQL SELECT queries. You'll use the previous function to set up the connection and pass it as the conn argument to the function. The function returns and ADODB record set. Remember to close the connection when you are done with it (conn.Close). 

Dim conn As New ADODB.Connection
Set conn = psqlconn(dbName)
dim strSQL as String

strSQL = "SELECT * FROM SomeTable WHERE id = '" & varID & "'"

Public Function psqlQuery(conn As ADODB.Connection, strSQL As String) As ADODB.Recordset

    Dim rs As New ADODB.Recordset
    Dim cmd As New ADODB.Command
    Set cmd.ActiveConnection = conn
    cmd.CommandTimeout = 120 ' Keep queries from timing out after the default of 30 seconds
    cmd.CommandText = strSQL
    cmd.CommandType = adCmdText
    Set rs = cmd.Execute
    
    Set psqlQuery = rs
    

End Function


The next function is for Insert, Update, and Delete queries. 

Public Function psqlNonQuery(conn As ADODB.Connection, strSQL As String) As Boolean

    Dim cmd As New ADODB.Command
    Set cmd.ActiveConnection = conn
    cmd.CommandTimeout = 120 ' Keep queries from timing out after the default of 30 seconds
    cmd.CommandText = strSQL
    cmd.CommandType = adCmdText
    cmd.Execute
    
    psqlNonQuery = True
    
End Function