VBA Table in Merged Letter - vba

I have an Excel Sheet with a lot of customer Data. All customers have common data (address, name etc.) that I implemented as simple mergefields. Some Customers have multiple Datasets that should be added as a Table at the end of the merged letter.
To find the Data from my excel Sheet I already came up with the following code.
noInt is the number of customers while noData is the number of different datasets (all customers together, some multiples). exWb is the excel workbook my data comes from and the data I want to display in the table lays in columns 5 to 9.
For i = 2 To noInt
For k = 2 To noData
If exWb.Sheets("Table1").Cells(k, 1) = exWb.Sheets("Table2").Cells(i, 1) Then
For j = 5 To 9
Insert into Table exWb.Sheets("Table1").Cells(k, j)
Next j
End If
Next k
Next i
Now my questions:
How can I insert this data into a newly created table after the placeholder "insert_table_here"?
How can I make sure that for every letterin the mail merge series there is only the data of the customer the letter is about included in this table?
To find a solution to this, I already thought about if there was maybe a function that gives the current "Mail Merge Number". In that case I could compare the field (MailMergeNumber, 1) with (k,1) to only show the results that include the current customer.
Example to make it more understandable:
Dear Mr A,
...
Table of items Mr. A bought
-End of document-
Dear Mr. B,
...
Table of items Mr. B bought
-End of document-
And so on...

If you're creating Word documents from a template (that's generally the easiest way I've found to do it), you can add a table to the template document with the header rows you need, and 1 blank row for the data. Then, after populating the basic mergefields, you could loop through the current customer fields, adding new rows to the Word table as you went. Something like this:
Dim exWs as Excel.Worksheet
Dim CurrentCustomerFirstCell as Excel.Range
Dim CurrentCustomerActiveCell as Excel.Range
Dim EmpRowOffset as integer
Dim wdDoc as Word.Document
Dim wdTable as Word.Table, wdCell as Word.Cell
' set up your existing references, including (I assume) to the Word document you're updating
set exWs = exWb.Sheets("Table1")
' initialize row for current employee
CurrentCustomerFirstCell = exWs.Cells(2,1)
do while CurrentCustomerFirstCell.Row <= noData ' consider renaming noData to somthing like "numberOfRows"
' populate basic mergefields
wdDoc.Fields(1).Result.Text = CurrentCustomerFirstCell.Value
' etc.
' populate table in Word document
set wdTable = wdDoc.Tables(1)
EmpRowOffset = 0
set CurrentCustomerActiveCell = CurrentCustomerFirstCell.Offset(Rowoffset:=EmpRowOffset)
set wdTable = wdDoc.Tables(1)
do while CurrentCustomerActiveCell.Value = CurrentCustomerFirstCell.Value
' this code would update the first "data" row in the existing Word table
' to the 6th column of the active employee row
set wdCell = wdTable.Cell(Row:=2 + EmpRowOffset, Column:=1)
wdCell.Range.Text = _
CurrentCustomerActiveCell.Offset(columnoffset:=5).Value
wdTable.Rows.Add
EmpRowOffset = EmpRowOffset + 1
set CurrentCustomerActiveCell = CurrentCustomerFirstCell.Offset(RowOffset:=EmpRowOffset)
Loop
' now that we're finished processing the employee, update CurrentCustomerFirstCell
set CurrentCustomerFirstCell = CurrentCustomerActiveCell
loop

