Using arrays and macros in VBA [closed] - excel

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
Questions asking for code must demonstrate a minimal understanding of the problem being solved. Include attempted solutions, why they didn't work, and the expected results. See also: Stack Overflow question checklist
Closed 9 years ago.
Improve this question
I'm a QA. I'm not a developer, but I asked my boss for some busy work. She gave me a development project, and I'm completely confused. We have a spreadsheet with a long list of salespeople and their customers. For each customer, the salesperson, customer name, sales amount, and location are listed like so:
Salesperson Customer Name Sales Amount Location
salesperson1 customerA 3456789 Atlanta
salesperson1 customerB 9475903 Atlanta
salesperson1 customerC 7236433 Atlanta
salesperson1 customerD 9809489 Raleigh
salesperson2 customerA 3456789 Raleigh
salesperson2 customerB 9475903 Raleigh
salesperson2 customerC 7236433 Raleigh
salesperson2 customerD 9809489 Raleigh
salesperson2 customerE 3456789 Raleigh
salesperson3 customerA 9475903 Portland
salesperson3 customerB 7236433 Portland
salesperson3 customerC 9809489 Portland
salesperson4 customerA 9475903 Portland
salesperson4 customerB 7236433 Portland
salesperson4 customerC 9809489 Portland
There are hundreds of lines of salespeople and customers on this spreadsheet. My job is to create a new workbook template. I have successfully done that. The template takes relative sales data and does calculations based on sales numbers. Currently, I've been able to get all of the data from the source in to the template, and the calculations work. However, I need to go further. For each salesperson, I need there data loaded on to a separate sheet, and I need a separate template for each location. For example: notice that salesperson3 and salesperson 4 are in the Portland location. I need a separate sheet for salesperson3 and a separate sheet for salesperson4 on the same workbook which needs to be saved as PORTLAND.XLSM. There will also need to be a template called RALEIGH.XLSM and ALTANTA.XLSM.
So here are my questions:
How do I make an array to notice when I go from salesperson1 to salesperson2 to salesperson3 to salesperson4?
How do I do an array that will eliminate anyone not at that location? I presume I will need a worksheet for each location, and if the name of the worksheet doesn't match the location of that salesperson, that salesperson doesn't get a sheet in that workbook.
I haven't used VB since I was in college almost 2 decades ago. I know the logic, but I don't know the language and syntax.

As Brad mentioned this is going to be a beast to maintain. That being said, if I understand your question correctly, I'd use the .Range() function. If you have your data in a table you can pull an array and do what you're looking for with this function.
Public Sub createSheets()
Dim salespersons As Range, location As String, fileLocation As String, salesperson As String, newSheet As Worksheet
Set salespersons = Range("Table1[Salesperson]")
For i = 1 To salespersons.Count
location = salespersons(i).Offset(0, 3).Value
If i = 1 Or (salespersons(i).Offset(0, 3).Value <> salespersons(i - 1).Offset(0, 3).Value) Then
Call createBook(location)
End If
Next i
For Each wbook In Application.Workbooks
For i = 1 To salespersons.Count
If i = 1 Or (i > 1 And salespersons(i) <> salespersons(i - 1)) Then
salesperson = salespersons(i).Value
location = salespersons(i).Offset(0, 3).Value
If location & ".xlsx" = wbook.Name Then
If i = 1 Or (salespersons(i).Value <> salespersons(i - 1).Value) Then
On Error Resume Next
Set newSheet = wbook.Worksheets(salesperson)
If Err.Number <> 0 Then
Set newSheet = wbook.Worksheets.Add
newSheet.Name = salesperson
End If
End If
End If
End If
Next i
Next
End Sub
Public Sub createBook(location As String)
Dim newBook As Workbook, newSheet As Worksheet, result1 As Boolean, result2 As Boolean
result1 = isWorkbookOpen(location & ".xlsx")
If result1 = False Then
On Error Resume Next
Workbooks(location).Save
If Err.Number <> 0 Then
On Error Resume Next
Workbooks.Open (location & ".xlsx")
If Err.Number <> 0 Then
Workbooks.Add.SaveAs (location & ".xlsx")
Error Err.Number
End If
End If
End If
End Sub
Function isWorkbookOpen(location As String)
Dim ff As Long, Errno As Long
On Error Resume Next
ff = FreeFile()
Open Filename For Input Lock Read As #ff
Close ff
Errno = Err
On Error GoTo 0
Select Case Errno
Case 0:
isWorkbookOpen = False
Case 70:
isWorkbookOpen = True
Case 75:
isWorkbookOpen = False
Case Else: Error Errno
End Select
End Function

