I have a list of workbooks that follow the same template, and all have a macro 'beforesave' event, which basically creates a save-log in on of the sheets, listing the time and user id from whoever is saving the workbook.
So sometimes I need to change a formula in all workbooks, and since it takes around a minute to close and save each of them manually, I created a macro that opens all of them changes what I need changed, and closes saving them. That would save me some significant amount of time, as on total this process ends up taking around 30 mins of my time, and a lot of boredom.
Problem is: when the macro tries to close and save the workbook, the 'beforesave' event doesn't run properly and as a result the workbook doesn't save and close with the macro. For some reason, the event seems to be skipped in the macro...
To solve that I want to run this macro to make all the changes in all workbooks, skipping the beforesave event (if necessary) and actually saving and close them at the end.
Help please?
Tried running the macro line by line, and when it gets to the event part, it calls it, but for some reason it stays in the same sheet instead of going to the log sheet, and writes the log info in the wrong sheet as a result. In any case, running line by line won't obviously work for me as it's basically the same as doing the process myself, manually.
Sub DoStuff()
Dim Row As Integer
Dim Col As Integer
Dim wbCopy As Workbook
Dim wbPaste As Workbook
Dim wbBP As Workbook
For Col = 4 To 4
ThisWorkbook.Activate
Set wbBP = Workbooks.Open(Cells(1, Col), False)
ThisWorkbook.Activate
Set wbCopy = Workbooks.Open(Cells(2, Col), False, True)
For Row = 3 To 19
ThisWorkbook.Activate
SetAttr Cells(Row, Col), vbNormal
Set wbPaste = Workbooks.Open(Cells(Row, Col), False)
wbCopy.Activate
Sheets("Base").Activate
Range("A7:EQ500").AutoFilter
wbPaste.Activate
Sheets("Base").Activate
Range("A7:EQ500").AutoFilter
wbCopy.Activate
Sheets("Base").Activate
Range("AL8:AS8").Copy
wbPaste.Activate
Sheets("Base").Activate
Range("AL8:AS" & Cells(Rows.Count, 1).End(xlUp).Row).PasteSpecial xlPasteFormulas
Application.CutCopyMode = False
wbPaste.Close True
ThisWorkbook.Activate
SetAttr Cells(Row, Col), vbReadOnly
Next Row
wbCopy.Close False
wbBP.Close False
Next Col
End Sub
Running the macro to do the changes I need, and having all workbooks involved saved and closed properly.
Welcome to SO. The answer to the title is too simple if you are OK to execute your update job running any events in the wbPaste workbooks. Just simply adding Application.EnableEvents = False before the saving the file and make it true after the save completion.
Also based on the points commented by #Mathieu Guindon your post is confusing about your exact requirement and the inadvertent use of Activate I just restructured your code a little to avoid running any events during update.
Sub DoStuff()
Dim Row As Integer
Dim Col As Integer
Dim wbCopy As Workbook
Dim wbPaste As Workbook
Dim wbBP As Workbook
‘Worksheet name “FileList” used for trial . May please change to yours or use activesheet
With ThisWorkbook.Worksheets("FileList")
For Col = 4 To 4
‘Could not understand why wbBP opened, it is not used anywhere in the code
Set wbBP = Workbooks.Open(.Cells(1, Col), False)
Set wbCopy = Workbooks.Open(.Cells(2, Col), False, True)
‘ This will disable any events including ‘BeforeSave’ events
Application.EnableEvents = False
‘Disabling ScreenUpdating will increase efficiency if large files used
Application.ScreenUpdating = False
For Row = 3 To 19
SetAttr .Cells(Row, Col), vbNormal ‘ failed to understand use of SetAttr
Set wbPaste = Workbooks.Open(.Cells(Row, Col), False)
wbCopy.Sheets("Base").Range("A7:EQ500").AutoFilter
wbPaste.Sheets("Base").Range("A7:EQ500").AutoFilter
wbCopy.Sheets("Base").Range("AL8:AS8").Copy
wbPaste.Sheets("Base").Range("AL8:AS" & Cells(Rows.Count , 1).End(xlUp).Row).PasteSpecial xlPasteFormulas
Application.CutCopyMode = False
wbPaste.Close True
SetAttr .Cells(Row, Col), vbReadOnly
Next Row
Application.EnableEvents = True
Application.ScreenUpdating = True
wbCopy.Close False
wbBP.Close False
Next Col
End With
End Sub
If you want to run other events in the wbPaste workbooks and only intend to avoid running only BeforeSave events or a part of code in that event and you have access to modify the event codes, then you may resort to introduce a branch in the `BeforeSave’ events code by checking value of cell as per my comment. If apprehensive about cell value being accidentally modified/deleted by user, it is better to introduce check a ‘CustomDocumentProperties’
You may opt to add & set the custom Document Property from document panel of all the wbPaste workbooks. I would prefer to introduce the Custom Document Property BeforeSaveCheck‘ by onetime running the code
Sub testOnce()
Dim Row As Integer
Dim Col As Integer
Dim wbPaste As Workbook
Col = 4
With ThisWorkbook.Worksheets("FileList")
Application.EnableEvents = False
'Application.ScreenUpdating = False
For Row = 3 To 19
SetAttr .Cells(Row, Col), vbNormal
Set wbPaste = Workbooks.Open(.Cells(Row, Col), False)
wbPaste.CustomDocumentProperties.Add Name:="BeforeSaveCheck", LinkToContent:=False, Type:=msoPropertyTypeBoolean, Value:=True
wbPaste.Close True
SetAttr .Cells(Row, Col), vbReadOnly
Next Row
Application.EnableEvents = True
'Application.ScreenUpdating = True
End With
End Sub
Now you may introduce the a simple branch in BeforeSave events of wbPaste workbooks like
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
If ThisWorkbook.CustomDocumentProperties("BeforeSaveCheck") Then
‘’’’’’’’’’’’’
‘The code section you want to bypass while updating with macro
‘’’’’’’’’’’’
End If
End Sub
and setting the property to True at the open event
Private Sub Workbook_Open()
ThisWorkbook.CustomDocumentProperties("BeforeSaveCheck") = True
End Sub
Finally in sub dostuff
Delete lines
Application.EnableEvents = False
Application.EnableEvents = True
And add line
ThisWorkbook.CustomDocumentProperties("BeforeSaveCheck") = False
wbPaste.Close True
Related
Scenario: I have two workbooks, one of them contains VBA (WB1) code and the other (WB2) contains calls to an API for data collection (e.g. Bloomberg Add in function). When the code in WB1 is run, it starts a loop of identifiers, for each, it opens an instance of WB2, and tries to recalculate all functions. Once that is done, it copies some of the data of WB2 to WB1, saves WB2 with the identifier name, closes it and moves on to the next identifier.
Issue: As each of the API calls in WB2 take some time to process ad retrieve data, the VBA script does not wait for the functions to be calculated, it just copies the same data and moves on in the loop. Consequently, the data copied to WB1 is incorrect.
What was tried so far: I used a series of Calculate commands in VBA, also used loops to with DoEvents and tried to set up a counter in WB2 with the number of cells with data still pending calculation. In all these cases, the functions are still not fully calculated.
Obs. In this case, as this is not a specific problem to a single API (e.g. Bloomberg) the solution needs to come from a VBA command in the script of WB1.
Code so far:
Private Sub DownloadData()
Application.ScreenUpdating = True
Application.DisplayAlerts = True
' clear raw data sheets
Dim mainwb As Workbook
Set mainwb = ThisWorkbook
ThisWorkbook.Worksheets(wsRawClassData.Name).UsedRange.ClearContents
Dim wsas As Variant
wsas = Evaluate(ThisWorkbook.Names("WSATickers").Value)
' path
Dim xlsPath As String
xlsPath = Evaluate(ThisWorkbook.Names("Path").Value)
If xlsPath = "" Then
xlsPath = ThisWorkbook.Path
End If
Dim c As Integer
For c = 1 To 100
If wsas(c, 1) = "" Then Exit For
Dim objXL
Set objXL = CreateObject("Excel.Application")
Dim objXLWB
Set objXLWB = Workbooks.Open(xlsPath & "WB2.xlsm")
objXLWB.Worksheets("Data").Range("Identifier").Value = wsas(c, 1)
Application.Calculation = xlManual
Application.Calculation = xlAutomatic
'wait for initial calculations
Do While objXL.CalculationState <> xlDone
DoEvents
Loop
' Recalculation forcing:
objXLWB.Activate
Application.Calculation = xlManual
Application.Calculation = xlAutomatic
Application.CalculateFull
Application.Calculation = xlAutomatic
Dim wsobj As Variant
For Each wsobj In objXLWB.Worksheets
wsobj.Calculate
Do While objXLWB.Worksheets("Data").Range("calcpend").Value <> 0
Application.Wait (Now + TimeValue("0:00:02"))
Loop
Next wsobj
Application.Calculation = xlAutomatic
mainwb.Activate
Do Until objXLWB.Worksheets("Data").Range("calcpend").Value = 0
DoEvents
Loop
ThisWorkbook.Worksheets(wsRawData.Name).Range("A" & (c + 1)).Value = wsas(c, 1)
' save, close, quit
objXLWB.SaveAs Filename:=xlsPath & wsas(c, 1) & ".xlsm", FileFormat:=xlOpenXMLWorkbookMacroEnabled
objXLWB.Close
objXL.Quit
Next c
End Sub
It has been a while (like 20 years) since I worked with VBA and Bloomberg, but as I recall, you can make calls to the Bloomberg API directly from VBA. A quick google search of "call bloomberg api from vba" led me to this page that seems promising:
https://github.com/tebbb/VBA-Bloomberg-API-Wrapper
The solution below takes the following approach.
make a copy of the formulas in all the cells you need to wait on
clear those cells so their values are empty strings
replace the formulas causing them to need to be recalculated
use application.onTime to check every second to see if all formulas have received values
This approach assumes that the formulas that rely on add-ins to fetch data will return not return a value until they have received thier data. If this is not true, you'll need to adjust the code accordingly.
I think the key making this work for you is the use of application.onTime becuase that allows all VBA to finish running which seems to be important to allow the data to be fully retrieved.
Option Explicit
Dim formulas As New Collection
Sub wait_until_filled()
Dim x As Long
Dim range_to_check As range
Dim sheet_to_check As Worksheet
Dim workbook_to_check
Dim cell As range
' specify the workbook that holds the formulas that we want to check
Set workbook_to_check = Workbooks("Book3.xlsx")
' specify the sheet that holds the formulas that we want to check
Set sheet_to_check = workbook_to_check.Worksheets("Sheet1")
' specify the set of cells that contain the formulas we are waiting for.
Set range_to_check = sheet_to_check.range("D1:D2,E1")
' clear out old formulas in case we have had a prior run
Do Until formulas.Count = 0
formulas.Remove 1
Loop
' remember each formula, then clear it
For Each cell In range_to_check
formulas.Add Array(workbook_to_check.Name, sheet_to_check.Name, cell.Address, cell.Formula)
cell.Formula = ""
Next
' replace the formulas, causing them to recalculate
For x = 1 To formulas.Count
Workbooks(formulas(x)(0)).Worksheets(formulas(x)(1)).range(formulas(x)(2)).Formula = formulas(x)(3)
Next
'wait a second then call the sub that checks to see if the data has returned
Application.OnTime DateAdd("s", 1, Now), "wait_for_data"
End Sub
Sub wait_for_data()
Dim x As Long
'check to see of all formulas have a value
For x = 1 To formulas.Count
' this if statement assumes that a formula that relies on an addin will produce
' a blank value until it has been filled in, which may not be true.
If Workbooks(formulas(x)(0)).Worksheets(formulas(x)(1)).range(formulas(x)(2)).Value = "" Then
'we have found a cell that has not updated, check again in another second
Application.OnTime DateAdd("s", 1, Now), "wait_for_data"
Exit Sub
End If
Next
' put code here to execute once all cells have received thier values
MsgBox "all cells have received thier values"
End Sub
Suppose I have Source.xlsm( it has only one sheet file) ,it has data in that sheet. and I have 10 different destination.xlsx file in that we have multiple sheets suppose abc, efg, ijk. I have to copy whole data present in Source.xlsx file to 10 different destination excel (under sheet_name ijk).
I want to have macro code in source.xlsm when i click on the button it should copy whole data to 10 different destination files(under sheet_name ijk).
stuck for days please help me
Private Sub CommandButton1_click()
Dim wb As Workbook
Dim lRow As Long
Dim lcol As Long
Dim total As String
ThisWorkbook.Worksheets("ViewList").Select
lRow = Cells(Rows.Count, 1).End(xlUp).Row
lcol = Cells(1, Columns.Count).End(xlToLeft).Column
Range(Cells(1, 1), Cells(lRow, lcol)).Copy
Set wb = Workbooks.Open("C:\Users\Desktop\Destination.xlsx")
wb.Worksheets("abc").Activate
ActiveSheet.Paste
ActiveWorkbook.Save
ActiveWorkbook.Close savechanges = True
ThisWorkbook.Worksheets("ViewList").Activate
ThisWorkbook.Worksheets("ViewList").Cells(1, 1).Select
Application.CutCopyMode = False
End Sub
This code is pasting data in the middle of destination file i want to paste it in from cell (1,1)
I recommend the following
Private Sub CommandButton1_click()
Dim wsViewList As Worksheet
Set wsViewList = ThisWorkbook.Worksheets("ViewList")
Dim lRow As Long
lRow = wsViewList.Cells(wsViewList.Rows.Count, 1).End(xlUp).Row
Dim lcol As Long
lcol = wsViewList.Cells(1, wsViewList.Columns.Count).End(xlToLeft).Column
Dim wb As Workbook
Set wb = Workbooks.Open("C:\Users\Desktop\Destination.xlsx")
wsViewList.Range("A1", wsViewList.Cells(lRow, lcol)).Copy Destination:=wb.Worksheets("abc").Range("A1").Paste
'wb.Save 'this statement is not needed because you save on closing the workbook. Otherwise you would save twice which takes twice the time.
wb.Close SaveChanges:=True
wsViewList.Cells(1, 1).Select 'this is actually not needed unless you want to move the users view to that cell. If that is not what you need remove that line.
Application.CutCopyMode = False
End Sub
Your code would work without any .Select or .Activate statements. Also your code needs to know where exactly you want to paste so you should specify the destination cell not only the worksheet. Also it is a good practice do do the copy and past in one statemant or at least don't do any further steps between copy and paste because that can interfere with the copyied range.
Finally named parameters need to be submitted with := not with = sign. I highly recommend you to activate Option Explicit because the line
ActiveWorkbook.Close savechanges = True
does actually do the oposite of what you think:
savechanges = True because of the missing := sees savechanges as an undeclared variable of type Variant and compares if this is True. Since it is not declared and not initialized with any value the result of this statement is False.
Finally you submit that result False as first parameter to ActiveWorkbook.Close so it is the same as writing ActiveWorkbook.Close False. So what your code actually does is it closes the workbook without saving changes.
If you used Option Explicit it would have notified you that savechanges is an undeclared variable. This way this fault would have been prevented. Without that notification it is much harder to see and find that issue.
Therefore I recommend always to activate Option Explicit: In the VBA editor go to Tools › Options › Require Variable Declaration.
I've encountered a strange thing: I've joined three workbooks: Personal Data Tracker, Global Tracker and the workbook with pivots and charts. The logic is as it follows: the user clicks on a button after the work is finished so the data is copied to the GL Tracker. Once the change event is triggered in the GL Tracker Table, the last workbook opens, the pivot is refreshed upon the open vent and the wb is closed.
Everything seems to be working fine, however when I run the macro live, at the very end I get an error message about
"Application-defined or object-defined error".
Only OK and Help button displayed, it doesn't make the VBE Open so I could debug it.
Would anyone know what it may be happening even if the whole chain works fine?
Thank you.
Code from the Personal Tracker:
Sub test()
Dim path As String
Dim wb As Workbook
path = ThisWorkbook.path & "\Dest.xlsm"
Application.ScreenUpdating = False
ThisWorkbook.Sheets("Total").Range("R1").Value = Date
Range("R1").Font.Color = VBA.ColorConstants.vbWhite
Worksheets("TOTAL").Range("B2:B13").Copy
On Error GoTo Handler
Workbooks.Open (path)
On Error GoTo 0
Set wb = Workbooks("Dest")
Worksheets("Sheet1").Range("B2").PasteSpecial Paste:=xlPasteValues
Exit Sub
Handler:
MsgBox "Someone else is saving their data at the moment." & vbNewLine & _
"Please try in a few seconds"
End Sub
Code from the GL Tracker:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim MRange As Range
Dim wbPivot As Workbook
Dim pt As PivotTable
Dim ws As Worksheet
Dim Name As String
Dim answer As VbMsgBoxResult
Set MRange = ThisWorkbook.Sheets(1).Range("Table1")
Name = Application.UserName
Application.ScreenUpdating = False
If Not Intersect(Target, MRange) Is Nothing Then
Application.EnableEvents = True
Set wbPivot = Workbooks.Open("C:\Users\jakub\Desktop\Excel - various\Pivot.xlsm")
End If
'refresh
For Each ws In wbPivot.Worksheets
For Each pt In ws.PivotTables
pt.PivotCache.Refresh
pt.Update
pt.RefreshTable
Next
Next
'saving
Application.ScreenUpdating = True
If Application.UserName <> "Jakub Tracz" Then
MsgBox "User not authorised. Workbook will be closed."
wbPivot.Close True
ThisWorkbook.Close True
Else
answer = MsgBox(Prompt:="Do you want to save and close the workbook?", _
Buttons:=vbYesNo + vbQuestion)
Select Case answer
Case vbYes
wbPivot.Close True
ThisWorkbook.Close True
Case vbNo
MsgBox "Welcome, " & Application.UserName
End Select
End If
End Sub
I'm going to give you a proof of concept code as an example for you to use. This will not exactly answer your question with code you can just copy/paste, but you will be able to use this to put it together the way you want it to work instead of me making assumptions about many things and restructuring it myself.
This simply demonstrates how to use a workbook object variable in one routine that can reference another workbook, and how to make changes to that 2nd workbook and save/close it.
Sub Tracker_Update()
Dim wbPivot as Workbook
' open the workbook
Set wbPivot = Workbooks.Open("C:\Users\jakub\Desktop\Excel - various\Test.xlsx")
' optionally make it hidden
wbPivot.Visible = False
With wbPivot
' pretend this code updates the pivot table
.Worksheets(1).Range("A1") = "hello world"
' Close and save it
.Close True
End With
' optionally clear the variable
' this is not really needed in VBA, but if you eventually
' start using VB.NET with Excel as a COM object,
' you will want to know how to do this part when you are done
Set wbPivot = Nothing
End Sub
I think you will like this approach in the end much better in the end anyway, as the code isn't scattered around so much in different places. Easier to debug later, and easier for someone else to understand what you are doing if and when you leave the company.
I've created a spreadsheet for tracking student data for my wife. There are 2 versions, the master and the teacher version. The only difference is that the teacher version has a couple of tabs and buttons hidden.
At the end of every day she gathers the teacher versions and merges their data into the master version using a macro attached to a button. On a regular basis it causes Excel to crash. It seems like trying to merge a workbook that is on a usb stick is a surefire way to make it crash, but there are other circumstances that I haven't identified yet.
She isn't technical enough to step through the code until it blows and I can't be there when she is doing it.
Previous attempts to harden the code involved getting rid of any instance of Activesheet or Activeworkbook, and always using a direct reference to the worksheet (ie Sheet1, but renamed to something meaningful - "merge" in the example below).
The function below, LoadTeacherData, is called once for each workbook to merge. All it does is copy the existing records on the teacher's data tab, copy them to the master merge tab and then delete them from the source. When it crashes it is immediately after selecting the file to load, I think.
Sub LoadTeacherData()
Dim wb_td As Workbook
Dim td As Worksheet
Dim newdata As Range
With Application.FileDialog(msoFileDialogOpen)
.AllowMultiSelect = False
.Filters.Clear
.Filters.Add "Excel files", "*.xlsm"
If .Show = -1 Then
file_name = .SelectedItems(1)
Else
Exit Sub
End If
End With
Application.ScreenUpdating = False
Set wb_td = Workbooks.Open(Filename:=file_name, UpdateLinks:=False, ReadOnly:=False)
If wb_td Is Nothing Then
MsgBox "Unable to open file, check path", vbOKOnly
Exit Sub
End If
file_name = wb_td.Name
Set td = wb_td.Worksheets("data")
row = LastRow(td, "C")
col = LastCol(td, 1)
Set newdata = td.Range("a2", td.Cells(row, col))
newdata.Copy Destination:=Merge.Cells(LastRow(Merge, "C") + 1, 1)
newdata.Clear
MsgBox (row - 1 & " records merged")
wb_td.Close
ThisWorkbook.Activate
ThisWorkbook.Save
Application.ScreenUpdating = True
End Sub
Function LastRow(ByRef ws As Worksheet, ByVal colname As String)
LastRow = ws.Range(colname & ws.Rows.Count).End(xlUp).row
End Function
Function LastCol(ByRef ws As Worksheet, ByVal rownum As Long)
LastCol = ws.Cells(rownum, ws.Columns.Count).End(xlToLeft).Column
End Function
My suspicion is that it has something to do with permissions when opening the file. Her excel version is configured so that she has to enable content every time when opening a file.
You may be dealing with a corrupted file. Look at the workbooks.open method. There are a few options under corruptload that may help. Try this one and experiment to see if any of the others work better in your situation.
Set wb_td = Workbooks.Open(Filename:=file_name, UpdateLinks:=False, ReadOnly:=False, corruptload:=xlRepairFile)
I have an Excel sheet that gets data from another closed Excel sheet. I managed to get the code working as follows when placed in 'ThisWorkbook' in my 'VBAProject':
Option Explicit
Private Sub Workbook_Open()
Call ReadDataFromCloseFile
End Sub
Sub ReadDataFromCloseFile()
On Error GoTo ErrHandler
Application.ScreenUpdating = False
Dim src As Workbook
' OPEN THE SOURCE EXCEL WORKBOOK IN "READ ONLY MODE".
Set src = Workbooks.Open("Source file path", True, True)
' GET THE TOTAL ROWS FROM THE SOURCE WORKBOOK.
Dim iTotalRows As Integer
iTotalRows = src.Worksheets("data entry").Range("A1:A" & Cells(Rows.Count, "A").End(xlUp).Row).Rows.Count
' COPY DATA FROM SOURCE (CLOSE WORKGROUP) TO THE DESTINATION WORKBOOK.
Dim iCnt As Integer ' COUNTER.
For iCnt = 1 To iTotalRows
Worksheets("display").Range("A1:W1" & iCnt).Formula = src.Worksheets("data entry").Range("A1:W1" & iCnt).Formula
Next iCnt
' CLOSE THE SOURCE FILE.
src.Close False ' FALSE - DON'T SAVE THE SOURCE FILE.
Set src = Nothing
ErrHandler:
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
Previously I have a button to trigger the code. Not only that it can auto update when I first open the file. Now I would like to add a timer function to my code so that it can refresh the data frequently and automatically.
So I added a timer to my Workbook_Open function as follows:
Private Sub Workbook_Open()
Application.OnTime Now + TimeValue("00:01:00"), "ReadDataFromCloseFile"
End Sub
However when I let the timer run I receive the message saying I cannot run the macro because it may not be available in workbook or all macros is disabled.
Clearly my timer has issues, but I can't figure out why. I even placed the time into the main sub but still no dice. Any suggestions?