You can use Word's Catalogue/Directory Mailmerge facility for this (the terminology depends on the Word version). To see how to do so with any mailmerge data source supported by Word, check out my Microsoft Word Catalogue/Directory Mailmerge Tutorial at:
http://www.msofficeforums.com/mail-merge/38721-microsoft-word-catalogue-directory-mailmerge-tutorial.html
or:
http://www.gmayor.com/Zips/Catalogue%20Mailmerge.zip
The tutorial covers everything from list creation to the insertion & calculation of values in multi-record tables in letters. Do read the tutorial before trying to use the mailmerge document included with it.
Depending on what you're trying to achieve, the field coding for this can be complex. However, since the tutorial document includes working field codes for all of its examples, most of the hard work has already been done for you - you should be able to do little more than copy/paste the relevant field codes into your own mailmerge main document, substitute/insert your own field names and adjust the formatting to get the results you desire. For some worked examples, see the attachments to the posts at:
http://www.msofficeforums.com/mail-merge/9180-mail-merge-duplicate-names-but-different-dollar.html#post23345
http://www.msofficeforums.com/mail-merge/11436-access-word-creating-list-multiple-records.html#post30327
Another option would be to use a DATABASE field in a normal ‘letter’ mailmerge main document and a macro to drive the process. An outline of this approach can be found at:
http://answers.microsoft.com/en-us/office/forum/office_2010-word/many-to-one-email-merge-using-tables/8bce1798-fbe8-41f9-a121-1996c14dca5d
Conversely, if you're using a relational database or, Excel workbook with a separate table with just a single instance of each of the grouping criteria, a DATABASE field in a normal ‘letter’ mailmerge main document could be used without the need for a macro. An outline of this approach can be found at:
https://answers.microsoft.com/en-us/msoffice/forum/msoffice_word-mso_winother-mso_2010/mail-merge-to-a-word-table-on-a-single-page/4edb4654-27e0-47d2-bd5f-8642e46fa103
For a working example, see:
http://www.msofficeforums.com/mail-merge/37844-mail-merge-using-one-excel-file-multiple.html
Alternatively, you may want to try one of the Many-to-One Mail Merge add-ins, from:
Graham Mayor at http://www.gmayor.com/ManyToOne.htm; or
Doug Robbins at https://onedrive.live.com/?cid=5AEDCB43615E886B&id=5AEDCB43615E886B!566

Related

Excel VBA: Create a stacked column chart with named table

