Private sub update date automatically when value in a cell changes - excel

Im trying to automatically update current date in cell T when text in cell Q is "won" and a value in cell AM is > 0. I tried the code below and it is working if first the value in cell is > 0 and then you update the text in cell Q BUT if you do it in another way (first update cell Q and secondly the value in cell AM) the date doesn't appear in cell T.
Any idea, what Im I missing?
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, [Q:Q]) Is Nothing Then
If UCase(Target) = UCase("won") And Target.Offset(, 22) > 0 Then
Target.Offset(, 2) = Int(Now())
End If
End If
End sub

Your code only checks for changes in Q therefore the update does not take place if you change AM first.
My solution has three parts:
use constants for the columns - in case there are changes to the sheet layout you only have to make adjustments here
worksheet_change: only check if one of the columns is affected then call the according sub - by that the reader of the code immediately understands what is going on here
the main routine that inserts the date if condition is met or removes the date if not (maybe you want to adjust this)
Option explicit
Private Const colStatus As String = "Q"
Private Const colValue As String = "AM"
Private Const colDateWon As String = "S"
Private Sub Worksheet_Change(ByVal Target As Range)
Dim c As Range
Set c = Target.Cells(1, 1)
If c.Column = Me.Columns(colStatus).Column Or c.Column = Me.Columns(colValue).Column Then
updateDateWon c.row
End If
End Sub
Private Sub updateDateWon(row As Long)
'--> adjust the name of the sub to your needs
Dim valueToInsert As Variant
With Me
If .Range(colStatus & row) = "won" And .Range(colValue & row) > 0 Then
valueToInsert = Int(Now)
Else
'reset the date in case conditions are not met
valueToInsert = vbNullString
End If
Application.EnableEvents = False 'disable events so that change-event isn't called twice
.Range(colDateWon & row) = valueToInsert
Application.EnableEvents = True
End With
End Sub

A Worksheet Change Applied to Two Non-Adjacent Columns
You need to monitor columns Q and AM for changes.
You need to account for Target being multiple adjacent and non-adjacent cells.
You need to disable events when writing to the worksheet containing this code to not retrigger this event (or trigger any other events).
It is good practice to ensure the re-enabling of events (by using error-handling).
You can combine the cells to be written to (dCell) into a range (drg) and write the stamp in one go.
Int(Now()) or Int(Now) is actually Date.
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
On Error GoTo ClearError
Const sColsAddress As String = "Q:Q,AM:AM"
Const dCol As String = "T"
Const fRow As Long = 2 ' (e.g. 2 for excluding headers in the first row)
Const sCriteria As String = "won"
Dim srg As Range
With Range(sColsAddress)
Set srg = Intersect(.Cells, Rows(fRow).Resize(Rows.Count - fRow + 1))
End With
Dim sirg As Range: Set sirg = Intersect(srg, Target)
If sirg Is Nothing Then Exit Sub
Dim sirg1 As Range: Set sirg1 = Intersect(sirg.EntireRow, srg.Areas(1))
Dim siCol2 As Long: siCol2 = srg.Areas(2).Column
'Dim dirg As Range: Set dirg = sirg1.EntireRow.Columns(dCol) ' not used
Dim siCell1 As Range
Dim siValue2 As Variant
Dim drg As Range
For Each siCell1 In sirg1.Cells
If StrComp(CStr(siCell1.Value), sCriteria, vbTextCompare) = 0 Then
siValue2 = siCell1.EntireRow.Columns(siCol2).Value
If IsNumeric(siValue2) Then
If siValue2 > 0 Then
If drg Is Nothing Then
Set drg = siCell1.EntireRow.Columns(dCol)
Else
Set drg = Union(drg, siCell1.EntireRow.Columns(dCol))
End If
End If
End If
End If
Next siCell1
If Not drg Is Nothing Then
' Prevent retriggering the event when writing to the worksheet.
Application.EnableEvents = False
drg.Value = Now ' only after testing, use 'dDate = Date'
End If
SafeExit:
' Enable events 'at all cost'.
If Not Application.EnableEvents Then Application.EnableEvents = True
Exit Sub
ClearError:
Debug.Print "Run-time error '" & Err.Number & "': " & Err.Description
Resume SafeExit
End Sub

