Multiple lookup in a closed workbook using ADO Connection - excel

From the below Image I want to compare Second Workbook(Records.xlsm) with First Workbook(HandBook.xlsm)
I want to check if Department ID and Course ID Combination is valid by comparing it with the first workbook(HandBook.xlsm) and highlight in yellow if the combination doesn't exist.
But When i tried to write the code,I was able to check only the first record, i.e in the below example Dept Id 3000 has three different course ID but when I try to compare it is validating only with the first record occurrence 3000-123 , if I try to put any other combination 3000-124 or 3000-125 it is highlighted as error which should not be the case.
Columns("B:B").Select
Range("B1").Select
ActiveCell.FormulaR1C1 = "=IF(OR(NOT(ISERROR(MATCH(RC[2],INDEX('[HandBook.xlsm]Dept-Course'!C2,MATCH(RC[1],'[HandBook.xlsm]Dept-Course'!C1,0),0),0)))),"""",""ERROR"")"
Selection.Copy
Range("A1").Select
Selection.End(xlDown).Select
Selection.End(xlDown).Select
Selection.End(xlDown).Select
Selection.End(xlUp).Select
ActiveCell.Offset(0, 1).Select
If ActiveCell.Row > 2 Then
Range(Selection, Selection.End(xlUp)).Select
End If
ActiveSheet.Paste

I prepared 2 solutions for you. The first doesn't require VBA. But it needs a helper column and this is why I think you won't like it. However, you may like to try. In the helper column enter this formula.
=SUMPRODUCT(('[082 STO 200829 Records.xlsm]Records'!$A:$A=C2)*('[082 STO 200829 Records.xlsm]Records'!$B:$B=D2))
The referenced workbook must be open at the time of writing the formula. After that it can be closed. The formula will return either 1 or 0 depending upon whether a match was found in the referenced file. Observe that column A:A in the referenced sheet holds data similar to C2 and B:B has that same relationship with D2. The result you can use to highlight cells using conditional formatting.
Select the first pair of Department/Course IDs on your Handbook sheet.
Create a New Rule to conditionally format these cells depending upon a formula. (In my worksheet that was C2:D2)
Insert this formula: =$E2=0 (In my example E:E is the helper column)
Choose the highlight you like.
Before you close the dialog correct the range to which the formula applies. The field originally shows just the selected cells. Extend the range all the way down your sheet. You might also have selected all to begin with but I prefer this way if the range is big and you don't want to drag the selection forever.
I prepared a VBA solution as well but I didn't much like that, either. It's a lot of code compared with your humble beginnings and that is before I got around to dealing with the screen flicker as the referenced file is opened and closed. I'm not sure I shall be able to deal with that entirely.
Therefore I abandoned that attempt when it was nearly done and now work on a solution that doesn't open the referenced workbook. I shall come back to publish it here later today.
Meanwhile I think the above solution has much to speak for it by way of simplicity. Bear in mind that you can have the helper column anywhere on the sheet, and you can hide it as well.

There are two parts of the code for this solution which must be placed exactly where they belong. The first part is an event procedure. It fires automatically when the user changes either the Department or the Course in the Handbook. This Change event will not be noticed anywhere in your workbook except in the worksheet concerned. Therefore the code must be in that tab's code module. That is an existing module, set up by Excel for this purpose.
The second part of the code deals with the external workbook which I identified as "Records.xlsm". Therefore I prefer it to be in a standard code module. That is a module you set up yourself. The default name will be Module1 but I (with the support of all but the most new newbies at programming) recommend to give a descriptive name. In my copy of the workbook I named it ADO_Conn for the ADODB Connection it contains.
In addition to the ADODB connection this part also contains various parameters which you may adjust to match your needs and liking. They take the shape of enumerations which offer an efficient way to allot names to numeric constants. I placed them here because some of them are used in both parts of the code. Their point is to let you make the code work differently without digging into the code itself. You just twiddle the knobs, as it were.
If you followed me thus far you may have noticed that there is no code for you to press a button or F5 so that it runs. The ADODB connection is called by the event procedure and the event procedure is triggered by the changes the user makes on the worksheet. The functionality is simple. When the user makes a change the macro looks for the combination of Department and Course and marks the cells if it isn't found. If the user thereupon changes the entry the process is repeated and the highlight may be removed. However, no change is triggered by a subsequent change in the Records. Such changes should be driven by change events in the Records workbook.
The more automation you want the more precise must be the setup. Start by copying part 2, here following, to a standard code module called ADO_Conn (if you like). Observe that the name avoids a space by substituting it with an underscore. This rule will also apply to the names of the two columns in Records that will be accessed. I renamed them as "Dept_ID" and "Course_ID". You can use different names, shift the columns to other locations, but you may not include any blanks in these names, nor should you change their sequence in the one place in the code where they are mentioned. If the names in the code differ from those in the workbook the workbook will still work but the code won't. Here is part 2.
Option Explicit
Enum Nwt ' worksheet Target ("Handbook" = ThisWorkbook)
' 082
NwtFirstDataRow = 2 ' change to suit
NwtDept = 3 ' Columns: 3 = C
NwtCourse ' if no value is assigned, [preceding + 1]
End Enum
Enum Nct ' search criteria: TriggerRng()
' 082
NctDept = 1 ' do not change (!!)
NctCourse
End Enum
Function HasMatch(Crits As Variant, _
SrcFile As String, _
SrcTab As String, _
SrcClms As String) As Boolean
' 082
Dim ConSpec As String
Dim Conn As Object ' late-bound ADODB.Connection
Dim Rs As Object ' late-bound ADODB.Recordset
Dim Query As String ' SQL query
Dim Sp() As String ' array of Clms
On Error GoTo ErrExit
' Create the record set and ADODB connection
Set Rs = CreateObject("ADODB.Recordset")
Set Conn = CreateObject("ADODB.Connection")
With Conn
.Provider = "Microsoft.ACE.OLEDB.12.0"
.ConnectionString = "Data Source=" & SrcFile & ";" & _
"Extended Properties=""Excel 12.0;" & _
"HDR=Yes;" & _
"IMEX=1"";"
.Open
End With
' create the SQL query string
Sp = Split("," & SrcClms, ",") ' first column index = 1
Query = "SELECT " & Sp(NctDept) & _
" FROM [" & SrcTab$ & "$]" & _
" WHERE " & Sp(NctDept) & " = " & Crits(1, NctDept) & _
" AND " & Sp(NctCourse) & " = " & Crits(1, NctCourse) & ";"
Rs.Open Query, Conn, 0, 1, 1 ' execute the query
' evaluate the retrieved recordset
HasMatch = Rs.EOF
ErrExit:
If Err Then
MsgBox "An error occurred during data retrieval:-" & vbCr & _
Err.Description, _
vbExclamation, "Error No. " & Err.Number
End If
Err.Clear
End Function
There are 2 sets of Department/Course ID numbers. The columns used in the Handbook sheet and an ID for each that the program itself uses. You can move the columns to where you want them. They don't have to stay together but I think the Department column must stay to the left of the Course column. Just change the numbers assigned to the names and the program will find them. You can also change the FirstDataRow for the Handbook sheet. But the the Records sheet only one header row is allowed - fixed, therefore not adjustable.
Here is the first part of the code. Paste it to the code module of the worksheet in Handbook where you want your entries checked.
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
' 082
' name the source workbook with its complete path (change to match your facts)
Const SrcPath As String = "D:\PVT Archive\Class 1\1-2020 (Jan 2022)\" ' end on \
Const SrcFile As String = "082 STO 200829 Records.xlsm"
Const SrcTab As String = "Records"
' match the column names in the workbook with the names used here.
' If they are changed assign names without spaces in them and
' maintain their logical sequence.
Const SrcClms As String = "Dept_ID,Course_ID"
Dim Matched As Boolean ' apply no highlight if True
Dim TriggerRng As Range ' the range that triggers action
Dim Crits As Variant ' search criteria
' don't react to changes in more than one cell
If Target.CountLarge > 1 Then Exit Sub
Set TriggerRng = Range(Cells(NwtFirstDataRow, NwtDept), _
Cells(Rows.Count, NwtDept).End(xlUp))
Set TriggerRng = Application.Union(TriggerRng, TriggerRng.Offset(0, NwtCourse - NwtDept))
If Not Intersect(Target, TriggerRng) Is Nothing Then
With Target
Set TriggerRng = Application.Union(Cells(.Row, NwtDept), _
Cells(.Row, NwtCourse))
Crits = TriggerRng.Value
If WorksheetFunction.CountA(TriggerRng) < 2 Then Exit Sub
End With
If Dir(SrcPath & SrcFile) = "" Then
' check if referenced workbook exists at the specified location
MsgBox "The workbook to be referenced" & vbCr & _
SrcFile & vbCr & "can't be found at" & vbCr & _
SrcPath & ".", _
vbInformation, "Data source not accessible"
Exit Sub
End If
With TriggerRng
If HasMatch(Crits, SrcPath & SrcFile, SrcTab, SrcClms) Then
.Interior.Color = vbYellow
Else
.Interior.Pattern = xlNone
End If
End With
End If
End Sub
There are 4 constants to be set by you. This must be done very precisely. You may also like to review the text of the messages, and I shall not mind if you improve them to better suit your needs. The rest of the code is intended to stay untouched. Whatever modifications you want must be done by using the parameters, unless you find flaws in the functionality, which I hope you will not.
SrcPath holds the path to the workbook Records. It must end on a backslash "". SrcFile holds the name of that file. This program doesn't mind if it's open or closed. SrcTab holds the name of the worksheet. I suspect that having a space in it might cause a problem. So, better avoid one. Finally, SrcClms gives the names of the column captions of the two columns in Records that we are concerned with here. Keep them aligned with what they really are, keep them free from blanks and keep their sequence aligned with the Enum Nct. Mind that ADO (ActiveX Data Object, btw) doesn't allow you to have more than 1 header row in the Records sheet. Not that it should make any difference in this particular application if there were more, unless the header rows contain potential matches. However, avoid having merged cells on that sheet anywhere.

Related

vba excel, formula, stop excel chainging the forumla in the table

I have a button that creates a table, (report generator)
in this table i link to various pages with the same syntax.
Worksheets("Engine").Range("E" & EngineStatus).Formula = "=" & newSheetName & "!I3"
It is working, and it is running on a loop.
However, when
I am linking the value from each sheet, in the same cell on each sheet.
but of course only for selected sheets.
The problem occur on the report, as seen on the screenshot.
Sheetname in this case is ACDTCM0137 and cell i3 is what i put in my code..
The output is it counts i as a increased number which it should not.
And it overwrites ALL rows in this column with it's LAST value..
So the last sheet might be called BDMETHR0148 and same cell..
But the last one in, is the one it shows for ALL rows.
How do i ensure that for each row it keeps the formatting from the cove above?
Meaning it should always bee by this syntax
You should read about relative and absolute reference in Excel.
Instead:
Worksheets("Engine").Range("E" & EngineStatus).Formula = _
"=" & newSheetName & "!I3"
Use:
Worksheets("Engine").Range("E" & EngineStatus).Formula = _
"=" & newSheetName & "!$I$3" ' dollar sign $ before column and row locks it
You wrote that you have button to create report, so maybe the code below will be even better (not recalculating until report generated again - values only, not formula):
' all vars before loop "dimmed" only once
(...)
Dim rngEngine As Range
Dim rngStatus As Range
' And in your loop
EngineStatus = ...
newSheetName = ...
Set rngEngine = Worksheets("Engine").Range("E" & EngineStatus)
Set rngStatus = Worksheets(newSheetName).Range("I3")
rngEngine.Value = rngStatus.Value

Switch Worksheet Names without Updating References to those Sheets

My file has two identical Worksheets for users to input two different sets of assumption variables, called "InputA" and "InputB". I want to quickly switch which Input sheet is feeding into the other sheets of the model.
Using Find and Replace took over 5 minutes, and there were over 350,000 references to "InputA".
I tried the following macro, which takes an instant to run, but unfortunately also changes all references in the workbook, effectively keeping everything referenced to the original input sheet.
Sheets("InputA").Name = "temp"
Sheets("InputB").Name = "InputA"
Sheets("temp").Name = "InputB"
Is there a way to execute the macro but prevent any references to worksheets from changing to the new sheet name, basically freezing everything except the sheet name change? Or perhaps any other solution that will work quickly? I don't want to go through 350,000 instances and rewrite using INDIRECT(), as that is the only other solution I've seen, because my references are complex and nested and that will take an age.
Thanks.
Assuming that your 2 Input-Sheets have the same structure, I would suggest the following:
Create a named range on Workbook-level, name it for example InputData. This range should contain all data from InputA.
Create a helper-sheet and name it Input - you can later set it to hidden.
Mark the range in the new sheet that is exactly the size of the Input-Data-Range and enter the formula =InputData as Array-formula. You can do so by entering Ctrl+Shift+Enter. The formula should have curly brackets and the sheet should now display the data of InputA.
Change all you formulas to refer to the helper Sheet Input instead of InputA.
Create a macro:
Sub switchInput()
Const sheetAName = "InputA"
Const sheetBName = "InputB"
With ThisWorkbook.Names("inputData")
If InStr(.RefersTo, sheetAName) > 0 Then
.RefersTo = Replace(.RefersTo, sheetAName, sheetBName)
Else
.RefersTo = Replace(.RefersTo, sheetBName, sheetAName)
End If
End With
End Sub
This routine will just change the definition of the named range to point either to the first or second input sheet. As a consequence, the helper sheet will show the data of the selected Input-Sheet. All your formulas itself stays unchanged.
As stated in the comments, you could take the approach recommended by Damian and use a conditional in all relevant cells. The generic conditional would be
=IF(A1="InputA",FORMULA INPUTA,FORMULA INPUTB)
This formula makes A1 the cell that decides which input to pull. This will make changing the around 350.000 output formulas in your workbook the bottleneck, the following macro takes care of changing all the formulas to conatin the conditional:
Sub changeFormulas()
Dim rng As Range, cll As Range
Set rng = shOutput.Range("A2:C10") 'Your relevant worksheet and range here
Dim aStr As String, bStr As String, formulaStr As String
aStr = "InputA"
bStr = "InputB"
For Each cll In rng
If cll.HasFormula And InStr(1, cll.Formula, aStr, 1) Then
formulaStr = Right(cll.Formula, Len(cll.Formula) - 1)
cll.Formula = "=IF(A1=" & Chr(34) & aStr & Chr(34) & "," & formulaStr & "," & Replace(formulaStr, aStr, bStr) & ")" 'Change A1 to the reference most suited for your case
End If
Next cll
End Sub
This might take a bit of time, since it has to access all the relevant cells one by one, but it will only have to run once.
To explain: This macro will go through all the cells in your range rng specified at the top. If a cell has a formula in it and the formula contains "InputA" it will change that formula to fit the generic conditional (with the cells own formula of course). Chr(34) is the quotation mark ", I find using Chr(34) more readable than multiple quotation marks """, but that is just preference.

