Inserting into Multiple Bookmark Locations - Excel VBA - excel

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

Related

Merging Word docs with Excel as variable data

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

Is there an equivalent to SCHEMA.INI for reading Excel Workbooks

I am currently working on a project that will import data from multiple different sources in a variety of formats and structures - e.g., CSV, fixed-length, other-delimited (tab, pipe, etc.) plain-text, and Excel worksheets/workbooks. For this, I'm attempting to build "generic" readers for these files which will throw the files' contents into a DataTable/DataSet I can use in other methods. The plain-text files are pretty simple as I've created a large SCHEMA.INI file which contains field definitions for each of the files the system will handle. That SCHEMA.INI resides in a "processing folder" where the files are temporarily stored until their data has been integrated with other systems. A defined text files' data can be easily extracted using this method:
Private Function TextFileToDataTable(ByVal TextFile As IO.FileInfo) As DataTable
Dim TextFileData As New DataTable("TextFileData")
Using TapeFileConnect As New OleDb.OleDbConnection("Provider=Microsoft.Jet.OleDb.4.0;Data Source='" + TextFile.DirectoryName + "';Extended Properties='Text';")
Using TapeAdapter As New OleDb.OleDbDataAdapter(String.Format("SELECT * FROM {0};", TextFile.Name), TapeFileConnect)
Try
TapeAdapter.Fill(TextFileData)
Catch ex As Exception
TextFileData = Nothing
End Try
End Using
End Using
Return TextFileData
End Function
This works well because a plain-text file isn't terribly complex in its data structure. A single file generally (at least for my requirements) contains, at most, one single table's worth of data - unless, of course, it's some sort of complex XML or JSON structure file, which can/should be handled completely differently anyway - so there's no need to go iterating through different elements beyond this.
NOTE: The code above is dependent on the SCHEMA.INI file being present in the same directory as the plain-text file being read and there being a section within that SCHEMA.INI defined with the same name as that plain-text file.
EXAMPLE:
[EXAMPLE_TEXT_FILE.TXT]
CharacterSet=ANSI
Format=FixedLength
ColNameHeader=FALSE
DateTimeFormat="YYYYMMDD"
COL1=CUSTOMER_NUMBER TEXT WIDTH 20
COL2=CUSTOMER_FIRSTNAME TEXT WIDTH 30
COL3=CUSTOMER_LASTNAME TEXT WIDTH 40
COL4=CUSTOMER_ADDR1 TEXT WIDTH 40
COL5=CUSTOMER_ADDR2 TEXT WIDTH 40
COL6=CUSTOMER_ADDR3 TEXT WIDTH 40
...
Excel workbooks, however, can be a bit trickier. Several of the workbooks I have to process contain multiple worksheets worth of data that I want to consolidate into a single DataSet with a DataTable for each worksheet. The basic functionality is, again, fairly straightforward and I've come up with the following method to read any and all sheets into a DataSet:
Private Function ExcelFileToDataSet(ByVal ExcelFile As IO.FileInfo, ByVal HasHeaderRow As Boolean) As DataSet
Dim ExcelFileData As New DataSet("ExcelFileData")
Dim ExcelConnectionString As String = String.Empty
Dim UseHeaders As String = "NO"
Select Case ExcelFile.Extension.ToUpper.Trim
Case ".XLS"
ExcelConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Extended Properties='Excel 8.0;HDR={1}'"
Case ".XLSX"
ExcelConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties='Excel 8.0;HDR={1}'"
End Select
If HasHeaderRow Then
UseHeaders = "YES"
End If
ExcelConnectionString = String.Format(ExcelConnectionString, ExcelFile.FullName, UseHeaders)
Try
Using ExcelConnection As New OleDb.OleDbConnection(ExcelConnectionString)
Dim ExcelSchema As New DataTable
ExcelConnection.Open()
ExcelSchema = ExcelConnection.GetOleDbSchemaTable(OleDb.OleDbSchemaGuid.Tables, Nothing)
For Each ExcelSheet As DataRow In ExcelSchema.Rows
Dim SheetTable As New DataTable
Using ExcelAdapter As New OleDb.OleDbDataAdapter
Dim SheetName As String = ExcelSheet("TABLE_NAME").ToString
Dim ExcelCommand As New OleDb.OleDbCommand
SheetTable.TableName = SheetName.Substring(0, SheetName.Length - 1)
ExcelCommand.Connection = ExcelConnection
ExcelCommand.CommandText = String.Format("SELECT * FROM [{0}]", SheetName)
ExcelAdapter.SelectCommand = ExcelCommand
ExcelAdapter.Fill(SheetTable)
End Using
ExcelFileData.Tables.Add(SheetTable)
Next ExcelSheet
End Using
Catch ex As Exception
ExcelFileData = Nothing
End Try
Return ExcelFileData
End Function
The above code will work in a majority of the cases I deal with, but my "difficulty" is that there may be some worksheets that have header rows and some that don't within the same workbook. Also, for those worksheets that do not have a header row, I'd like to be able to define the field names and data types similar to how I can with the plain-text SCHEMA.INI. The only thing I have going for me in these cases is that the "client" provides me with a data map to help me identify what data elements are in each field.
What I'd like to know is if there is a way similar to the text file's SCHEMA.INI to define the structure of an Excel workbook and the worksheet(s) it contains - including column data types to avoid the OleDb driver from "misinterpreting" a column's data - ahead of time. I imagine this could be any sort of structured file such as INI, XML, or whatever, but it would need to be capable of identifying whether or not a particular sheet contains a header row or, in lieu of such a row, the (expected) column definitions. Does any such "standard definition" file exist for Excel workbooks?
One thing to note: As you may have noticed in the code for the ExcelFileToDataSet() method, I may be dealing with the older .XLS (97-03) format or the .XLSX (07+) format, so I can't necessarily rely on the workbook being Open XML compliant. I suppose I could try breaking the methods out to one for each extension, but I'd rather find something that I can use regardless of which file format the Excel file is using.