Related

Automatically show date when particular result detected from Excel formula

I am trying with VBA to get the current date on column H (Date).
In column F (Result)
If I manually type Preferred or Non-preferred.
After pressing Enter, today's date will be automatically put on column H (Date)
When I paste formula instead (which will consider data from column A-E to show result on its cell).
Even if the result gives Preferred or Non-preferred the date will not automatically show up.
Unless I press double-click and enter on each result cell then it will show up.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim c As Range
Dim St As String
St = "Preferred|Non-Preferred"
If Not Intersect(Columns("F"), Target) Is Nothing Then
Application.EnableEvents = False
For Each c In Intersect(Columns("F"), Target).Cells
If InStr(1, St, c.Value, vbTextCompare) >= 1 Then
Cells(c.Row, "H").Value = Date
Else
If IsEmpty(c) Then Cells(c.Row, "H").Value = ""
End If
Next c
Application.EnableEvents = True
End If
End Sub
Example
A Worksheet Change: Using Precedents
In column F is a formula that calculates dependent on the values of columns A:E (precedents). The code will trigger when the values in columns A:E are manually changed, possibly modifying the value in column H (see OP's description (requirements)).
The Select Case statement could be reduced to two cases: either it begins with Criteria (1) or it doesn't (Else).
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
On Error GoTo ClearError
Const sFirstCellAddress As String = "F2"
Const dCol As String = "H"
Const Criteria As String = "Preferred"
Dim srg As Range
With Me.Range(sFirstCellAddress)
Set srg = .Resize(Me.Rows.Count - .Row + 1)
End With
Dim irg As Range: Set irg = Intersect(srg.Precedents, Target)
If irg Is Nothing Then Exit Sub
Set srg = Intersect(irg.EntireRow, Columns(srg.Column))
Dim drg As Range: Set drg = srg.EntireRow.Columns(dCol)
Application.EnableEvents = False
Dim dCell As Range
Dim n As Long
For Each dCell In drg.Cells
n = n + 1
Select Case InStr(1, CStr(srg.Cells(n).Value), Criteria, vbTextCompare)
Case 0
dCell.Value = Empty ' criteria not found
Case 1
dCell.Value = Date ' begins with criteria
Case Else
dCell.Value = Empty ' contains criteria
End Select
Next dCell
SafeExit:
If Not Application.EnableEvents Then Application.EnableEvents = True
ProcExit:
Exit Sub
ClearError:
Debug.Print "Run-time error '" & Err.Number & "': " & Err.Description
Resume SafeExit
End Sub

VBA Code to repeat a function in rows 2-100

I have little experience with VBA code and I am trying to get this code to repeat for rows 2-100. The issue I have found vs other codes that repeat in rows is that mine has multiple end arguments and I'm not sure how to account for this. Any help is greatly appreciated.
Private Sub Worksheet_SelectionChange2(ByVal Target As Range)
a = Date
b = 2
If Cells(b, 3).Value <> Blank Then
If Cells(b, 2).Value = Blank Then
Cells(b, 2).Value = a
Exit Sub
End If
If Cells(b, 2).Value < a Then
Exit Sub
End If
Cells(b, 2).Value = a
End If
End Sub
This is what I'm working with. I tried to make the cell reference a variable that I could count up but whatever I tried it didn't work.
Edit: Sorry for lack of clarification. The code is supposed to put today's date in B2 when C2 goes from being empty to having anything in it. It also prevents the date from being changed if there is already a date there, even if C2 is cleared. I am trying to extend it so that rather than just C2 and B2 it is C2-C100 and then corresponding B2-B100.
Edit 2: C2 is being changed by a manual input. The purpose is to have someone input data into C2 (and the rest of the row) and for the date to be automatically entered and locked so they cannot change it and I can see when the data was inputted.
A Worksheet Change (Timestamp)
This will only work if the values in column C are modified manually i.e. by manually entering, by copy/pasting, and by writing via VBA.
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
On Error GoTo ClearError
Const fRow As Long = 2
Const sCol As String = "C"
Const dCol As String = "B"
Dim scrg As Range: Set scrg = Columns(sCol).Resize(Rows.Count - fRow + 1)
Dim srg As Range: Set srg = Intersect(scrg, Target)
If srg Is Nothing Then Exit Sub
Dim drg As Range
Dim dCell As Range
Dim sCell As Range
For Each sCell In srg.Cells
Set dCell = sCell.EntireRow.Columns(dCol)
If Len(CStr(dCell.Value)) = 0 Then
If drg Is Nothing Then
Set drg = dCell
Else
Set drg = Union(drg, dCell)
End If
End If
Next sCell
' All cells already contain a date.
If drg Is Nothing Then Exit Sub
Dim dDate As Date: dDate = Now ' after prooving that it works, use 'Date'
' To prevent retriggering this event and any other event while writing.
Application.EnableEvents = False
' Write in one go.
drg.Value = dDate
SafeExit:
' Enable events 'at any cost'.
If Not Application.EnableEvents Then Application.EnableEvents = True
Exit Sub
ClearError:
Debug.Print "Run-time error '" & Err.Number & "': " & Err.Description
Resume SafeExit
End Sub

How can I get this Excel VBA to run for all rows, instead of just the first row?

So darn new to VBA, almost got this working. I'm trying to reset some Data Validation lists so that if someone changes a Country selection, then a couple of cells will reset. Example, if I pick USA then I want the corresponding State and Shift column to display "Please select..." and if the user changes the country to something other than USA then it doesn't say anything in the State column, only in the Shift column. I got this working but it only runs for the first row. I'm not sure if my range is wrong or if I'm supposed to loop, both of which I'm totally ignorant on.
Option Explicit
'The way this works is if the Payroll Country changes then the sub selections
'of State and Shift should reset based on if the country is USA
'Payroll Country = A column
'State = X column
'Shift = Y column
Private Sub Worksheet_Change(ByVal Target As Range)
'"If Target.Count > 1 Then Exit Sub" is the VBA code to prevent an error if user highlights the range and deletes the data
If Target.Count > 1 Then Exit Sub
If Target.Address = "$A$6" And Target.Value = "USA" Then
Range("X6").Value = "Please select..."
Range("Z6").Value = "Please select..."
ElseIf Target.Address = "$A$6" And Target.Value <> "USA" Then
Range("X6").Value = ""
Range("Z6").Value = "Please select..."
End If
End Sub
Try this:
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.CountLarge > 1 Then Exit Sub 'CountLarge handles larger ranges...
'check Target column and row...
If Target.Column = 1 and Target.Row >= 6 Then
With Target.EntireRow
.Columns("X").Value = IIf(Target.Value = "USA", _
"Please select...", "")
.Columns("Z").Value = "Please select..."
End With
End If
End Sub
A Worksheet Change
If you out-comment the line Application.EnableEvents = False you will notice that there will be more Debug.Print lines (one more for USA, two more for non-USA) i.e. after writing (crg.Value = InitialString), the code calls itself (actually the worksheet change event calls the InitializeCountry procedure) again but 'luckily' exits after the line If irg Is Nothing Then Exit Sub since it is writing to a non-intersecting range, but nevertheless debug-printing the Source Range Address once more, before it continues with the line Debug.Print "Criteria Range (Both Columns): " & crg.Address(0, 0). This should help you to fully understand why the disabling of events is necessary.
Out-comment or delete the range addresses related Debug.Print lines when done testing because they're slowing down the code.
Sheet Module e.g. Sheet1
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
InitializeCountry Target
End Sub
Standard Module e.g. Module1
Option Explicit
Sub InitializeCountry( _
ByVal Target As Range)
' Declare constants.
Const SourceFirstCellAddress As String = "A6"
Const StateColumn As String = "X"
Const ShiftColumn As String = "Z"
Const CriteriaString As String = "USA"
Const InitialString As String = "Please select..."
' Read.
Dim ws As Worksheet: Set ws = Target.Worksheet
Dim sfCell As Range: Set sfCell = ws.Range(SourceFirstCellAddress)
Dim srg As Range: Set srg = sfCell.Resize(ws.Rows.Count - sfCell.Row + 1)
Debug.Print "Source Range Address: " & srg.Address(0, 0)
Dim irg As Range: Set irg = Intersect(srg, Target) ' Intersect Range
If irg Is Nothing Then Exit Sub
Debug.Print "Intersect Range Address: " & irg.Address(0, 0)
' ShiftColumn
Dim crg As Range ' Criteria Range
Set crg = irg.EntireRow.Columns(ShiftColumn)
Debug.Print "Criteria Range (ShiftColumn): " & crg.Address(0, 0)
' StateColumn
Dim erg As Range ' Empty Range
Dim iCell As Range ' Current Intersect Cell
For Each iCell In irg.Cells
If iCell.Value = CriteriaString Then
Set crg = Union(crg, iCell.EntireRow.Columns(StateColumn))
Else
If erg Is Nothing Then
Set erg = iCell.EntireRow.Columns(StateColumn)
Else
Set erg = Union(erg, iCell.EntireRow.Columns(StateColumn))
End If
End If
Next iCell
' Write.
Application.ScreenUpdating = False
' This is crucial to not retrigger the event procedure when writing!
Application.EnableEvents = False
On Error GoTo ClearError
crg.Value = InitialString
Debug.Print "Criteria Range (Both Columns): " & crg.Address(0, 0)
If Not erg Is Nothing Then
erg.ClearContents ' erg.Value = Empty
Debug.Print "Empty Range (StateColumn): " & erg.Address(0, 0)
End If
SafeExit:
Application.EnableEvents = True
Application.ScreenUpdating = True
Exit Sub
ClearError:
' Don't uncomment this line!
Debug.Print "Run-time error '" & Err.Number & "': " & Err.Description
Resume SafeExit
End Sub
' Test multi-range (only possible with VBA).
Sub InitializeCountryTEST()
Dim rg As Range: Set rg = Range("A6:A20,A31:A50,A61:A100")
rg.Value = "USA"
End Sub
' Debug.Print result for 'USA' (no 'Empty Range'):
'Source Range Address: A6:A1048576
'Intersect Range Address: A6:A20,A31:A50,A61:A100
'Criteria Range(ShiftColumn): Z6:Z20
'Criteria Range (Both Columns): Z6:Z20,X6:X20,X31:X50,X61:X100
' Debug.Print result otherwise:
'Source Range Address: A6:A1048576
'Intersect Range Address: A6:A20,A31:A50,A61:A100
'Criteria Range(ShiftColumn): Z6:Z20
'Criteria Range (Both Columns): Z6:Z20
'Empty Range (StateColumn): X6:X20,X31:X50,X61:X100

Execute Procedure when Value in a Cell/Range Changes

I'm new to VBA and wrote the following codes according to my data set. The goal here is to execute my procedure if a cell/range gets changed by pasting new data into the worksheet, most probably the sheet will be empty as it will follow by a clear content procedure.
However, the code is not triggering the change event, I've tried several codes from Google, but none of them worked. Please note that my procedure gets me exactly the data I want in the format I want, however, if changes are needed, kindly let me know.
PLEASE HELP
1. Change event trigger - stored under Sheet1
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Me.Range("A1")) Is Nothing Then
Application.EnableEvents = False
Call LoopandIfStatement
Application.EnableEvents = True
End If
End Sub
2. My procedure - stored under Sheet1 below the event above
Sub LoopandIfStatement()
Dim SHT As Worksheet
Set SHT = ThisWorkbook.Worksheets("CB")
MyLr = SHT.Cells(Rows.Count, 1).End(xlUp).Row
Dim I As Long
For I = 1 To MyLr
Dim O As Long
Dim U As Range
Set U = SHT.Range("A" & I)
If IsEmpty(SHT.Range("a" & I).Value) = False Then
SHT.Range("k" & I).Value = SHT.Range("A" & I).Value
Else
On Error GoTo ABC
SHT.Range("k" & I).Value = U.Offset(-1, 0)
End If
Next I
For O = 2 To MyLr
If SHT.Range("g" & O).Value = "Closing Balance" Then
SHT.Range("l" & O).Value = SHT.Range("j" & O).Value
End If
Next O
ABC:
End Sub
Results
This will trigger whenever new data is pasted in any cell of columns A to J
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Me.Range("A:J")) Is Nothing Then
Application.EnableEvents = False
Call LoopandIfStatement
Application.EnableEvents = True
End If
End Sub
Regarding your sub LoopandIfStatement here are some suggestions:
Use Option explicit at the top of your modules (see this)
Declare all your variables (you're missing: Dim MyLr as long)
Try to name your variables to something understandable (e.g. instead of MyLr you could have lastRow)
If you need to exit a Sub you can use Exit Sub instead of a Goto ABC
EDIT:
Added code for the loop and the change worksheet event.
Paste it behind the CB Sheet module
Some highlights:
When you triggered the loop on each worksheet change, it would re-apply all the steps to all the cells. You can work with changed ranges using the Target argument/variable in the Worksheet_Change event
To loop through an existing range see the AddAccountBalanceToRange procedure
Try to think and plan your code in steps or actions that can be grouped
Use comments to describe the purpose of what you're doing
Remember to delete obsolete code (saw you had a copy of the procedure in a module)
Option Explicit
Private Sub CommandButton1_Click()
ThisWorkbook.Worksheets("Data").Columns("A:J").Copy
ThisWorkbook.Worksheets("CB").Range("A:J").PasteSpecial Paste:=xlPasteValues
End Sub
Private Sub CommandButton2_Click()
ThisWorkbook.Worksheets("CB").Range("A:L").ClearContents
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
Dim targetUsedRange As Range
' Do something on non empty cells
Set targetUsedRange = Intersect(Target, Target.Parent.UsedRange)
If Not Intersect(Target, Me.Range("A:J")) Is Nothing Then
Application.EnableEvents = False
Call AddAccountBalance(targetUsedRange)
Application.EnableEvents = True
End If
End Sub
Private Sub AddAccountBalance(ByVal Target As Range)
Dim targetSheet As Worksheet
Dim evalRow As Range
Dim lastColumn As Long
Dim accountNumber As String
Dim balanceString As String
Dim narrative As String
Dim balanceValue As Long
balanceString = "Closing Balance"
' If deleting or clearing columns
If Target Is Nothing Then Exit Sub
' Do something if there are any values in range
If Application.WorksheetFunction.CountA(Target) = 0 Then Exit Sub
' Get the parent sheet of the cells that were modifid
Set targetSheet = Target.Parent
' Get the last empty cell column in row 1 -Cells(3 -> this is row 3)- In the sample book: column K
lastColumn = targetSheet.Cells(3, targetSheet.Columns.Count).End(xlToLeft).Column
' Loop through each of the rows that were modified in range
For Each evalRow In Target.Cells.Rows
' Do something if account number or narrative are not null
If targetSheet.Cells(evalRow.Row, 1).Value <> vbNullString Or targetSheet.Cells(evalRow.Row, 7).Value <> vbNullString Then
' Store columns values in evaluated row
accountNumber = targetSheet.Cells(evalRow.Row, 1).Value
narrative = targetSheet.Cells(evalRow.Row, 7).Value
If IsNumeric(targetSheet.Cells(evalRow.Row, 10).Value) Then balanceValue = targetSheet.Cells(evalRow.Row, 10).Value
' Add account number
If accountNumber <> vbNullString Then
targetSheet.Cells(evalRow.Row, lastColumn).Value = accountNumber
End If
' Add closing balance
If narrative = balanceString Then
targetSheet.Cells(evalRow.Row, lastColumn).Value = targetSheet.Cells(evalRow.Row, 1).Offset(-1, 0).Value
targetSheet.Cells(evalRow.Row, lastColumn).Offset(0, 1).Value = balanceValue
End If
' Format last two columns (see how the resize property takes a single cell and expands the range)
With targetSheet.Cells(evalRow.Row, lastColumn).Resize(, 2).Interior
.ThemeColor = xlThemeColorAccent6
.TintAndShade = 0.799981688894314
.PatternTintAndShade = 0
End With
' Auto fit last column (K) (you could use the resize property as in the previous statement)
targetSheet.Columns(lastColumn).EntireColumn.AutoFit
End If
Next evalRow
End Sub
Public Sub AddAccountBalanceToRange()
Dim targetSheet As Worksheet
Dim evalRange As Range
Set targetSheet = ThisWorkbook.Worksheets("CB")
Set evalRange = targetSheet.Range("A1:A42")
AddAccountBalance evalRange
End Sub

worksheet change event only works when region selected - how to adjust to automatic update

the combination of this sub in a module
Sub hithere3()
Dim Rng As Range
Dim Unique As Boolean
For Each Rng In Worksheets("Sheet8").Range("FS3:FS30") 'for each cell in your B1 to B30 range, sheet1
Unique = True 'we'll assume it's unique
Lastunique = Worksheets("TRADES").Range("C:C").Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
For i = 3 To Lastunique 'for each cell in the unique ID cache
If Rng.Value = Worksheets("TRADES").Cells(i, 3).Value Then 'we check if it is equal
Unique = False 'if yes, it is not unique
End If
Next
If Unique Then Worksheets("TRADES").Cells(Lastunique + 1, 3) = Rng 'adds if it is unique
Next
End Sub
with the loop check in a worksheet change events
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Intersect(Target, Range("FS3:FS33")) Is Nothing Then
'Do nothing '
Else
Call hithere3
End If
End Sub
works except it only updates when I select one of the cells in FS3:FS33
Can anyone suggest how this can be overcome?
maybe with a workchange change range selection type from below?
Private Sub Worksheet_Change(ByVal Target As Range)
Dim Rng As Range, Dn As Range, n As Long
Dim RngB As Range, RngC As Range
If Target.Column = 2 And Target.Count = 1 And Target.Row > 1 Then
With CreateObject("scripting.dictionary")
.CompareMode = vbTextCompare
Set RngB = Range(Range("B2"), Range("B" & Rows.Count).End(xlUp))
Set RngC = Range(Range("C2"), Range("C" & Rows.Count).End(xlUp))
ray = Array(RngB, RngC)
For n = 0 To 1
For Each Dn In ray(n)
If Not Dn.Address(0, 0) = "C1" And Not Dn.Value = "" Then
.Item(Dn.Value) = Empty
End If
Next Dn
Next n
Range("C2").Resize(.Count) = Application.Transpose(.Keys)
End With
End If
Use either the worksheet Calculate event or the worksheet Change event:
use Calculate if the range contains formulas
use Change if the cells in the range are changed manually
If Intersect(Target, Range("FS3:FS33")) Is Nothing is the culprit. You must change Range("FS3:FS33") to whatever range you want to affect this change.
Private Sub Worksheet_Change(ByVal Target As Range) '<<delete the "Selection" from the name of event
If Intersect(Target, Range("FS3:FS33")) Is Nothing Then
'Do nothing '
Else
Call hithere3
End If
End Sub
Finally figured it out, the following code works :
Private Sub Worksheet_calculate()
If Range("FS3:FS33") Is Nothing Then
'Do nothing'
Else
Call hithere3
End If
End Sub

Resources