Hide all sheets except selected - Excel VBA

First post so please excuse any poor etiquette.
I am writing a VBA which produces an information pack for our customers to use. This pack is built to be dynamic based on certain user selections - for example FAQ pages will be specific to selections made by the user. I have built each module to add the relevant sheet names to a contents sheet as it runs. At the end, I pick up this list of sheets, select them and produce a PDF.
What I am trying to do is hide all of the other sheets other than the ones that are selected - I can do this by calling them specifically by name, or by adding "zzz" to the end of sheet names which are pure reference sheets, but I am hoping there is a better way.
The code I want would do this:
Select all of the sheets which have been added to the contents page and create an array (this already works)
Produce the PDF for the selected sheets (this also works)
Hide any sheet which is not in the array (this is what I am struggling with)
Code for producing the PDF is below - note that due to some ill-planned naming, "Contents Array" is a named range on the contents sheet, and "ContentsList" is the VBA array:
For Each cell In wsContents.Range("ContentsArray")
ContentsList(j) = cell.Value
wb1.Sheets(cell.Value).Select
j = j + 1
Next 'cell
wb1.Sheets(ContentsList()).Select
FileName = wsControl.Range("CustomerName") & " Pack " & Format(wsControl.Range("ReportDate"), "dd-mm-yyyy")
PDFFilePath = wb1.Path & "/AutoGenerated Packs/" & FileName & ".pdf"
ActiveSheet.ExportAsFixedFormat Type:=xlTypePDF, FileName:=PDFFilePath, _
Quality:=xlQualityStandard, IncludeDocProperties:=True, IgnorePrintAreas:=False, OpenAfterPublish:=True
First, you can set your array to the range in one go:
ContentsList = wsContents.Range("ContentsArray").Value
You will then need to loop through your array for each sheet in your workbook to check if it exists... Something like the below (if you use the same method for the array as you already have, else you will need another way to determine j (the number of elements in your array)):
Dim ws As Worksheet
Dim i As Long
For Each ws In Thisworkbook.Worksheets
For i = 1 To j
If ws.Name = ContentsList(i) Then
'if found exit for as we do not want to hide
Exit For
End If
'if not found then hide
If i = j Then
ws.Visible = xlSheetHidden
End If
Next j
Next ws
Hope this helps!