Here's an option using pivot tables that requires NO VBA. If you use this, maintenance takes a little more work, but it's very simple to set up and hand off:
From your source, make a pivot-table and add it to a new document.
For your Report Filter, add the SalesPerson and Location. Leave the actual dropdowns at (All) for now or just pick values at random.
For your Row label, you can add the Customer.
For your Values, you can sum your sales amounts.
Make the tab as pretty as you like. This will be your template for all the sales people. The Options ribbon lets you add custom calcuations, % of row totals, etc. Go wild. You now have a tab which can serve as a template for every salesperson!
Make a blank workbook for each location. For each location's workbook, add this template tab for each salesperson that works there. In that tab's pivot table's filters, pick the location and a salesperson.
Repeat #6 until each workbook has a tab for each salesperson and each of those tabs has the proper salesperson and location selected.
Yes, that was monotonous, but now you're done with the hard part! Every salesperson has their own tab and every location has their own file. Thanks to the report filters in the many, many tabs you've just made, each tab is filtered to just the sales in that location for that salesperson.
Updating the data:
The biggest drawback to this option is that everytime a SalesPerson starts selling at a new location, or a new SalesPerson is hired, you'll have to add a new tab. But thanks to your common template tab, this isn't very painful.
For regular updates, just open each file and go to the Data ribbon and Refresh all.