Change built-in Document properties without opening

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.

Insert a line break from Excel VBA in a Word document

From my Excel Sheet I open up a Word document. Excel generates some text within the document. Between the text I want a LineBreak.
The code results in an error message stating: parameter value out of acceptable range
Using the following documentation this should be possible using InsertBreak.
Sub InsertLineBreak()
Set wrd = CreateObject("Word.Application") 'Open Word
Set objDoc = wrd.Documents.Add 'Add new document
Set objSelection = wrd.Selection 'Select this document
objSelection.InsertBreak Type:=wdLineBreak 'Insert Break
End Sub
It works perfectly fine for me, are you sure you have added correct reference?
Try objSelection.InsertBreak Type:=6 or add Microsoft Word 16.0 Object Library reference, or declare a globar variable wdLineBreak
For future readers:
In this case the Microsoft Word 16.0 Object Library was missing.
It can be added Tools-> References and selecting the corresponding Library

Insert cell content to word bookmarks doesnt delete the bookmark signs

I have a Worddocument with bookmarks. From Excel I write cell content to the places where I set the bookmarks.
My problem: You can still see the bookmarks.
What I tried:
First I used a placehoder bookmark with
.item("Name1").Range.InsertAfter Rep.NName1
Second I used an enclosing bookmark with
.item("Name1").Range.InsertAfter Rep.NName1
and
.item("Name1").Range.InsertBefore Rep.NName1
I still cannot get rid of the bookmarks.
All I could do is using the sledgehammer approach and delete them but I think there should be a way to replace them during the insert.
Source
If you want to overwrite the bookmark (ie replace any text contained within the bookmark and delete the bookmark itself), you can just set the Text property of the Bookmark's range:
.Item("Bookmark1").Range.Text = "Some new text"
If you want to replace the content of the existing bookmark but identify the new text with the bookmark, you'll need to replace the text then mark the new text as the bookmark:
Dim bmRange As Range
Set bmRange = .Item("Bookmark2").Range
bmRange.Text = "Some new text"
.Add Name:="Bookmark2", Range:=bmRange

Resources