Mail-Merge loop and passing query parameters - vba

I am looking to optimize and rewrite some code to loop a bunch of mail merges that run on command click. Right now, there exists 8 individual merges that run after each other. My problem is that I run 8 queries, which all require a start/end date to pull information from a table and feed it over to the mail merge document. I would like to specify the parameter date, through say, inputbox and then pass it to all the merges in the loop.
For i = 1 To 1 'will change to 8 :)
Select Case i
Case 1
wordDoc = pathToDocToMerge
sqlStr = "SELECT * FROM [QUERY - FIRST LETTERS]"
sqlConex = "QUERY - FIRST LETTERS"
strExport = "Normal"
fileout = desktopPath & "\ " & strmonth & " - FIRSTLETTERS.pdf"
Case 2
Case 3
Case 4
Case 5
Case 6
Case 7
Case 8
End Select
Set objword = GetObject(wordDoc)
objword.Application.Visible = False
objword.MailMerge.OpenDataSource _
Name:=pathToMDB, _
LinkToSource:=True, _
Connection:=sqlConex, _
sqlstatement:=sqlStr
objword.MailMerge.Destination = wdSendToNewDocument
objword.MailMerge.Execute
If printtopdf = vbYes Then
objword.Application.ActiveDocument.ExportAsFixedFormat OutputFileName:= _
fileout _
, ExportFormat:=wdExportFormatPDF, OpenAfterExport:=True, OptimizeFor:= _
wdExportOptimizeForPrint, Range:=wdExportAllDocument
objword.Application.ActiveDocument.close SaveChanges:=wdDoNotSaveChanges
Else
objword.Application.Options.PrintBackground = False
'Print one copy of the letter
objword.Application.ActiveDocument.PrintOut
'Close Word and do not save the changes
End If
objword.Application.Quit SaveChanges:=wdDoNotSaveChanges
Set objword = Nothing
Next i
I know parameters can be passed like so:
Dim dbs As DAO.Database
Dim qdf As DAO.QueryDef
Dim prm As DAO.Parameter
Dim rst As DAO.Recordset
Set dbs = CurrentDb
Set qdf = dbs.QueryDefs("qry_AMIS")
For Each prm In qdf.Parameters
prm = Eval(prm.Name)
Next prm
Set rst = qdf.OpenRecordset
But I am unsure how to tie this into a mail merge, seeing as the query becomes RST. Can anyone shed some light?

After a quick look at the documentation for MailMerge.OpenDataSource here, I don't see why you couldn't just change your existing code...
sqlStr = "SELECT * FROM [QUERY - FIRST LETTERS]"
...to something like...
sqlStr = "SELECT * FROM [QUERY - FIRST LETTERS] WHERE SomeDate=#" & Format(CDate(Me.txtSpecifyDate.Value), "yyyy-mm-dd") & "#"
...where Me.txtSpecifyDate is a text box on the form you use to prompt the user to supply the date.

Related

How to Transfer VBA UserForm Data To Access Database?