This will loop over a source sheet (the active sheet) and generate a number of workbooks (one per location). A sheet containing customer name and sales amount for each sales person is added to the workbook.
once the workbooks are generated it saves them as their location's name.
' get a named worksheet from specified workbook, creating it if required
Public Function GetSheet(ByVal Name As String, ByVal Book As Workbook, Optional ByVal Ignore As Boolean = False) As Worksheet
Dim Sheet As Worksheet
Dim Key As String
Dim Result As Worksheet: Set Result = Nothing
Key = UCase(Name)
' loop over all the worksheets
For Each Sheet In Book.Worksheets
' break out of the loop if the sheet is found
If UCase(Sheet.Name) = Key Then
Set Result = Sheet
Exit For
End If
Next Sheet
' if the sheet isn't found..
If Result Is Nothing Then
If Ignore = False Then
If Not GetSheet("Sheet1", Book, True) Is Nothing Then
' rename sheet1
Set Result = Book.Worksheets("Sheet1")
Result.Name = Name
End If
Else
' create a new sheet
Set Result = Book.Worksheets.Add
Result.Name = Name
End If
End If
Set GetSheet = Result
End Function
Sub Main()
Dim Source As Worksheet
Dim Location As Workbook
Dim Sales As Worksheet
Dim LocationKey As String
Dim SalesKey As String
Dim Index As Variant
Dim Map As Object: Set Map = CreateObject("Scripting.Dictionary")
Dim Row As Long
Set Source = ThisWorkbook.ActiveSheet
Row = 2 ' Skip header row
Do
' break out of the loop - assumes that the first empty row signifies the end
If Source.Cells(Row, 1).Value2 = "" Then
Exit Do
End If
LocationKey = Source.Cells(Row, 4).Value2
' look at the location, and find the workbook, creating it if required
If Map.Exists(LocationKey) Then
Set Location = Map(LocationKey)
Else
Set Location = Application.Workbooks.Add(xlWBATWorksheet)
Map.Add LocationKey, Location
End If
SalesKey = Source.Cells(Row, 1).Value2
' get the sheet for the salesperson
Set Sales = GetSheet(SalesKey, Location)
' insert a blank row at row 1
Sales.Rows(1).Insert xlShiftDown
' populate said row with the data from the source
Sales.Cells(1, 1).Value2 = Source.Cells(Row, 2)
Sales.Cells(1, 2).Value2 = Source.Cells(Row, 3)
'increment the loop
Row = Row + 1
Loop
' loop over the resulting workbooks and save them - using the location name as file name
For Each Index In Map.Keys
Set Location = Map(Index)
Location.SaveAs Filename:=Index
Next Index
End Sub
Update
This code is only really helpful if you are generating read-only reports, that get replaced every time you generate the report. (this code will probably fail to save on subsequent runs as it does not check to see if a file exists when trying to save, you'll need to delete any previous output before running it again).
This code (in its current form) would need to be added into the workbook with your source data, and the data sheet would need to be active before you run the Main() macro.
If you are trying to do more than just generate a report (like replacing the source data sheet with your new files) it would be much simpler in Access as a few people have already suggested.

Related

Exporting a DB to XLS: It ignores formatting?

So I have a form that executes a VBA script via a macro. The purpose of said script is to open Excel, create a new workbook, gather information from several tables and export them to a formatted spreadsheet. Each person has a sheet with their name, and the relevant data is printed in said sheet. It works perfectly for the most part. Only one problem... The table in Access where the name and demographics data is gathered from is formatted to sort by last name ascending alphabetically. The VBA script exports it in the order the names were entered. I want my VBA script to respect the formatting in the database table, and I would prefer not to have to add an alphabetizing subroutine to my VBA script.
Table A Format: ID, Active, Last, First, Role, Traveler, Resident, Preceptee, Phone, Completion
Table B Format: ID, Course, Course ID, Offered, HLC, Course Type
Last in Table A called "Roster" is the field with which I want my VBA script to sort alphabetically. The database is already configured to do this.
Thanks in advance!
VBA Code:
Option Compare Database
' This module exports the database to a spreadsheet with specific formatting when called from a Macro
' Each Employee will have a sheet named thier last name which will contain all HLC modules they have completed in a list
' It is specific to this Database, but can be adapted to others.
' Version 1.0 Stable
Public Function ExportXLS(TblA As String, TblB As String, Optional names As String, Optional specific As Boolean)
'****************'
'Set up variables'
'****************'
Dim ctrA As Integer
Dim ctrB As Integer
Dim var As Long
Dim str As String
Dim excel As Object 'Pointer to Excel Application
Dim book As Object 'Pointer to Excel Workbook
Dim sheet As Object 'Pointer to Excell Sheet
Dim Roster As DAO.Recordset
Dim Course As DAO.Recordset
Dim Child As DAO.Recordset
Dim last_name As DAO.Recordset 'Matrix of pointers that will hold parts of the tables to be printed to the corresponding Excel sheets
Dim course_name As DAO.Recordset 'Matrix of pointers that will hold parts of the tables to be printed to the corresponding Excel sheets
'********************************************************'
'Initialize our tables into thier recordsets for analysis'
'********************************************************'
Set Roster = CurrentDb.OpenRecordset(TblA)
Set Course = CurrentDb.OpenRecordset(TblB)
str = "SELECT Last FROM Roster"
Set last_name = CurrentDb.OpenRecordset(str)
str = "SELECT Course FROM [Course List]"
Set course_name = CurrentDb.OpenRecordset(str)
'**************************************************************************'
'Create the new excel file with default parameters and print the cover page'
'**************************************************************************'
Set excel = CreateObject("Excel.Application")
Set book = excel.Workbooks.Add
excel.Visible = True
Set sheet = book.Worksheets("Sheet1")
str = "Coversheet"
sheet.Name = str
sheet.Range("B2") = "HLC Database Export tool V1.0"
sheet.Range("B3") = "Written by Levi T Jackson, RN, BSN"
sheet.Range("B4") = "All rights reserved, Copyright 2021"
sheet.Range("B5") = "For use only by Emory Healhtcare, and others with permissions"
'**********************************'
'Main Loop, where the magic happens'
'**********************************'
ctrA = 0
Roster.MoveFirst
last_name.MoveFirst
Do Until last_name.EOF 'Move through the list of last names in the table Roster, one at a time
If Roster!Active = True Then 'No need to report on inactive employees, use access query for that
Set Child = Roster!Completion.Value 'Open a Recordset for the multivalued field Completion in Roster
ctrB = 1
If Child.EOF = True Then 'save the number of records for printing, or set to 0
var = 0
Else
Child.MoveLast
var = Child.RecordCount
Child.MoveFirst
End If
Course.MoveLast
If Child.EOF = False Then 'Avoid errors by not processing a page if no completion records exist
Set sheet = book.sheets.Add(After:=book.Worksheets(book.Worksheets.count)) 'For active employees, make a new sheet and switch to it, and set its name to the current last name from Roster
sheet.Activate
sheet.Range("A1").SELECT
str = Roster!Last & ", " & Roster!First
sheet.Name = str
sheet.Range("B2") = "Courses Completed"
Do Until Child.EOF 'If there are records in Completion for the current name, print them, move on when done
Course.MoveFirst
course_name.MoveFirst
Do Until Course.EOF
If Course![Course ID] = CInt(Child!Value.Value) Then
sheet.Range("D" & Mid(coordinates(ctrB), 2, Len(coordinates(ctrB)) - 1)) = Course![Course ID] 'prints course ID next to the name
sheet.Range("D2") = "'" & CStr(var) & " / " & CStr(Course.RecordCount) 'Prints number of records in completions
sheet.Range("B3") = "Course Name"
sheet.Range("D3") = "Course ID"
sheet.Range(coordinates(ctrB)) = Course!Course 'Prints course name
ctrB = ctrB + 1
Course.MoveLast
Course.MoveNext
Else
Course.MoveNext
course_name.MoveNext
End If
Loop
Child.MoveNext
Loop
End If
ctrA = ctrA + 1 'I might use this later in code updates, counts how manmy records are processed
Child.Close
excel.ActiveSheet.Cells.SELECT 'Selects all of the cells
excel.ActiveSheet.Cells.EntireColumn.AutoFit 'Does the "autofit" for all columns
sheet.Range("A1").SELECT 'Selects the first cell to unselect all cells
End If
Roster.MoveNext
last_name.MoveNext
Loop
'Clean up recordsets
last_name.Close
course_name.Close
Roster.Close
Set Roster = Nothing
Course.Close
Set Course = Nothing
End Function
'Converts the iteration of the print course sub loop into a sheet coordinate cell and returns it as a string
'This function is here so that later a more complicated printing coordinate system can be easily added as the database grows larger
Private Function coordinates(num As Integer) As String
coordinates = "B" & CStr(num + 4)
End Function
Add an order by clause to your OpenRecordset statements.

Excel VBA Trying to send data from excel workbook to an other

I haven't been able to find a question that is close enough to what i need or maybe i'm not as good as googling as I thought I was.
Anyway I have an issue where I need to use excel as a database as my place of work doesn't have access.
The way in which this is supposed to work is that in the 'ui' workbook after the user has inputed the data required it needs to take the users employee number from the 'ui' workbook and use that to find them in a list of all employees on the 'database' worksheet then increase a field that indicates that they have completed this specific task one more time. Then on a different sheet also on the second workbook it needs to record the date completed and also input the users id against this date.
so far I haven't attempted the code for the second part as I am having trouble with the first. Here is what i have so gotten to so far and it errors at the match function
Sub SendToDB()
Application.ScreenUpdating = False
Dim DB As Workbook
Dim UI As Workbook
Dim ENo As Long
Dim CopyFrom As Worksheet
Set CopyFrom = Worksheets("Checklist")
ENo = Range("G45").Value
Set UI = ThisWorkbook
Set DB = Workbooks.Open("file location here\database.xlsx", True)
Dim EmCount As Long
Dim EmRow As Long
Dim EmCell As Range
Application.ScreenUpdating = False
DB.Worksheets("EMLIST").Activate
EmRow = Application.WorksheetFunction.Match(ENo, Range("AML"), 0)
EmCell = Range("G" & EmRow)
EmCount = Application.WorksheetFunction.VLookup(EmENo, Range("AML"), 7, False)
EmCount = EmCount + 1
EmCell.Value = EmCount
DB.Close True
Set DB = Nothing
Application.ScreenUpdating = True
End Sub
AML is a named range on the database spreadsheet which contains all the employee data
Thanks

When I Call another sub using an argument, it performs the actions in my original worksheet rather than the newly created worksheet

In my code, I select items from my workbook and create a new workbook to paste the selected items within. I then call another sub (Callothers) using an argument to pass along this new workbook such that the remaining code runs in the new workbook. However, rather than run in the new workbook, the rest of the code occurs in the original.
I have messed with the argument, however I am not reaching a solution.
Sub occurences()
'sort
Set oldbook = ActiveWorkbook
lRow = Cells(Rows.Count, 42).End(xlUp).Row 'Finds the last used row
Dim coll As New Collection 'Collections are similar to arrays, but you don't need to declare a size
For Row = 2 To lRow 'Loop through each row
newitem = Sheets("Sheet1").Cells(Row, 42) 'Grab the contents of the row
flag = False 'flag will be false unless we find a match in our collection
For Each Item In coll 'loop through our collection
If Item = newitem Then 'check for a match
flag = True 'if there is a match, set flag
End If
Next Item
If flag = False Then 'if a match wasn't found,
coll.Add newitem 'add the new item to the collection
End If
Next Row 'now go to the next row and start again
MsgBox (coll.Count) 'this tells us how many items are in the collection
For Each Item In coll 'this displays each item in the collection
Set newbook = Workbooks.Add
MsgBox ("oldbook a2 = " & oldbook.Sheets("Sheet1").Range("A2"))
With newbook
Row = 1
oldbook.Sheets("Sheet1").Range("a1:ar1").Copy .Sheets("Sheet1").Rows(Str(Row))
nRow = 2
For Row = 2 To lRow
If oldbook.Sheets("Sheet1").Cells(Row, 42) = Item Then
oldbook.Sheets("Sheet1").Rows(Str(Row)).Copy .Sheets("Sheet1").Rows(Str(nRow))
nRow = nRow + 1
End If
Next Row
fname = Replace(Item, " ", "-")
fname = fname & ".xlsx"
MsgBox ("about to call")
Call CallOthers(newbook)
.SaveAs Filename:=fname '("C:\Users\Joshua.Elphee\Desktop\TEST Save\" & fname)
.Close
End With
Next Item
End Sub
Sub CallOthers(newbook)
Call Delete_Rows_Based_On_Value(newbook)
Call Delete_Rows_Based_On_Value1(newbook)
End Sub
No error message, just performs the actions within the wrong workbook
You need put more info, but if idea is: You have 2 workbooks OLDBook and NEWBook , you extract info from OLDBook and put in NEWBook then use "Call Sub OTHER()" and you problem is that instead delete rows in NEWBook delete rows in OLDBook . For you, problem is in your code OTHER but you dont put here (maybe is top secret XD) so you need are explicit sentences like OLDBook.Sheets(1) and NEWBook.close then be sure active workbook you are using like OLDBook.active because when you use .ADD you create a variable as workbooks (collection) that have 2 elements workbook OLDBook and workbook OLDBook ; however if you dont like use this way you also can use public variables on top your module put
Public OLDBook as workbook
Public OLDBook as workbook
so only you need to use inside your sub()
Set OLDBook = ActiveWorkbook
Set NEWBook = new Workbooks
or if you have path
OLDBook.open "C:\T\"
NEWBook = new Workbook
NEWBook.open

Moving records between worksheets when input form value updates

I am unable to figure out how to cut and paste records between two worksheets when a value from the input form is changed from "Open Project" to "Closed Project."
This is for a project management database I am creating to to cut down the time spent adding new data. So far this is my only time working with VBA.
Option explicit
Private Sub CommandButton1_Click()
'Searches for record, and cuts/ pastes records into Project Log when Project Status is set to Closed
Dim ab As Worksheet
Dim aa As Worksheet
Set ab = ThisWorkbook.Sheets("Open Projects No SLA - Log")
Set aa = ThisWorkbook.Sheets("Project Log")
Dim q As Long
Dim m As Long
If Me.proj_stat_combo.Value = "OPEN PROJECTS (No Current Open SLA)" Then
q = Application.Match(VBA.CLng(Me.srnew_combo.Value), ab.Range("C:C"), 0)
m = ab.Range("C" & Application.Rows.Count).End(xlUp).Row
Worksheets("Open Projects No SLA - Log").Range(1).Cut Worksheets("Project Log").Range(m + 1)
End If
Hoping to get the records from "Open Projects No SLA - Log" to be cut out of the worksheet and pasted into the "Project Log" worksheet" when the input form data for Project Status has changed from "OPEN PROJECTS (No Current Open SLA)" to closed.
Try something like this:
Option explicit
Private Sub CommandButton1_Click()
'Searches for record, and cuts/ pastes records into Project Log
' when Project Status is set to Closed
Dim ab As Worksheet, q As variant, m As Long
Dim aa As Worksheet
Set ab = ThisWorkbook.Sheets("Open Projects No SLA - Log")
Set aa = ThisWorkbook.Sheets("Project Log")
If Me.proj_stat_combo.Value = "OPEN PROJECTS (No Current Open SLA)" Then
q = Application.Match(VBA.CLng(Me.srnew_combo.Value), ab.Range("C:C"), 0)
'check we got a match
If not iserror(q) then
'adjust next line to fit your # of columns and exact paste destination...
ab.Cells(q,"A").Resize(1,30).cut aa.Cells(rows.count,"C").End(xlUp).Offset(1,0)
Else
Msgbox "No match for project id!"
End if
End If

VBA - Adding Counter For Tracking

New to writing code in VBA, and I need to add in a way to count/track how often a macro is used. I'm struggling to find any examples of a code that would perform this function. Requesting any examples there might be for this.
Excel VBA doesn't have any built-in storage that persists across sessions - when the application is closed, all variables are released. However you do have a readily available source of storage - the workbook itself. Here's one way to do it:
Make a worksheet called Counter (or something). Optionally, hide it so no one messes with it. Then, when your macro runs, increment the value of Cell(1,1), which will hold your counter value.
Dim runCount As Integer
Dim counterWS As Worksheet
Set counterWS = ThisWorkbook.Worksheets("Counter")
If counterWS.Cells(1,1) <> vbNullString And IsNumeric(counterWS.Cells(1,1)) Then
runCount = counterWS.Cells(1,1) + 1
Else
runCount = 1
End If
counterWS.Cells(1,1) = runCount
Use static when declaring your variable.
From "EXCEL" Ytics:
Can we declare a variable to retain its value from a previous run?
Static Declaration within a procedure limits the variable to that procedure – but helps retain values of variables from previous runs, until forcefully reinitialized by explicit declaration using ‘ReDim’ or value setting.
Sub MySub( )
Static Cnt As Integer
Dim Msg As String
Cnt = Cnt + 1
Msg = “You’ve run the procedure ” & Cnt & ” times”
MsgBox Msg
End Sub
This will build a table for you on a hidden sheet showing all historical uses of the macro up to the nth time the macro was used along with a time stamp so you know when the macro was used. With this, you can use equations to extract exact data that you want as well. If you want a cell to show usage count, just to "Countif("A:A","*")-1” ... (minus 1 to ignore the header in A1)
To Implement this macro:
Create a new sheet titled "MacroCount"
Set A1 = "Instance"
Set B1 = "Time Stamp"
I would personally make this the last thing the macro does so it will only count the instance once the macro completes all of its assigned duties.
Also, Remove the "Sub MacroCount()" & "End Sub" from the below code.
Sub MacroCount()
Dim WS As Worksheet
Set WS = ThisWorkbook.Sheets("MacroCount")
Application.Screenupdating = False
WS.Visible = True
Dim LRow As Long
LRow = WS.Range("A" & Rows.Count).End(xlUp).Offset(1).Row
WS.Range("A" & LRow).Value = LRow - 1
WS.Range("B" & LRow).Value = Now()
WS.Visible = False
Application.Screenupdating = True
End Sub

Resources