Loop Through List and Generate Reports

I'm struggling with what I believe is a loop problem. I'm more of a "backyard mechanic" with Excel VBA so please excuse my simple question.
I can't share the workbook due to proprietary information unfortunately but I have the code I use with some field name changes.
Background: I have a column that I take 1 cell at a time and feed it into a pivot table field and run reports. The way I have it now, I delete the current Row which returns the reference back to cell A2. Think of it like a programming Pez dispenser. Awful and very brute force I know. The row delete operation takes a lot of system resources and I'd like to optimize it. I've tried reading through here and a few other websites for the past couple hours but I can't make heads or tails of what I'm coming across.
Any help would be very greatly appreciated!
Sub AutoReport()
Dim strPage As String
Worksheets("HomePage").Select
Beginning:
'Sets the name in Home Page to the name in Feederlist cell F2
With Sheet1
strPage = Worksheets("FeederList").Range("A2")
Worksheets("HomePage").PivotTables("PivotTable1").PivotFields("UNIQUE ID"). _
CurrentPage = strPage
End With
**Do a Bunch of Stuff**
' Feeds the next input into the machine
MoveToNext:
Worksheets("FeederList").Activate
Worksheets("FeederList").Range("A2").EntireRow.Delete
If Worksheets("FeederList").Range("A2") = "" Then
MsgBox "All Reports have been created.", vbInformation + vbOKOnly
Exit Sub
Else
GoTo Beginning
End If
End Sub
the acceptance is the first step to learning. In general, the stackoverflow community encourages to invigorate the technical and logical parts of the brain by providing the hints to solution and not the exact solution. However, as you are very new, I am starting with giving off hints and later on the code to rectify your issue. You are going very well with your code, however some minor tweaks will optimize your code significantly.
Worksheets("HomePage").Select
Dim lstRow As Long
Dim rngCell As Range
Dim rngSelection As Range
'Let's find the last row with data in column A.
'So that we only traverse the required range without the need of
'deleting previous cells while using the For loop.
lstRow = Worksheets("FeederList").Range("A" & Application.Rows.Count).End(xlUp).Row
Set rngSelection = Worksheets("FeederList").Range("A2:A" & lstRow)
For Each rngCell In rngSelection.Cells
'Ignore all the cells with blank value or else the pivot table will throw the error
If Trim(rngCell.Value) <> vbNullString Then
'Sets the name in Home Page to the name in Feederlist cell F2
strPage = rngCell.Value
Worksheets("HomePage").PivotTables("PivotTable1").PivotFields("UNIQUE ID"). _
CurrentPage = strPage
' **Do a Bunch of Stuff**
End If
Next
rngSelection.Clear ' Optional - if the range really needs to be cleared
MsgBox "All Reports have been created.", vbInformation + vbOKOnly
You need to understand the changes and ask any further questions that you might have. Below are the changes made to the original code.
Introduced calculation of last cell in Column A having some data
Introduced For Loop instead of using labels eliminating the need of deleting rows

