I'm trying to work out a simple way to get "variable" data into a Word docs.
I have a client who is supplying Word templates for various procedures. This would form a document that goes out to several locations, with personalized information for each location.
This could be done with a simple Mail Merge from Excel, but there are additional issues:
The doc will contain tables with supervisor contact information. It's the general info: Name, phone contacts, email, etc. But there could be 1 to 5 contacts. I could see that would mean several rows with the same key, meaning several copies where I only want one.
Is there an "easy" way to merge data into both document text and tables? The idea is to present the client with a "goof proof" solution so they can easily update the template and the data files.
I'm also looking at using Word variables, but one source said I'd be limited to 15 variable in a document.
Any ideas will be greatly appreciated. Also looking up previous questions on this stack.
One option would be to use a DATABASE field in a normal ‘letter’ mailmerge main document and a macro to drive the process. Here is an outline of this approach:
Suppose you have a data set that has fields for the subordinates' Firstname, Surname, Employee ID, & Job Title, and that against each record is recorded their Manager's ID, whose email address is also recorded. Let's also assume the mailmerge main document is kept in the same folder as the data source. In that case, you could use a DATABASE field coded as:
{DATABASE \d "{FILENAME \p}/../MM datasource.xlsx" \s " SELECT [Firstname], [Surname], [Employee ID], [Job Title] FROM [Sheet1$] WHERE [Manager ID] = {MERGEFIELD Manager_ID} ORDER BY [Job Title] " \l "15" \b "49" \h}
in an email merge, where 'MM datasource.xlsx' is the data source filename - it could even be an Access database filename, if that's what you chose to use. If the mailmerge main document is NOT kept in the same folder as the data source, you'd need to replace all of "{FILENAME \p}/../MM datasource.xlsx" with the full path & filename (plus the encompassing double-quotes). The \l and \b switches control the table format, while the \h switch inserts a table header row - see: https://support.office.com/en-us/article/Field-codes-Database-field-04398159-a2c9-463f-bb59-558a87badcbc?ui=en-US&rs=en-US&ad=US
Note: The field brace pairs (i.e. '{ }') for the above example are all created in the document itself, via Ctrl-F9 (Cmd-F9 on a Mac); you can't simply type them or copy & paste them from this post. Nor is it practical to add them via any of the standard Word dialogues. The spaces represented in the field constructions are all required.
Then, to drive the process, you could use a macro like:
Sub Merge_by_Group()
Application.ScreenUpdating = False
Dim MainDoc As Document, StrMgrID As String, i As Long
Set MainDoc = ActiveDocument
With MainDoc
For i = 1 To .MailMerge.DataSource.RecordCount
With .MailMerge
.Destination = wdSendToEmail
.SuppressBlankLines = True
With .DataSource
.FirstRecord = i
.LastRecord = i
.ActiveRecord = i
End With
If .DataSource.DataFields("Manager_ID") <> StrMgrID Then
StrMgrID = .DataSource.DataFields("Manager_ID")
.MailAddressFieldName = "Email"
.MailSubject = "Your Team's Details"
.MailFormat = wdMailFormatHTML
.Execute Pause:=False
End If
End With
Next i
End With
Application.ScreenUpdating = True
End Sub
A merge to email is assumed, but not necessary. If you want the output to go to a Word document, change:
.Destination = wdSendToEmail
to:
.Destination = wdSendToNewDocument
and delete:
.MailAddressFieldName = "Email"
.MailSubject = "Your Team's Details"
.MailFormat = wdMailFormatHTML
Note: If you rename the above macro as 'MailMergeToEmail' (or 'MailMergeToDoc' to send the output to a document), clicking on the 'Send Email Messages' (or 'Edit Individual Documents') button will intercept the merge and the process will run automatically. The potential disadvantage of intercepting the 'Send Email Messages' (or 'Edit Individual Documents') process this way is that you no longer get to choose which records to merge at that stage. However, you can still achieve the same outcome - and with greater control - via the 'Edit Recipient List' tools.
Conversely, if you're using a relational database or, an Excel workbook with a separate table listing each of the grouping criteria (e.g. the Manager_ID & email address) and any other fields that occur once per group, a DATABASE field in a normal ‘letter’ or 'email' mailmerge main document could be used without the need for a macro. In this case, the same DATABASE field would be used, but the mailmerge would be connected to the Excel worksheet (or database table) containing just the Manager_IDs and Names. For some working examples of this approach, see:
https://www.msofficeforums.com/mail-merge/37844-mail-merge-using-one-excel-file-multiple.html
https://www.msofficeforums.com/mail-merge/45044-using-mailmerge-include-grouped-information-letter-2.html#post151706
https://www.excelforum.com/excel-general/1273421-merge-excel-list-into-word-receipt.html#post5110813
(the second of these uses a macro to apply some additional formatting).
If you'd like to have each group's mailmerge output from the second approach saved as a separate document/pdf, see the Send Mailmerge Output to Individual Files topic in my Mailmerge Tips & Tricks page: https://www.msofficeforums.com/mail-merge/21803-mailmerge-tips-tricks.html
Related
Wow, my first stack question despite using the answers for years. Very exciting.
I'm fairly new to VBA and Excel and entirely new to Access, full disclosure. So Im trying to create a core database of lab reports, and I have a form for entering the information about a new report which adds info about the report to a master table of all reports, including assigning it a unique label. After entering the info, I then have a button which allows the user to select the Excel .csv file accompanying the report and imports it into the DB as a new table. It returns a success or error message. And it works! (And the code came from somewhere on here)
The problem is I'd like to then add a field to the new table that adds the label assigned to the new report to all records so it can be referenced by queries through the use of that label. I'd also like to add an index field to the new table if possible as it doesn't seem like importing the .csv as a table creates an index. I figure I'll make another sub that gets passed the new report name as a name for the new field (which will also be the value of the field through all records) and the table to append that to.
How do I pass this sub the newly imported table if I just imported it? I need this all to work from the button as it will mostly be my manager using this form/button to import new files, and they won't be able to just manually go into the tables as they are created and add fields (yes, I know that's the obvious solution, but trust me...this must be a button)
Heres the code I'm using (yes, I know lots of it could be done differently but it works!)
Public Function ImportDocument() As String
On Error GoTo ErrProc
Const msoFileDIalogFilePicker As Long = 3
Dim fd As Object
Set fd = Application.FileDialog(msoFileDIalogFilePicker)
With fd
.InitialFileName = "Data Folder"
.Title = "Enthalpy EDD Import"
With .Filters
.Clear
.Add "Excel documents", "*.xlsx; *.csv", 1
End With
.ButtonName = " Import Selected "
.AllowMultiSelect = False 'Manual naming currently requires one file at a time be imported
'If aborted, the Function will return the default value of Aborted
If .Show = 0 Then GoTo Leave 'fb.show returns 0 if 'cancel' is pressed
End With
Dim selectedItem As Variant
Dim NewTableName As String
NewTableName = InputBox(Prompt:="Enter the Report Name", _
Title:="Report Name")
For Each selectedItem In fd.SelectedItems 'could later be adapted for multiple imports
DoCmd.TransferText acImportDelim, , NewTableName, selectedItem, True 'Imports csv file selected, true is 'has headers'
Next selectedItem
'Return Success
ImportDocument = "Success"
'Append report label and index
AppendReportLabelField(NewTableName, #What to put here as the table to append to?)
'error handling
Leave:
Set fd = Nothing
On Error GoTo 0
Exit Function
ErrProc:
MsgBox Err.Description, vbCritical
ImportDocument = "Failure" 'Return Failure if error
Resume Leave
End Function
The AppendReportLabelField would get passed the name (and value) of the field and the name of the (newly imported) table. How do I pass it the table? NewTableName is just a string currently. If I can pass the new sub the table I'm sure the rest will be simple.
Thanks in advance for the help!
Consider storing all user input data in a single master table with all possible fields and use a temp, staging table (a replica of master) to migrate CSV table to this master table. During the staging, you can update the table with needed fields.
SQL (save as stored queries)
(parameterized update query)
PARAMETERS [ParamReportNameField] TEXT;
UPDATE temptable
SET ReportNameField = [ParamReportNameField]
(explicitly reference all columns)
INSERT INTO mastertable (Col1, Col2, Col3, ...)
SELECT Col1, Col2, Col3
FROM temptable
VBA
...
' PROCESS EACH CSV IN SUBSEQUENT SUBROUTINE
For Each selectedItem In fd.SelectedItems
Call upload_process(selectedItem, report_name)
Next selectedItem
Sub upload_process(csv_file, report_name)
' CLEAN OUT TEMP TABLE
CurrentDb.Execute "DELETE FROM myTempStagingTable"
' IMPORT CSV INTO TEMP TABLE
DoCmd.TransferText acImportDelim, , "myTempStagingTable", csv_file, True
' RUN UPDATES ON TEMP TABLE
With CurrentDb.QueryDefs("myParameterizedUpdateQuery")
.Parameters("ParamReportNameField").Value = report_name
.Execute dbFailOnError
End With
' RUNS APPEND QUERY (TEMP -> MASTER)
CurrentDb.Execute "myAppendQuery"
End Sub
If CSV uploads vary widely in data structure, then incorporate an Excel cleaning step to standardize all inputs. Alternatively, force users to use a standardized template. Staging can be used to validate uploads. Databases should not be a repository of many, dissimilar tables but part of a relational model in a pre-designed setup. Running open-ended processes like creating new tables by users on the fly can cause maintenance issues.
I am attempting to run the below line of code in a sub. The purpose of the sub overall is to automatically create agendas for recurring meetings, and notify the relevant people.
'Values for example;
MtgDate = CDate("11/06/2020")
Agenda ="Z:\Business Manual\10000 Management\11000 Management\11000 Communications\Operations Meetings\11335 - OPS CCAR Performance Review Agenda 11.06.20.docx" 'NB it's a string
'and the problematic line:
Word.Application.Documents(Agenda).BuiltinDocumentProperties("Publish Date") = MtgDate
Two questions:
1) Can I assign a document property just like that without opening the document? (bear in mind this vba is running from an excel sheet where the data is stored)
2) Will word.application.documents accept the document name as a string, or does it have to be some other sort of object or something? I don't really understand Word VBA.
Attempts so far have only resulted in
runtime error 427 "remote server machine does not exist or is
unavailable"
or something about a bad file name.
Although Publish Date can be found under Insert > Quick Parts > Document Property it isn't actually a document property. It is a "built-in" CustomXML part, a node of CoverPageProperties, and can be addressed in VBA using the CustomXMLParts collection.
The CustomXML part is only added to the document once the mapped content control is inserted.
Below is the code I use.
As already pointed out for document properties the document must be open.
Public Sub WriteCoverPageProp(ByVal strNodeName As String, ByVal strValue As String, _
Optional ByRef docTarget As Document = Nothing)
'* Nodes: Abstract, CompanyAddress, CompanyEmail, CompanyFax, CompanyPhone, PublishDate
'* NOTE: If writing PublishDate set the content control to store just the date (default is date and time).
'* The date is stored in the xml as YYYY-MM-DD so must be written in this format.
'* The content control setting will determine how the date is displayed.
Dim cxpTarget As CustomXMLPart
Dim cxnTarget As CustomXMLNode
Dim strNamespace As String
If docTarget Is Nothing Then Set docTarget = ActiveDocument
strNodeName = "/ns0:CoverPageProperties[1]/ns0:" & strNodeName
strNamespace = "http://schemas.microsoft.com/office/2006/coverPageProps"
Set cxpTarget = docTarget.CustomXMLParts.SelectByNamespace(strNamespace).item(1)
Set cxnTarget = cxpTarget.SelectSingleNode(strNodeName)
cxnTarget.Text = strValue
Set cxnTarget = Nothing
Set cxpTarget = Nothing
End Sub
You cannot modify a document without opening it. In any event, "Publish Date" is not a Built-in Document Property; if it exists, it's a custom one.
Contrary to what you've been told, not all BuiltinDocumentProperties are read-only; some, like wdPropertyAuthor ("Author"), are read-write.
There are three main ways you could modify a Word document or "traditional" property (which are the ones you can access via .BuiltInDocumentProperties and .CustomProperties):
a. via the Object Model (as you are currently trying to do)
b. for a .docx, either unzipping the .docx, modifying the relevant XML part, and re-zipping the .docx.
c. For "traditional" properties, i.e. the things that you can access via .BuiltInDocumentProperties and .CustomDocumentProperties, in theory you can use a Microsoft .dll called dsofile.dll. But it hasn't been supported for a long time, won't work on Mac Word and the Microsoft download won't work on 64-bit Word. You'd also have to distribute and support it.
But in any case, "Publish Date" is not a traditional built-in property. It's probably, but not necessarily, a newer type of property called a "Cover Page Property". Those properties are in fact pretty much as "built-in" as the traditional properties but cannot be accessed via .BuiltInDocumentProperties.
To modify Cover Page properties, you can either use the object model or method (b) to access the Custom XML Part in which their data is stored. Method (c) is no help there.
Not sure where your error 427 is coming from, but I would guess from what you say that you are trying to see if you can modify the property in a single line, using the fullname of the document in an attempt to get Word to open it. No, you can't do that - you have to use GetObject/CreateObject/New to make a reference to an instance of Word (let's call it "wapp"), then (say)
Dim wdoc As Word.Document ' or As Object
Set wdoc = wapp.Documents.Open("the fullname of the document")
Then you can access its properties, e.g. for the read/write Title property you can do
wdoc.BuiltInDocumentProperties("Title") = "your new title"
wdoc.Save
If Publish Date is the Cover Page Property, once you have a reference to the Word Application and have ensured the document is open you can use code along the following lines:
Sub modPublishDate(theDoc As Word.Document, theDate As String)
' You need to format theDate - by default, Word expects an xsd:dateTime,
' e.g. 2020-06-11T00:00:00 if you only care about the date.
Const CPPUri As String = "http://schemas.microsoft.com/office/2006/coverPageProps"
Dim cxn As Office.CustomXMLNode
Dim cxps As Office.CustomXMLParts
Dim nsprefix As String
Set cxps = theDoc.CustomXMLParts.SelectByNamespace(CPPUri)
If cxps.Count > 0 Then
With cxps(1)
nsprefix = .NamespaceManager.LookupPrefix(CPPUri)
Set cxn = .SelectSingleNode(nsprefix & ":CoverPageProperties[1]/" & nsprefix & ":PublishDate[1]") '/PublishDate[1]")
If Not (cxn Is Nothing) Then
cxn.Text = theDate
Set cxn = Nothing
End If
End With
End If
Set cxps = Nothing
As for this, "Will word.application.documents accept the document name as a string", the answer is "yes", but Word has to have opened the document already. as mentioned above. Word can also accept an integer index into the .Documents collection and may accept just the name part of the FullName string.
Finally, if you do end up using a "traditional Custom Document Property", even after you have set the property and saved the document (approximately as above) you may find that the new property value has not actually saved! If so, that's down to an old error in Word where it won't save unless you have actually visited the Custom Document Property Dialog or have modified the document content in some way, e.g. adding a space at the end.
Trying to get data from Excel and merge it into Word using MailMerge (just like how it is done in this video).
However, fields aren't getting updated after running this code. VBA isn't throwing any error so looks like code is fine. Can you please help?
Sub getdata()
Dim numRecord As Integer
Dim myName As String
myName = InputBox("Enter the field name and relax!")
Set dsMain = ActiveDocument.MailMerge.DataSource
If dsMain.FindRecord(FindText:=myName, Field:="Fields") = True Then
numRecord = dsMain.ActiveRecord
End If
End Sub
Note: Data in Excel looks like this:
Fields First Layer Second Layer
CC 5 3
So when someone enters CC in Input box I want first_layer and Second_layer fields in word to get updated to 5 and 3 respectiely.
If you're running the mailmerge from Word, you don't actually need any VBA for this - it can all be done with a SKIPIF field. For example the following field code does the same as the macro in the video is supposed to:
{SKIPIF{FILLIN "Name to merge" \o}<> {MERGEFIELD Name}}
or:
{SKIPIF{FILLIN "Name to merge" \o}<> «Name»}
Note: The field brace pairs (i.e. '{ }') for the above example are all created in the document itself, via Ctrl-F9 (Cmd-F9 on a Mac or, if you’re using a laptop, you might need to use Ctrl-Fn-F9); you can't simply type them or copy & paste them from this message. Nor is it practical to add them via any of the standard Word dialogues. Likewise, the chevrons (i.e. '« »') are part of the actual mergefields - which you can insert from the 'Insert Merge Field' dropdown (i.e. you can't type or copy & paste them from this message, either). The spaces represented in the field constructions are all required.
I am using an Excel macro where I am inserting the same text to several bookmarks in a Word doc. How can I do this by specifying the insert command once and apply it to all the bookmark locations?
Now I am doing the following for all bookmarks?
Dim monYear As String
monYear = Format(DateAdd("m", -1, Now), "mmmm yyyy")
wdApp.Selection.GoTo what:=-1, Name:="Front_Page_Month_Year"
wdApp.Selection.TypeText monYear
wdApp.Selection.GoTo what:=-1, Name:="Page2_Month_Year"
wdApp.Selection.TypeText monYear
And on and on....
There's no way to use a single command to write to multiple bookmark locations. What you can do, however, is reference one bookmark location in order to display that bookmark's content in multiple locations.
Create the bookmark, then insert a cross-reference to it in each location where you want to display the bookmark contents. (Or create one cross-reference, then copy/paste to the other locations.)
A tip for your code: the approach you're using with the Selection object is not optimal. As in Excel, it's better to work with the object model, rather than rely on a Selection. Thus:
Dim doc as Word.Document
Set doc = wdApp.ActiveDocument 'Note: if you're opening a document, set in the Open method
doc.Bookmarks("Front_Page_Month_Year").Range.Text = monYear
'When you're done, update the REF fields (references)
doc.Fields.Update
I am currently working on a Outlook 2010 VBA macro to pull information from a email messages and place it into an Excel file. The idea is that each email has the same fields in tables embedded in the email message every time (name, order number, date, etc.) and that data is put into a spreadsheet. To do this, I have currently used the following code to get the table and move it into Excel:
'This code is inside a for each loop for each message
Set excelWorksheet2 = excelWorkbook.Worksheets.item(2)
Set excelWorksheet3 = excelWorkbook.Worksheets.item(3)
Set excelWorksheet4 = excelWorkbook.Worksheets.Add(After:=excelWorkbook.Sheets(excelWorkbook.Sheets.count))
Dim mailObj As Outlook.MailItem
Dim doc As Word.Document
Set doc = mailObj.GetInspector.WordEditor
Dim table1, table2, table3 As Object
Set table3 = doc.Tables(4).Range
Set table2 = doc.Tables(3).Range
Set table1 = doc.Tables(2).Range
table1.Copy
excelWorksheet2.Paste
table2.Copy
excelWorksheet4.Paste
table3.Copy
excelWorksheet3.Paste
Set table1 = Nothing
Set table2 = Nothing
Set table3 = Nothing
'I do much more of this to get the data from each table and put it into a master worksheet...
excelWorksheet.Cells(rows, cols + 1).Value = excelWorksheet2.Cells(4, 2).Value 'Contract Number
excelWorksheet.Cells(rows, cols + 2).Value = excelWorksheet2.Cells(4, 4).Value 'Contractor Name
Set doc = Nothing
Set excelWorksheet2 = Nothing
Set excelWorksheet3 = Nothing
Set excelWorksheet4 = Nothing
I get the following errors every so often, but it doesn't occur on the same data, it is sort of random and seems to occur on the Outlook/email side only:
"The requested member of the collection does not exist." (Error code
5941) at the .Range line
"Method 'Copy' of object 'Range' failed" at the .Copy
line
Sometimes both of these errors occur if I step through, if the copy fails, the macro will crash.
I have tried:
Putting in 2 second delays
Go through fewer emails (this code usually fails when I select > 10
emails to process)
Clearing the clipboard after every email
Close/deallocate objects through Nothing (not sure if
this is the best practice as I'm more of a C/C++/C#/Java guy)
None of these seemed to remotely fix this issue as both errors pop up frequently, but intermittently.
I'm truly at a loss as to what the next step would be in debugging this issue, any help would be much appreciated!
Based on research I have been doing on the issue of the WordEditor tables, it seems as the amount of copy/paste operations and the searching HTML tables just simply do not allow for reliable behavior. One possible solution to this could be to copy the entire email into Excel and parse the tables that way (this still requires copy/paste).
What I did to solve this problem (indirectly) is to save all the emails as HTML files (through Outlook.Application running in Excel: email.SaveAs folderPath + fileName + ".html", olHTML) in a temporary directory and have Excel open the HTML files in a workbook and work with the data that way. This has been much more reliable (added overhead though) and allows for large volumes of emails to be exported to Excel properly.
If anyone wants the exact code to my problem, message me (vindansam at hotmail.com, it's a tad long and has some proprietary information in it).
It's too bad that the WordEditor seems to not handle the information well, maybe Microsoft will beef things up in the next version of Office.