I have created a user form in excel to save my records in a sheets like sheet1.
But after few days working with this UserForm, it is now goes slower, because of heavy data saving in sheet1.
Now I want to save all records to a database and want to keep clean my sheet1.
So I can work on my UserForm easily or without any delay. Also wants updates my record by calling it via serial numbers.
but I don't want to keep any record in my sheet1.
my little code is below: -
Sub cmdAdd_Click()
On Error GoTo ErrOccured
BlnVal = 0
If BlnVal = 0 Then Exit Sub
With Application
.ScreenUpdating = False
.EnableEvents = False
End With
Dim txtId, txtName, GenderValue, txtLocation, txtCNum, txtEAddr, txtRemarks
Dim iCnt As Integer
iCnt = fn_LastRow(Sheets("Data")) + 1
If frmData.obMale = True Then
GenderValue = "Male"
Else
GenderValue = "Female"
End If
With Sheets("Data")
.Cells(iCnt, 1) = iCnt - 1
.Cells(iCnt, 2) = frmData.txtName
.Cells(iCnt, 3) = GenderValue
.Cells(iCnt, 4) = frmData.txtLocation.Value
.Cells(iCnt, 5) = frmData.txtEAddr
.Cells(iCnt, 6) = frmData.txtCNum
.Cells(iCnt, 7) = frmData.txtRemarks
.Columns("A:G").Columns.AutoFit
.Range("A1:G1").Font.Bold = True
.Range("A1:G1").LineStyle = xlDash
End If
End With
Dim IdVal As Integer
IdVal = fn_LastRow(Sheets("Data"))
frmData.txtId = IdVal
ErrOccured:
'TurnOn screen updating
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
I will always be grateful to you.
Then, please try the next way. I will try creating of the necessary DB, table and fields using Excel VBA, too:
Copy the next piece of code which will create an empty DB, on the path you want:
Sub CreateEmptyDB()
Dim strPath As String, objAccess As Object
strPath = "C:\Your path\testDB"
Set objAccess = CreateObject("Access.Application")
Call objAccess.NewCurrentDatabase(strPath)
objAccess.Quit
End Sub
Programatically create the necessary table with its fields (`Start Date' added only to see how this type of data is handled...):
Sub createTableFields()
'It needs a reference to `Microsoft ActiveX Data Objects 2.x Library` (x = 2 to 9)
Dim Catalog As Object, cn As ADODB.Connection
Dim dbPath As String, scn As String, strTable As String
dbPath = "C:\Teste VBA Excel\testAccess\testDB.accdb"
strTable = "MySpecial_Table"
scn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & dbPath & ";"
Set Catalog = CreateObject("ADOX.Catalog")
Set cn = New ADODB.Connection
With cn
.Open scn
.Execute "CREATE TABLE " & strTable & " ([Name] text(255) WITH " & _
"Compression, " & "[Gender] text(255) WITH Compression, " & _
"[Location] text(255) WITH Compression, " & _
"[Address] text(255) WITH Compression, " & _
"[Number] number, " & _
"[Remarks] text(255) WITH Compression, " & _
"[Start Date] datetime)"
End With
cn.Close
End Sub
Add records to the newly created DB/Table:
Sub FillDataInDB()
'It needs a reference to `Microsoft ActiveX Data Objects 2.x Library` (x = 2 to 9)
Dim AccessDB As String, strTable As String, sql As String
Dim con As ADODB.Connection, rs As ADODB.Recordset, lastNo As Long
AccessDB = "C:\Teste VBA Excel\testAccess\testDB.accdb"
strTable = "MySpecial_Table"
Set con = CreateObject("ADODB.connection")
con.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & AccessDB
sql = "SELECT * FROM " & strTable
Set rs = CreateObject("ADODB.Recordset")
rs.CursorType = 1 'adOpenKeyset on early binding
rs.LockType = 3 'adLockOptimistic on early binding
rs.Open sql, con
If rs.RecordCount = 0 Then
lastNo = 0 'when no records in the table
Else
rs.MoveLast: lastNo = rs("Number") 'the last recorded value
End If
rs.AddNew
rs("Name") = "Test name" 'frmData.txtName
rs("Gender") = "Test gender" 'GenderValue
rs("Location") = "Test Location" 'frmData.txtLocation.Value
rs("Address") = "Test Address" 'frmData.txtEAddr
rs("Number") = IIf(lastNo = 0, 100, lastNo + 1) 'auto incrementing against the last value
'but starting from 100
'you can use frmData.txtCNum
rs("Remarks") = "Remarkable table..." 'frmData.txtRemarks
rs("Start Date") = Date
rs.Update
rs.Close: con.Close
Set rs = Nothing: Set con = Nothing
End Sub
Run the first two pieces of code in consecutive order (only once) and then start playing with the third one...
You can read the newly created DB Table (returning in an Excel sheet) in this way:
Sub ADO_Connection_ReadTable()
Dim conn As New Connection, rec As New Recordset, sh As Worksheet
Dim AccessDB As String, connString, query As String, strTable As String
AccessDB = "C:\Teste VBA Excel\testAccess\testDB.accdb"
strTable = "MySpecial_Table"
Set sh = ActiveSheet 'use here the sheet you want
connString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & AccessDB
conn.Open connString
query = "SELECT * from " & strTable & ";"
rec.Open query, conn
'return in the sheet
sh.cells.ClearContents
'getting data from the recordset if any and returning some in columns A:B:
If (rec.RecordCount <> 0) Then
Do While Not rec.EOF
With sh.Range("A" & sh.cells(Rows.count, 1).End(xlUp).row).Offset(1, 0)
.Value2 = rec.fields(0).Value
.Offset(0, 1).Value2 = rec.fields(3)
End With
rec.MoveNext
Loop
End If
rec.Close: conn.Close
End Sub
You can use a query to return specific data according to a specific table field. You can find plenty of examples on the internet.
I tried also showing how to handle an automate recording for the 'Number' field. Of course, if you are able to keep track of it in a different way, you can record it as you need/wont.
Please, test the above code(s) and send some feedback. You can use the DB path as a Private constant at the module level and much other ways to optimize the code. It is just a minimum workable solution only showing the way... :)

recordset close when updatesql?

I have an Excel VBA to update data in Access. I use the script below.
I just have a small question. if I close the recordset with rs.close I get the message 'operation is not allowed when the object is closed'. If I do not use the rs.close then it works perfectly.
Is the rs.close really necessary when using a sql?
I also tried to use cn.execute updatesql. How to set the recordlocking property?
Public Sub Upload_to_DB()
Call setparameters
' exports data from the worksheet to a table in an Access database
Dim cn As ADODB.Connection ws As Worksheet , updatesql As String, r_fld As Long, r_val As Long, c As Long, str As String, rs As ADODB.Recordset
Set cn = CreateObject("ADODB.Connection")
cn.mode = 16 + 3
cn.Open "Provider=Microsoft.ACE.OLEDB.12.0; " & _
"Data Source= " & Path & filename & ";"
cn.CursorLocation = adUseClient
Set rs = CreateObject("ADODB.Recordset")
Set ws = Sheets("Opportunity_down_and_upload")
With ws
r_fld = 13
r_val = 14
c = 3
str = Empty
str = .Cells(r_fld, c).Offset(0, -1).Value & " = " & .Cells(r_val, c).Offset(0, -1).Value
continue = True
Do
str = str & " , " & .Cells(r_fld, c).Value & " = " & .Cells(r_val, c).Value
c = c + 1
If IsEmpty(.Cells(r_fld, c)) Then continue = False
Loop Until continue = False
End With
'This part is important:'
updatesql = "UPDATE tbl_D_opp_prod_offer SET "
updatesql = updatesql & str
updatesql = updatesql & " WHERE [Opp_ID] = " & ws.Range("A10") & ";"
rs.Open updatesql, cn, , adLockOptimistic, adCmdText
cn.Close
Set rs = Nothing
Set cn = Nothing
End Sub
Is the rs.close really necessary when using a sql?
Nope. In fact, rs.close is rarely necessary at all. VBA will automatically remove objects when they go out of scope, which means that if they were defined in a sub, they get removed at the end of it. For ADODB, there were some bugs in an old version causing this to be unreliable, which means old code often contains rs.close or connection.close while they're not really necessary
I also tried to use cn.execute updatesql. How to set the recordlocking property?
You can't. It doesn't really apply to action queries. The record locking options define if a record should be locked when starting to edit it, when updating it, or can't be locked at all because it's read-only, and since you're using an update query there's no time in between those

dbf files in Excel with SQL

Every day i need to make a report for salesman, we have 2 dbf files from witch i automatic want to make the report in Excel. Report from 1 dbf file works perfect, but i don't know how to join 2 dbf files in VBA.
I have to following script:
Option Explicit
Sub ReadDBF()
Dim con As Object
Dim rs As Object
Dim DBFFolder As String
Dim FileName As String
Dim FileName1 As String
Dim sql As String
Dim myValues() As String
Dim i As Integer
Dim j As Integer
Application.ScreenUpdating = False
DBFFolder = ThisWorkbook.Path & "\"
FileName = "project1.dbf"
FileName1 = "project2.dbf"
On Error Resume Next
Set con = CreateObject("ADODB.connection")
If Err.Number <> 0 Then
MsgBox "Connection was not created!", vbCritical, "Connection error"
Exit Sub
End If
On Error GoTo 0
con.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & DBFFolder & ";Extended Properties=dBASE IV;"
sql = "SELECT project_id, COUNT(*) AS total, salesman, MAX(date) AS max_date, projectname FROM " & FileName & FileName1 & " where DateValue(datumtijd) = Date() and FileName.project_id = FileName1.project_id " & "group by project_id, salesman"
On Error Resume Next
Set rs = CreateObject("ADODB.recordset")
If Err.Number <> 0 Then
MsgBox "Connection was not created!", vbCritical, "Connection error"
Exit Sub
End If
On Error GoTo 0
rs.CursorLocation = 3
rs.CursorType = 1
rs.Open sql, con
ReDim myValues(rs.RecordCount, 20)
i = 1
If Not (rs.EOF And rs.BOF) Then
'Go to the first record.
rs.MoveFirst
Do Until rs.EOF = True
myValues(i, 1) = rs!project_id
myValues(i, 2) = rs!salesman
myValues(i, 3) = rs!Total
myValues(i, 4) = rs!max_date
myValues(i, 5) = rs!project
rs.MoveNext
i = i + 1
Loop
Else
rs.Close
con.Close
Set rs = Nothing
Set con = Nothing
Application.ScreenUpdating = True
MsgBox "There are no records in the recordset!", vbCritical, "No Records"
Exit Sub
End If
Sheet1.Activate
For i = 1 To UBound(myValues)
For j = 1 To 4
Cells(i + 1, j) = myValues(i, j)
Next j
Next i
rs.Close
con.Close
Set rs = Nothing
Set con = Nothing
Columns("A:D").EntireColumn.AutoFit
Application.ScreenUpdating = True
MsgBox "The values were read from recordset successfully!", vbInformation, "Done"
End Sub
The query doesn't work just by adding the two tables together as the from source. What is it you are trying to actually get? Also, you should never concatenate strings to build a query. They should always be parameterized.
Now, that said, your syntax is simply incorrect. Your variables are referred to as "FileName" and "FileName1", but the underlying TABLES you are querying from are "Project1" and "Project2" respectively. You should learn about aliases to help simplify queries too, and learn proper JOIN syntax.
By listing the tables one after the other with no comma will cause it to fail. Here is a more accurate syntax and formatted for readability. Then I have edited your original query to match the context. You should also always qualify the field names from the respective table so others trying to help know where things are coming from. In the sample below, I have only GUESSED at which table has which columns by using the alias "P1." and "P2." respectively. You will probably need to change them. Also, since "Date" could be interpreted as a reserved word, I have wrapped it in [], but may need to be changed to tick characters (next to number 1) ``
select
P1.Project_ID,
COUNT(*) as Total,
P1.SalesMan,
MAX( P2.[Date] ) as Max_Date,
P1.ProjectName
from
Project1 P1
JOIN Project2 P2
on P1.Project_ID = P2.Project_ID
where
DateValue( P2.datumtijd ) = date()
group by
P1.Project_ID,
P1.SalesMan
The JOIN clause identifies the relation BETWEEN the two tables on respective columns. The WHERE clause is additional criteria you are looking for.
sql = "SELECT project_id, COUNT(*) AS total, salesman, " & _
"MAX(date) AS max_date, projectname " & _
" FROM " & FileName & " P1 " & _
" JOIN " & FileName1 & " P2 ON P1.Project_ID = P2.Project_ID" & _
" where DateValue(datumtijd) = Date() " & _
" group by project_id, salesman"

Run time Error too few parameters Expected 2

I am trying to run a query from the Access Query designer that is working fine in Access but when I try to bring the statement across to VBA it is giving me this error message:
Run time error too few parameters. Expected 2.
I have printed the statement in the immediate window and run it in Access and it is running without asking for parameters. I have done a number of web searches the general consensus seems to be to declare it all in VBA, including the parameters -
Private Sub CmdAppend_Click()
Dim db1 As Database
Dim mystr As Recordset2
Dim UserName As String
Dim UpdateSQL As String
Dim SelectIDSQL As String
Dim checkstr As String
If Validate_Data = True Then
UserName = Environ$("Username")
SelectIDSQL = "Select Distinct ChecklistResults.[StaffID]" _
& " From ChecklistResults" _
& " Where (((ChecklistResults.[ClientID])=[Forms]![TeamLeader]![ComClientNotFin])" _
& " And ((ChecklistResults.[DateofChecklist])=[Forms]![TeamLeader]![ComDateSelect])" _
& " AND ((ChecklistResults.[ManagerID]) Is Null));"
Debug.Print SelectIDSQL
Set db1 = CurrentDb
Set mystr = db1.OpenRecordset(SelectIDSQL)
checkstr = mystr!StaffID
If checkstr <> UserName Then
I receive the above error message when I try to set mystr to the recordset. I think I can get the recordset by following the format below but is there a way of getting the above SQL statement/assignment to work?
Dim qdf1 As DAO.QueryDef
Set qdf1 = db1.QueryDefs("Get_StaffID")
qdf1.Parameters(0) = [Forms]![TeamLeader]![ComClientNotFin]
qdf1.Parameters(1) = [Forms]![TeamLeader]![ComDateSelect]
Set rst1 = qdf1.OpenRecordset(dbOpenDynaset)
As I look at this page, I see examples where the OpenRecordSet method takes two arguments. You have an error message that says something was expecting 2 parameters. Try changing this:
Set mystr = db1.OpenRecordset(SelectIDSQL)
to this:
Set mystr = db1.OpenRecordset(SelectIDSQL, dbOpenDynaset)
Thanks for the input, I used the following code to get the result I was looking for. It uses the query SelectClientID to return the ID of the person who completed the first stage of a checklist. it then checks the person who has done the second check and if they match it returns an error message. If two different people have completed it, it uses the SQL statement to update the previous record with the second checker's ID -
Private Sub CmdAppend_Click()
Dim rst1 As Recordset2
Dim db1 As Database
Dim mystr As Recordset2
Dim UserName As String
Dim UpdateSQL As String
Dim SelectIDSQL As String
Dim checkstr As String
Dim qdf1 As DAO.QueryDef
Set db1 = CurrentDb
Set qdf1 = db1.QueryDefs("SelectClientID")
qdf1.Parameters(0) = [Forms]![TeamLeader]![ComClientNotFin]
qdf1.Parameters(1) = [Forms]![TeamLeader]![ComDateSelect]
Set rst1 = qdf1.OpenRecordset(dbOpenDynaset)
If Validate_Data = True Then
UserName = Environ$("Username")
UpdateSQL = "UPDATE ChecklistResults" _
& " SET ChecklistResults.[ManagerID] = '" & UserName & "'" _
& " WHERE (((ChecklistResults.[ClientID])=[Forms]![TeamLeader]![ComClientNotFin])" _
& " AND ((ChecklistResults.[DateofChecklist])=[Forms]![TeamLeader]![ComDateSelect])" _
& " AND ((ChecklistResults.[ManagerID]) Is Null));"
checkstr = rst1!StaffID
If checkstr <> UserName Then
DoCmd.SetWarnings False
DoCmd.RunSQL UpdateSQL
DoCmd.SetWarnings True
DoCmd.Close
Else
MsgBox ("This Checklist was created by you and cannot therefore Checked by you")
End If
Else
Exit Sub
End If
End Sub

Looking up Access database in Excel

I want to do something very simple: I have an Access database with one table mapping thousands of product IDs to product information fields. In an Excel worksheet, the user types in perhaps 100 product IDs in the first column. I need for the remaining columns to pull in information from the Access database for the corresponding IDs. Specifically:
if I use MS-Query, it seems to insist on the output being a table. I simply want the output to be inside a single cell. Preferably, a formula that involves a SQL-type query.
I don't want any of the values to be updated automatically, but rather want all the columns updated only on user demand (the user could either choose refresh through a menu, or a VBA-based refresh button on the sheet is fine as well).
I'm thinking this would be a straightforward use case, but it seems surprisingly hard to find a solution. Thank you in advance!
Working from Excel, you can use ADO to connect to a database. For Access and Excel 2007/2010, you might:
''Reference: Microsoft ActiveX Data Objects x.x Library
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
''Not the best way to refer to a workbook, but convenient for
''testing. it is probably best to refer to the workbook by name.
strFile = ActiveWorkbook.FullName
''Connection string for 2007/2010
strCon = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & strFile _
& ";Extended Properties=""Excel 12.0 xml;HDR=Yes;"";"
cn.Open strCon
''In-line connection string for MS Access
scn = "[;DATABASE=Z:\Docs\Test.accdb]"
''SQL query string
sSQL = "SELECT a.Stuff, b.ID, b.AText FROM [Sheet5$] a " _
& "INNER JOIN " & scn & ".table1 b " _
& "ON a.Stuff = b.AText"
rs.Open sSQL, cn
''Write returned recordset to a worksheet
ActiveWorkbook.Sheets("Sheet7").Cells(1, 1).CopyFromRecordset rs
Another possibility returns a single field from MS Access. This example uses late binding, so you do not need a library reference.
Dim cn As Object
Dim rs As Object
Dim strFile As String
Dim strCon As String
Dim strSQL As String
Dim s As String
Dim i As Integer, j As Integer
strFile = "z:\docs\test.accdb"
strCon = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & strFile
''Late binding, so no reference is needed
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
cn.Open strCon
''Select a field based on a numeric reference
strSQL = "SELECT AText " _
& "FROM Table1 a " _
& "WHERE ID = " & Sheets("Sheet7").[A1]
rs.Open strSQL, cn, 3, 3
Sheets("Sheet7").[B1] = rs!AText
OK, this may seem a bit lengthy - Create an Excel-table - in the first row (from column two) you have the Fieldnames Exactly as you have them in the access-table, in the first column you have the desired key-values (e.g. CustomerIDs).
When you run the macro it fills in what it finds...
Sub RefreshData()
Const fldNameCol = 2 'the column with the first fieldname in it'
Dim db, rst As Object
Set db = DBEngine.workspaces(0).OpenDatabase("C:\path\to\db\name.accdb")
Set rst = db.openrecordset("myDBTable", dbOpenDynaset)
Dim rng As Range
Dim showfields() As Integer
Dim i, aRow, aCol As Integer
ReDim showfields(100)
Set rng = Me.Cells
aRow = 1 'if you have the fieldnames in the first row'
aCol = fldNameCol
'***** remove both '' to speed things up'
'On Error GoTo ExitRefreshData'
'Application.ScreenUpdating = False'
'***** Get Fieldnames from Excel Sheet'
Do
For i = 0 To rst.fields.Count - 1
If rst.fields(i).Name = rng(aRow, aCol).Value Then
showfields(aCol) = i + 1
Exit For
End If
Next
aCol = aCol + 1
Loop Until IsEmpty(rng(aRow, aCol).Value)
ReDim Preserve showfields(aCol - 1)
'**** Get Data From Databasetable'
aRow = 2 'startin in the second row'
aCol = 1 'key values (ID) are in the first column of the excel sheet'
Do
rst.FindFirst "ID =" & CStr(rng(aRow, aCol).Value) 'Replace ID with the name of the key field'
If Not rst.NoMatch Then
For i = fldNameCol To UBound(showfields)
If showfields(i) > 0 Then
rng(aRow, i).Value = rst.fields(showfields(i) - 1).Value
End If
Next
End If
aRow = aRow + 1
Loop Until IsEmpty(rng(aRow, aCol).Value)
ExitRefreshData:
Application.ScreenUpdating = True
On Error GoTo 0
End Sub
And if you dont want your fieldnames in the excel sheet replace the paragraph "Get Fieldnames From Excelsheet" with this:
fieldnames = Split("field1name", "", "", "field3name")
For j = 0 To UBound(fieldnames) - 1
For i = 0 To rst.fields.Count - 1
If rst.fields(i).Name = fieldnames(j) Then
showfields(j + fldNameCol) = i + 1
Exit For
End If
Next
Next
ReDim Preserve showfields(UBound(fieldnames) - 1 + fldNameCol)
and add this at the top
dim j as integer
dim fieldnames