I'm trying to use VBA to create a stacked column graph using 3 columns of data out of a large ~30 column named table in excel. The desired outcome would be a stacked column graph, where the columns are based on the column "Program" (there are 5 distinct values across ~200 rows) and the numbers that make up the columns are the "SAVINGS - USE THIS" with the corresponding "Project Number"s as those chunks labels. Each row is a distinct project.
For instance, if I have 5 projects in "Program 1," I would want the "SAVINGS - USE THIS" values stacked on top of each other and when a run my mouse over the portions of the column the Project Number would show.
I am fairly new to VBA and am currently editing my previous code to make the project numbers and their savings into a pie graph (originally I didn't care about the program), so if there is a better way to do any of this please let me know.
Sub CreateChart()
Dim labelRng As Range
Dim dataRng As Range
Dim progRng As Range
Dim chtRng As Range
Dim cht As Object
Dim mySeries As Series
Dim vntValues As Variant
Dim i As Integer
Set chtRng = Union(Sheets("CMF").ListObjects("CMF").ListColumns("Program").Range, _
Sheets("CMF").ListObjects("CMF").ListColumns("Project Number").Range, _
Sheets("CMF").ListObjects("CMF").ListColumns("SAVINGS - USE THIS").Range)
'Set progRng = Sheets("CMF").ListObjects("CMF").ListColumns("Program").Range
'Set labelRng = Sheets("CMF").ListObjects("CMF").ListColumns("Project Number").Range
'Set dataRng = Sheets("CMF").ListObjects("CMF").ListColumns("SAVINGS - USE THIS").Range
'Set chtRng = Union(progRng, dataRng, labelRng) 'Sets range for pie
Set cht = Sheets("CMF").Shapes.AddChart2 'Creates chart
For j = cht.Chart.SeriesCollection.Count To 1 Step -1 'Had to be added to avoid errors
cht.Chart.SeriesCollection(j).Delete
Next j
cht.Chart.SetSourceData chtRng 'Sets data range for chart
cht.Chart.ChartType = xlColumnStacked
If this is the best way to do this, my current issue is defining the data range for the graph. You can see I am trying a couple different things, but the problem I'm having is that instead of pulling the "Program" column it is pulling the column that is next to it.
For reference, Project Number is in Column A, Program is in Column C and SAVINGS is in column X. It is pulling Columns A, B and X. Even if I specify the column number as "3" or pull them in a different order, I always have the same issue. The only way I don't have the issue is if I stop pulling in the Project Number and just pull in Program and Savings, which it gets right.
What am I doing that is causing it to pull back the wrong column of data, and once I get the right data in how can I make the stacked columns be organized by Program?
Thanks for reading all of that!
Since you are just assigning SetSourceData at the end, excel is going to do what it thinks is best...which isn't what you want in this case.
Try creating the different Series one at a time and assigning the ranges for each individually after you delete them. Just use the Macro Recorder to go over all the steps that you need to do to create the chart from scratch.

Programmatically Update Linked Named Range of excel object in MS Word (2007)

First question, excuse me if this has already been solved, but I've searched thoroughly and cannot find an answer:
I have linked several named ranges into a word document. This word doc (and the related excel workbook with named ranges) is a template: it's for a coworker who will make many copies of these templates (of both the word doc and the excel workbook).
I would like to include a command button in the word doc that, when clicked, will update the sources for the linked named ranges. Specifically, I want it to set the workbook with the same name as the worddoc, as the source.
The issue is that it does not like the named range I have entered. I get the:
Run-time error '6083': Objects in this document contain links to files that cannot be found. The linked information will not be updated.`
However, I have quadrupled-checked my excel doc, the named range exists. AND, when I hit Alt+F9 in word, I clearly see the link contains the named range!
{LINK Excel.Sheet.8 C:\Users\Marc\Documents\WIP_SSS.xlsm CED \a \p}
Here is my code:
Public Sub ChangeSource()
Dim filename As Variant
Dim fieldcount As Integer
Dim x As Integer
filename = Left(Application.ActiveDocument.Name, Len(Application.ActiveDocument.Name) - 4) & "xlsm"
fieldcount = ActiveDocument.Fields.Count
For x = 1 To fieldcount
'Debug.Print ActiveDocument.Fields(x).Type
If ActiveDocument.Fields(x).Type = 56 Then
ActiveDocument.Fields(x).LinkFormat.SourceFullName = ActiveDocument.Path & "\" & _
filename & "!CED"
End If
Next x
End Sub
If I don't enter the named range at all, the macro works, but it embeds the entire excel worksheet (which I do not want it to do). Any ideas on how/ why it is not liking the named range?
Thanks,
Marc
UPDATE:
With help from Bibadia, I found a solution; in addition, I want to document some strange behavior exhibited by Word VBA:
First off, the solution code:
Public Sub ChangeSource()
Dim filename As Variant
Dim fieldcount As Integer
Dim x As Integer
filename = ThisDocument.Path & "\" & Left(Application.ActiveDocument.Name, Len(Application.ActiveDocument.Name) - 4) & "xlsm"
fieldcount = ActiveDocument.Fields.Count
For x = 1 To fieldcount
On Error Resume Next
If ActiveDocument.Fields(x).Type = 56 Then
ActiveDocument.Fields(x).Delete
End If
Next x
ActiveDocument.Bookmarks("R1").Range.InlineShapes.AddOLEObject filename:=filename & "!Range1", LinkToFile:=True
End Sub
I first deleted all type 56 fields (linked object, or more technically, "wdfieldlinked"). Then, I added OLEObjects at pre-set bookmark locations.
Interestingly, just as Bibadia noted, the key was to input the LinkToFile:=True code. It seems Word will not accept the object if it is embedded: if I remove that line, I get the error Word Cannot obtain the data for the C:\...\document!NamedRange link.
Finally, I found one other odd behavior: When trying to simply replace the link, using this code,
ActiveDocument.Fields(1).LinkFormat.SourceFullName = filepath+name & _
"!CED" 'that is the named range
it would work once, when I changed both the word document's and the excel workbook's filenames (see original message for context). So, when the new filepath+name DID NOT match the existing filepath+name, Word VBA accepted the change. However, once initially updated, if I tried to run the macro again, I would get:
run-time error '6083': Objects in this document contain links to files that cannot be found. The linked information will not be updated.
I would get this error even if I changed the named range to another named range in the same worksheet (and obviously same workbook). So it appears that Word VBA does not like "updating" filepath+name when the filepath+name does not change.
Just so anyone who didn't know (like me) now knows. Sorry for the long update, I just wanted to be thorough.
I am not completely sure of this, but it is a little too long for a comment.
As far as I know, you can only set LinkFormat.FullSourceName to the name of a file, not a fullname + subset name, which is what you are trying to do when appending the "!CED". Although you can read the subset name (CED) from OleFormat.Label, you can't modify it as it's a read-only property.
So if you actually need to modify the subset name (CED), AFAICS the only way to do it is to delete and reinsert the LINK field. If you reinsert using Fields.Add, you just specify the text of the field, so you can get the file name and Subset name right. What is slightly confusing is that if you insert a LINK using InlineShapes.AddOleObject, you can specify fullname+subset name in exactly the way that you are trying to do in your code.
However, I do not think you are trying to modify the Subset name. So let's assume that you already have a LINK field along the lines of
{ LINK Excel.SheetMacroEnabled.12 "the full pathname of a .xlsm file" CED \a f 0 \p }
Word will only be able to update that link if the path+filename is valid (i.e. there's a .xlsm at that location, the workbook has a Range Name called CED, and the Range Name is in the first Sheet. Otherwise, you have to specify a Sheet name as well, e.g.
{ LINK Excel.SheetMacroEnabled.12 "the full pathname of a .xlsm file" Sheet2!CED \a f 0 \p }
It's just a guess, but if your code is trying to connect to a Workbook where the range defined by CED is not in the first sheet, you would see the error you describe.
Further, the scope of the CED Range Name has to be either "workbook" or the name of the first sheet. Otherwise, if the scope is the first sheet but the range is actually in another sheet, or vice versa, I do not think Word can make the connection whatever subset name you provide (my guess is that Word never really caught up with Excel after Excel introduced multi-sheet workbooks).
If CED can reference sheets other than the first one, I think you will probably have to use the Excel object model to discover which sheet its Range is in, construct the appropriate Subset name, and delete/re-insert the LINK field.

Copy certain table row from one table to another

I have a word document with several tables.
The first table in the document is supposed to be a summary table containing copies of certain rows of the other tables in the document.
How would I have to tackle the automated population of the summary table with the specific rows of the other tables?
I assume there is no built in function and I'd have to use VBA.
I'd think I have to mark the rows to be copied with some marker (e.g.: "###") for the code to detect. Then the code would have to go through the document, find all the "###", mark them as "dealt with" (e.g., with "##-") and copy/append the respective row into the first table, and then loop through the document again.
You can mark the row(s) using bookmarks; name them with a special prefix. Loop through the bookmarks collection, check the prefix and if it's the correct one, append the row to the sumamry table. You can delete the bookmark in the process, if you wish.
Here's some sample code which works for me in a quick test. The boomkark prefix is "tbl"; if you don't want it to be visible to the user, start the name with an underscore (_).
The target range for the summary table is at the start of the document. Change the assignment to the rng object if you need it elsewhere.
Dim doc As word.Document
Dim rng As word.Range
Dim rwSource As word.Row
Dim bkm As word.Bookmark
Set doc = ActiveDocument
Set rng = doc.Range(0, 0)
For Each bkm In doc.Bookmarks
If Left(bkm.Name, 3) = "Tbl" Then
rng.FormattedText = bkm.Range.FormattedText
rng.Collapse wdCollapseEnd
End If
Next
Note: I tried using REF fields to pick up the bookmarked rows, without needing VBA. This inserted an empty row between each REF field, however, so I don't think it can be done without VBA.

Check if unique identifier is present in "new data", if not, add entries to "historical data" sheet?

I've got data with a unique ID number in one column. This is pasted into a "Raw Data" sheet by the user.
Then, I've got macros that manipulate the "Raw Data", including archiving some of it on a "archival" worksheet which includes the ID numbers.
I want to run a check to see if there are any "new" ID numbers in the "Raw Data", and if so add a new row with some of the data including the ID number to the "archival" sheet.
I've googled and checked here. It looks like I want to use a Collection? Never encountered this so far, not sure where to start.
Sorry that this isn't the most well structured question, and that it doesn't include any code. Not sure how to get started.
It should be noted that this reconciliation only needs to go one way -- I don't need to delete "old" unique IDs from the "archival" sheet.
Thanks!
If you specifically want to use collections, over Range.Find, you can do so like this.
Although handling errors this way in VBA is best to avoid most times, for iterating over collections, it can result in faster and often less verbose code.
I have used named ranges for a Rawdata and Archive worksheet tabs, so you will need to adapt this to your own situation. What this does is use collections to store unique items already in Archive, and compare these against items in Rawdata - when a new (unique) item is found in Rawdata that is not already in Archive, it is added to the sheet (and collection).
Sub IDcollection()
Dim IDcoll As New Collection
Dim cells, Rng, Rng_a As Range
Set Rng = Worksheets("Rawdata").Range("IDRANGE")
Set Rng_a = Worksheets("Archive").Range("IDRANGE_A")
'IDRANGE looks like string IDs 'abc', 'ab1',etc
'get unique IDs already in ARCH sheet via named range
On Error Resume Next 'includes only unique items
For Each cells In Rng_a.cells
IDcoll.Add cells.Text, cells.Text
'use the IDs as the KEY in the VBA Collection
Next cells
'check for unique items not in Archive, but in Rawdata (i.e. new items)
On Error Resume Next
For Each cells In Rng
IDcoll.Item cells.Text
If Err.Number <> 0 Then
IDcoll.Add cells.Text, cells.Text
LastRow = Rng_a.Rows(Rows.Count).End(xlUp).Row + 1
Rng_a(LastRow).Value = cells.Text
Err.Clear
End If
Next
End Sub
Just stick this in a module in your VBA for your worksheet, set up some named ranges as above, populated with dummy data.

Take results from one sheet and move them into many other sheets

I have looked at similar answers to this question, but whatever I do I cannot get them to do what I need.
I have a daily email which has a CSV file giving call stats for our Sales team for the previous day. What I need is to put them into Excel to give trending and historical call activity for the year. Without VBA or Macros this is a very time consuming process.
The stats it gives are number of calls, and average call length (that are of any importance) I have already got VBA to calculate the total outgoing with this:
Dim Call_Number As Integer
Dim Call_Time As Date
Dim Call_Total As Date
Call_Number = .Cells(2, 6).Value
Call_Time = .Cells(2, 7).Value
Call_Total = Call_Number * Call_Time
.Cells(12, 7).Value = Call_Total
So what I need is to take the 3 cells for each sales member, and move them into the right place in their relative sheets, which are separated by name. I also need it to move into the next cell to the right if the destination cell is full, so I'm thinking I need to start the pasting process as Jan 1st and keep moving to the right until it finds blank cells. Is there a way this can be done either in a button or automatically?
I have the first sheet used as the data import sheet, where we just import the data into csv, and because its standard formatting, every day it will give it all in the right formatting.
Code I have so far. It doesn't error, but doesn't do anything:
Sub Move_Data()
Dim Dean As Worksheet
Dim Chris As Worksheet
Dim Paul As Worksheet
Dim Nigel As Worksheet
Dim Calc As Worksheet
Dim Lastrow As Long
Dim J As Long
Dim i As Long
Set Dean = ThisWorkbook.Worksheets("DEAN 822")
Set Chris = ThisWorkbook.Worksheets("CHRIS 829")
Set Paul = ThisWorkbook.Worksheets("PAULP 830")
Set Nigel = ThisWorkbook.Worksheets("NIGEL 833")
Set RUSSELL = ThisWorkbook.Worksheets("RUSSELL 835")
Set Calc = ThisWorkbook.Worksheets("Calculation Sheet")
Lastrow = Range("C" & Dean.Columns.Count).End(xlToRight).Column
J = 2
For i = 0 To Lastrow
Set Rng = Dean.Range("C5").Offset(i, 0)
If Not (IsNull(Rng) Or IsEmpty(Rng)) Then
Calc.Cells(2, 4).Copy
Dean.Range("c" & J).PasteSpecial xlPasteValues
J = J + 1
End If
Next i
Application.CutCopyMode = False
End Sub
Instead of
Lastrow = Range("C" & Dean.Columns.Count).End(xlToRight).Column
I think you want
Lastrow = Range("C" & Dean.Columns.Count).End(xlUp).Row
"I also need ... in a button or automatically?"
LastCol = WshtName.Cells(CrntRow, Columns.Count).End(xlToLeft).Column
will set LastCol to the last used column in row CrntRow.
J = 2
For i = 0 To Lastrow
Set Rng = Dean.Range("C5").Offset(i, 0)
If Not (IsNull(Rng) Or IsEmpty(Rng)) Then
Calc.Cells(2, 4).Copy
Dean.Range("c" & J).PasteSpecial xlPasteValues
J = J + 1
End If
Next i
Application.CutCopyMode = False
I am not sure what this code is attempting.
It sets Rng to C5, C6, C7, C8, ... to Cn where n is Lastrow+5. If C5, for example, if empty it copies C2 to `Calc.Cells(2, 4).
Did you mean to copy column C from worksheet Dean to column B of worksheet Calc?
If the removal of empty cells is not important then this will be faster and clearer:
Set Rng = Dean.Range(.Cells(5 ,"C"), .Cells(Lastrow ,"C"))
Rng.Copy Destination:=Calc.Cells(2, 4)
New information in response to comment
I cannot visualise either your source data or your destination data from your description so cannot give any specific advice.
Welcome to Stack Overflow. I believe this is a good place to find previously posted information and a good place to post new questions but you must follow the site rules.
Right of centre in the top bar is the Help button. Click this and read how to use this site. Learn how to post a question that will be classified as a good question and will be answered quickly and helpfully.
I believe the biggest three problems with your question are:
You ask too much. You can ask as many good questions as you wish but there should only be one issue per question.
You ask for information that is already available.
You are too vague about your requirement to permit anyone to help. You say you want to move three values per staff member. But you do not show how either the worksheet “Calculation Sheet” or the staff member worksheets are arranged. You cannot post images until you have a higher reputation but you can use the code facility to create “drawings” of the worksheets.
To avoid asking too much, you must break your requirement into small steps. The following is my attempt to identify the necessary small steps based on my guess of what you seek.
The CSV files containing staff detail arrive as attachments to a daily email. Are you manually saving those attachment? An Outlook VBA macro to save an attachment would not be difficult to write. I suggest you leave this for later but if you search Stack Overflow for “[outlook-vba] Save attachment” you will find relevant code.
The above shows how I search Stack Overflow. I start with the tag for the language and follow it with some key words or a key phrase. Sometimes it takes me a few goes to get the right search term but I rarely fail to find something interesting
How are you importing the CSV to Excel? Are you doing this manually? There are many possible VBA approaches. Try searching for “[excel-vba] xxxx” where xxxx describes your preferred approach.
I assume the structure of the CSV file is pretty simple and there is no difficulty in find information in the individual rows. You appear to know the easiest technique for finding the last row so you should have no difficulty in creating a loop that works down the rows.
How do you relate the staff member’s name in the CSV file with the name of their worksheet? In your question you have worksheet names such as "DEAN 822", "CHRIS 829" and "PAULP 830". Are these the names used in the CSV file? What happens when a new staff member joins? I doubt this happens very often but you do not want to be amending your macro when it does happen.
I do not understand your requirement for the new data to be added to the right of any existing data. There will be three values per day so with around 200 working days per year that gives 600 columns. To me that sees an awkward arrangement. I would have thought one row per day would have been more convenient.
How will you run the macro? You mention a button or automatically. I do not like buttons since I find the tool bars cluttered enough already. I prefer to use shortcut keys such as Ctrl+q. I rarely have more than one macro per workbook of this type so that works well for me. By automatically, I assume you mean the macro will run automatically when the workbook is open. I would start with the shortcut key but when you are ready look up “Events” and “Event routines”. You will find an explanation of how you can have a macro start automatically when the workbook opens.
I hope the above is of some help.