Custom Excel Formats that reference cells

I would like to have an Excel sheet that displays a custom format. The custom format uses the contents of another cell. Here is an example:
Column A : Show a column of numbers in accounting format in the currency of cell(b1).
Cell(B1) : "XYZ"
The intention is that users can enter their own currency. I know there are formatting tools to do this in Excel but this is a question that implements a custom format based upon another cell contents. That's the real question...
A worksheet_change in the worksheet's private code sheet can alter the number formatting in column A.
private sub worksheet_change (byval target as range)
if not intersect(target, range("b1")) is nothing then
if len(cells(1, "b").value2) = 3 then
dim f as string
f = ucase(cells(1, "b").value2)
f = "0 \" & left(f, 1) & "\" & mid(f, 2, 1) & "\" & right(f, 1)
range(cells(2, "a"), cells(rows.count, "a").end(xlup)).numberformat = f
end if
end if
end sub
There are a maximum number of custom number formats that can be added to the existing number formats without deleting previously created cnfs; I think it's around 30 or so.
Assuming you want something like this:
..then just put this in the Sheet Module:
Sub worksheet_Change(ByVal Target As Range)
Dim sFormat As String
If Not Intersect(Target, Range("NumberFormat")) Is Nothing Then
sFormat = Chr(34) & Target.Value & Chr(34) & " "
Range("FormatWhat").NumberFormat = sFormat & "$#,##0;" & sFormat & "[Red]-$#,##0;-"
End If
End Sub
...and give B1 the Name NumberFormat in the Name Box:
...and likewise name some or all of column A "FormatWhat".
(Using Named Ranges avoids hard-coding references in your code. If you hard code cell address into your code, those references will be pointing at the wrong place if you (or a user) later adds new rows/columns above/to the left of those hard-coded references. Using Names avoids this, and makes for more robust code.
I almost never hard-code cell addresses in my code. I almost always use Excel Tables aka ListObjects to hold any data that VBA interacts with for the same reason...ListObjects are dynamic named ranges that Excel automatically expands/contracts to suit the data.)

Resources