Excel VBA Macro - Can it be simplified or structured differently? - excel

I made a simple VBA macro that I run against a CSV file that I open in excel. This macro formats the sheet, deletes certain data, inserts columns, etc. It then copies the properly formatted CSV to a server where the data is imported into our ERP. The CSV file is a Bill of Material and everything works great. I am wondering if it could be simplified. When I import this macro as an excel add-in, instead of showing one macro, it shows all the various sub-routines within the macro, along with the main sub that calls all the other subs in the order I need them to run. Is there a better way to arrange this code?
Sub ProcessBOM()
Call DeleteColumn
Call DelBinFill
Call DelBlankRows
Call Insert3Columns
Call DelRow1
Call ClearColumns
Call InsertProjectName
Call InsertLineItemNo
Call InsertEA
Call MoveColumn
Call InsertDate
Call GetUserName
Call SaveAs
Call MessageBox
End Sub
'Delete first column
Sub DeleteColumn()
Columns(1).EntireColumn.Delete
End Sub
'Delete rows containing BIN FILL
Sub DelBinFill()
Dim i As Integer
For i = Cells(Rows.Count, 1).End(xlUp).Row To 1 Step -1
If Cells(i, 1) = "BIN FILL" Then Cells(i, 1).EntireRow.Delete
Next i
End Sub
'Delete rows with blank RDI Item #
Sub DelBlankRows()
Dim i As Integer
For i = Cells(Rows.Count, 1).End(xlUp).Row To 1 Step -1
If Cells(i, 1) = "" Then Cells(i, 1).EntireRow.Delete
Next i
End Sub
'Insert 3 blank columns
Sub Insert3Columns()
Range("A:C").EntireColumn.Insert
End Sub
'Delete Row 1
Sub DelRow1()
Rows(1).EntireRow.Delete
End Sub
'Clear Contents of specified columns
Sub ClearColumns()
Range("E:G").EntireColumn.Clear
End Sub
'Grabs Project Name from Active Sheet and inserts to last row
Sub InsertProjectName()
Dim LastRow As Long
LastRow = Range("D" & Rows.Count).End(xlUp).Row
Range("C1:C" & LastRow) = ActiveSheet.Name
End Sub
'Insert Line Item Numbers
Sub InsertLineItemNo()
ActiveCell.FormulaR1C1 = "1"
LastRow = Range("D" & Rows.Count).End(xlUp).Row
Selection.AutoFill Destination:=Range("A1:A" & LastRow), Type:=xlFillSeries
End Sub
'Insert EA Into Column E
Sub InsertEA()
LastRow = Range("D" & Rows.Count).End(xlUp).Row
Range("E1:E" & LastRow) = "EA"
End Sub
' Moves QTY Data from H to F
Sub MoveColumn()
Columns("H:H").Select
Selection.Cut Destination:=Columns("F:F")
Columns("F:F").Select
End Sub
'Insert Date Into Column G
Sub InsertDate()
Dim LDate As String
LDate = Date
LastRow = Range("D" & Rows.Count).End(xlUp).Row
Range("G1:G" & LastRow).Resize(, 2) = Array(Date, "=""""")
End Sub
'Get logged on username and insert into Column B
Sub GetUserName()
Dim strName As String
strName = Environ("UserName")
LastRow = Range("D" & Rows.Count).End(xlUp).Row
Range("B1:B" & LastRow) = strName
End Sub
'Save file
Sub SaveAs()
Application.DisplayAlerts = False
MyName = ActiveSheet.Name
ActiveWorkbook.SaveAs Filename:="\\navapp1svr\boms$\solidworks\inbound" & "\" & MyName & ".csv", FileFormat:=xlText
ActiveWorkbook.Saved = True
ActiveWorkbook.Close SaveChanges:=False
End Sub
'Prompt the user to verify data upload in Microsoft Dynamics NAV
Sub MessageBox()
MsgBox ("BOM upload complete. Please check Dynamics for accuracy.")
End Sub

I think this is primarily opinion based, but I have a strong opinion here so I'm sharing it. I feel like your code is way over-refactored and there is some extra superfluous stuff in here (variables being set but never used, .SELECT being used to copy/paste, variables declared and set and then only used once)
Consider a single routine:
Sub ProcessBOM()
Dim i As Integer
'Delete first column
Columns(1).EntireColumn.Delete
'Delete rows containing BIN FILL or Nothing
For i = Cells(Rows.Count, 1).End(xlUp).Row To 1 Step -1
If Cells(i, 1) = "BIN FILL" OR Cells(i, 1) = "" Then Cells(i, 1).EntireRow.Delete
Next i
'Insert 3 blank columns
Range("A:C").EntireColumn.Insert
'Delete Row 1
Rows(1).EntireRow.Delete
'Clear Contents of specified columns
Range("E:G").EntireColumn.Clear
'Define last used row
Dim LastRow As Long
LastRow = Range("D" & Rows.Count).End(xlUp).Row
'Grabs Project Name from Active Sheet and inserts to last row
Range("C1:C" & LastRow) = ActiveSheet.Name
'Insert Line Item Numbers
'What is this. How do you know what the "ActiveCell" is at this point or what is "Selected"
'Commenting out because this is risky. Explicitly set which cells you want to do this to
'ActiveCell.FormulaR1C1 = "1"
'Selection.AutoFill Destination:=Range("A1:A" & LastRow),Type:=xlFillSeries
'Insert EA Into Column E
Range("E1:E" & LastRow) = "EA"
' Moves QTY Data from H to F
Columns("H:H").Cut Destination:=Columns("F:F")
'Insert Date Into Column G
Range("G1:G" & LastRow).Resize(, 2) = Array(Date, "=""""")
'Get logged on username and insert into Column B
Range("B1:B" & LastRow) = Environ("UserName")
'Save file
Application.DisplayAlerts = False
ActiveWorkbook.SaveAs Filename:="\\navapp1svr\boms$\solidworks\inbound" & "\" & ActiveSheet.Name & ".csv", FileFormat:=xlText
ActiveWorkbook.Saved = True
ActiveWorkbook.Close SaveChanges:=False
'Prompt the user to verify data upload in Microsoft Dynamics NAV
MsgBox ("BOM upload complete. Please check Dynamics for accuracy.")
End Sub
It's only 54 rows including comments and whitespace . In fact, it's only 23 lines of actual code. It's very clear what each step is doing and it can be read by a human without bouncing from the top routine down to whatever step is next. Your getting really close to spaghetti-code and you don't want to go there.
Expanding this out into 15 subroutines doesn't really make sense as they don't really encapsulate much more than a line or two of code and they aren't terribly reusable as they all do a VERY specific thing to a specific range that is only applicable at a single point-in-time while your code is running. If you have more code that may need to reuse some of the code that is present here, then MAYBE consider separating out the logic into it's own subroutine.
There are some pieces that might make sense as their own subroutine or function. For instance you have two routines that are similar DelBinFill and DelBlankRows. These could be written as a single routine with a parameter:
Sub DelRows(criteria As String)
Dim i As Integer
For i = Cells(Rows.Count, 1).End(xlUp).Row To 1 Step -1
If Cells(i, 1) = criteria Then Cells(i, 1).EntireRow.Delete
Next i
End Sub
And called like:
Call DelRows("Bin Fill")
Call DelRows("")
But... now you have to loop through the same range twice and delete rows. It would be MUCH more efficient to loop once (as I do above) and delete based on both criteria.

Related

Delete cells depend on thier values works fine but skipped the half

I need to delete 2 or more (variable depending on work) cells in the same row starting from row 2 if the 2 cells are = ""
I used this code and it's already working fine except 1 problem
Sub Macro3()
Dim s As Integer
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet2")
For s = 2 To 22
If ws.Range("G" & (s)) = "" And ws.Range("H" & (s)) = "" Then
Union(ws.Range("G" & s), ws.Range("H" & s)).Select
Selection.Delete Shift:=xlUp
End If
Next s
End Sub
the problem is if I have for example from G2:H4 (2rows or more achieve the if condition) it's only delete half of them,
if 5 rows delete 3 only...etc
so I think the loop doesn't operate on the current cell (just guessing)
Attach screens is before and after running the code for more clarification
before
after
Sub Macro3()
Dim s As Long
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet2")
For s = 22 To 2 Step -1
If ws.Range("G" & s).Value = "" And ws.Range("H" & s).Value = "" Then
ws.Rows(s).Delete Shift:=xlUp
'or:
'ws.Range("G" & s & ":H" & s).Delete Shift:=xlUp
End if
Next s
End Sub
After running the code:

Copy row of data based on criteria AND "label" that copied data in last column

I have working code that checks for a criteria in each row, and if met, copies that whole row of data over to a different workbook. But! I need to be able to add text to the last column of the copied data (Column S) that essentially labels what criteria was met that made the code copy it over because I will soon be expanding to check for multiple different criteria.
So for every row that meets the criteria and gets copied, I want to add "Criteria1" next to it in column S in the new workbook (it will always be column S that will be the first available column).
I have mangled this code together through inheritance and all of your help, so I don't really even know where to begin.
Private Sub Workbook_AfterSave(ByVal Success As Boolean)
Dim CoderBook As Workbook
Dim Referrals As Worksheet
Dim Review As Workbook
Dim Crit As Worksheet
Dim LastRow As Long
Dim NextRow As Long
Dim i As Long
Set CoderBook = Workbooks.Open("Coder Referrals.xlsx")
Set Referrals = CoderBook.Sheets("Sheet1")
Set Review = ThisWorkbook
Set Crit = Review.Sheets("Criteria")
'Search code
LastRow = Crit.Cells.Find(What:="*", After:=[A1], SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
Application.ScreenUpdating = False
'Loop search code
For i = 2 To LastRow
'Specialized Criteria1 Check
If Crit.Range("F" & i) <> Crit.Range("G" & i) Or _
Crit.Range("I" & i) <> Crit.Range("J" & i) Then
'If meets Criteria1 check, then copy appropriate rows to CoderBook Referrals sheet
Referrals.Cells(Rows.Count, 1).End(xlUp).Offset(1, 0).EntireRow.Value = Crit.Rows(i).Value
End If
Next i
'End loop code
CoderBook.Close SaveChanges:=True
Application.ScreenUpdating = True
End Sub
Split the or into two statements:
For i = 2 To LastRow
j = Referrals.Cells(Rows.Count, 1).End(xlUp).row + 1
'Specialized Criteria1 Check
If Crit.Range("F" & i) <> Crit.Range("G" & i) Then
'If meets Criteria1 check, then copy appropriate rows to CoderBook Referrals sheet
Referrals.Rows(j).EntireRow.Value = Crit.Rows(i).Value
Referrals.Range("S" & j).Value = "Criteria1"
End If
If Crit.Range("I" & i) <> Crit.Range("J" & i) Then
Referrals.Rows(j).EntireRow.Value = Crit.Rows(i).Value
if Referrals.Range("S" & j).value = vbNullString then
Referrals.Range("S" & j).Value = "Criteria2"
Else
Referrals.Range("S" & j).Value = Referrals.Range("S" & j).Value & ", " & "Criteria2"
End if
Next i

Delete row if cells equal a set of values

I created a macro to in order to generate a daily report. The portion of the macro that finds a value in column AN and deletes the entire row (code edited to delete rows starting from the last used row), works well.
The following example deletes all the rows that do not contain the value "CAT","BAT", or "DOG in column AN.
'False screen updating
Application.ScreenUpdating = False
'deleting all other types other than CAT from "samples" tab (excluding the header row, row 1)
Sheets("sample").Select
Lastrow = Cells(Rows.Count, "AN").End(xlUp).Row
'Deleting rows from bottom up
For i = Lastrow To 2 Step -1
If Range("AN" & i).Value <> "CAT" And _
Range("AN" & i).Value <> "BAT" And _
Range("AN" & i).Value <> "DOG" Then
Rows(i).EntireRow.Delete
End If
Next i
However, would like to create another Sub that deletes all the rows that do contain a specific set of values.
I tried replacing <> with = and ==, however neither worked and no rows were deleted
Below is a sample how to delete rows based on a criteria in column A. Keep in mind that if we delete rows we go backwards to avoid index errors.
Try:
Option Explicit
Sub test()
Dim Lastrow As Long, i As Long
With ThisWorkbook.Worksheets("Sheet1")
Lastrow = .Cells(.Rows.Count, "A").End(xlUp).Row
'Where you delete you go backwards
For i = Lastrow To 2 Step -1
If .Range("A" & i).Value = "CAT" Then
.Rows(i).EntireRow.Delete
End If
Next i
End With
End Sub
Thank you everyone for help resolving this issue. I have found that the root cause of my problem was simply the condition statement at the end of my If/Then line. The "And_" statement was saying "If cell equals CAT and BAT and DOG, then delete row" NOT "If cell equals CAT or BAT or DOG, then delete row". Replacing "And_" with "Or_" has fixed this issue.
'False screen updating
Application.ScreenUpdating = False
'deleting all other types other than CAT from "samples" tab (excluding the header row, row 1)
Sheets("sample").Select
Lastrow = Cells(Rows.Count, "AN").End(xlUp).Row
'Deleting rows from bottom up
For i = Lastrow To 2 Step -1
If Range("AN" & i).Value = "CAT" Or _
Range("AN" & i).Value = "BAT" Or _
Range("AN" & i).Value = "DOG" Or _
Range("AN" & i).Value = "" Then
Rows(i).EntireRow.Delete
End If
Next i
However, I would also like to delete rows if the cells is Blank "". Why would the Sub ignore this line?
Range("AN" & i).Value = "" Then
Thanks!
A site that might be able to help you be the following.
https://www.excelcampus.com/vba/delete-rows-cell-values/
I adjusted the code a little.
Sub Delete_Rows_Based_On_Value()
'Apply a filter to a Range and delete visible rows
'Source: https://www.excelcampus.com/vba/delete-rows-cell-values
Dim ws As Worksheet
'Set reference to the sheet in the workbook.
Set ws = ThisWorkbook.Worksheets("sampel")
ws.Activate 'not required but allows user to view sheet if warning message appears
'Clear any existing filters
On Error Resume Next
ws.ShowAllData
On Error GoTo 0
'1. Apply Filter
ws.Range("AN3:BG1000").AutoFilter Field:=1, Criteria1:="<>CAT"
'2. Delete Rows
Application.DisplayAlerts = False
ws.Range("B1:G1000").SpecialCells(xlCellTypeVisible).Delete
Application.DisplayAlerts = True
'3. Clear Filter
On Error Resume Next
ws.ShowAllData
On Error GoTo 0
End Sub
I would tend to do it this way:
Sub DeleteRows()
Dim i As Integer
Dim sht As Worksheet
Set sht = ThisWorkbook.Sheets("sample")
i=1
While sht.(i,1) <> "" 'Assuming first column is full of data to the bottom
If sht.Range("AN" & i) = "CAT" Then
sht.Rows(i).EntireRow.Delete
Else
i=i+1
End If
Wend
End Sub

VBA: Next Empty Row Randomly Overwriting Some Rows

I have a list of files:
They share one common format, only have one sheet, but can have multiple rows with data. They are meant to be opened, all cells with data copied, and then pasted to a sheet called Addresses. Like this:
However what I'm getting is this:
Now I have stepped in and noticed that my other data IS being put in the destination, it's just getting overwritten (in what appears to be a random pattern). Here's the code I used:
Option Explicit
Sub AddressListing()
Dim Cell As Range
With Worksheets("ghgh")
For Each Cell In .Range("A1", .Cells(.Rows.Count, "A").End(xlUp)).Cells
If Len(Dir(Cell.Value)) Then
With Workbooks.Open(Cell.Value)
Range("A2:X" & Cells(Rows.Count, "D").End(xlUp).Row).copy _
ThisWorkbook.Worksheets("Addresses").Cells(Rows.Count, "A").End(xlUp).Offset(1)
.Close SaveChanges:=False
End With
Else
MsgBox "File not found: " & Cell.Value
End If
Next Cell
End With
'Call RemoveViaFilter
End Sub
In an effort to combat this and not waste everyone's time, I created a NextRow variable to find the next blank row in the workbook. It still didn't work. I don't get an error message, the data is simply input the same way.
Here's the code with NextRow:
Option Explicit
Sub AddressListing2()
Dim Cell As Range
Dim NextRow As Long
NextRow = ThisWorkbook.Sheets("Addresses").Range("D" & Rows.Count).End(xlUp).Row + 1
With Worksheets("ghgh")
For Each Cell In .Range("A1", .Cells(.Rows.Count, "A").End(xlUp)).Cells
If Len(Dir(Cell.Value)) Then
With Workbooks.Open(Cell.Value)
Range("A2:X" & Cells(Rows.Count, "D").End(xlUp).Row).copy _
ThisWorkbook.Worksheets("Addresses").Range("A" & NextRow)
.Close SaveChanges:=False
End With
Else
MsgBox "File not found: " & Cell.Value
End If
Next Cell
End With
'Call RemoveViaFilter
End Sub
I have never encountered that type of error with NextRow. I know 'Find next blank row and put data there' is a common question, which is why I thought NextRow would solve the issue. However, data is still being overwritten and I have not come across any questions that address this.
I don't want defined ranges (like A2:J100 for example) and have purposefully avoided them, because the length of my lists constantly changes. That goes for the rows I want to paste and the rows of file paths.
Any help is much appreciated, I've used the 'find empty row' several times before with no issues, and don't know why it's overwriting data. It seems antithetical to the whole process of find the empty row.
This is where to you put the additional line...
Option Explicit
Sub AddressListing2()
Dim Cell As Range
Dim NextRow As Long
NextRow = ThisWorkbook.Sheets("Addresses").Range("D" & Rows.Count).End(xlUp).Row + 1
With Worksheets("ghgh")
For Each Cell In .Range("A1", .Cells(.Rows.Count, "A").End(xlUp)).Cells
If Len(Dir(Cell.Value)) Then
With Workbooks.Open(Cell.Value)
Range("A2:X" & Cells(Rows.Count, "D").End(xlUp).Row).copy _
ThisWorkbook.Worksheets("Addresses").Range("A" & NextRow)
.Close SaveChanges:=False
End With
Else
MsgBox "File not found: " & Cell.Value
End If
'Add line here before going to new loop
NextRow = ThisWorkbook.Sheets("Addresses").Range("D" & Rows.Count).End(xlUp).Row + 1
Next Cell
End With
'Call RemoveViaFilter
End Sub
It is clear that NextRow is not being calculated correctly. Put some validation code in after you calculate it:
NextRow = ThisWorkbook.Sheets("Addresses").Range("D" & Rows.Count).End(xlUp).Row + 1
While Application.WorksheetFunction.CountA(Rows(NextRow)) <> 0
NextRow = NextRow + 1
Wend
This will insure NextRow will be an empty row.

VBA Selection.Formula returning "False" instead of "N/A"

I am using VBA to run a set of data against five "rule" columns stored in another sheet in the workbook. Put simply, I seem to have code which works, but the VBA use of Selection.Formula = returns "False" when an cell formula would return #N/A or #VALUE. It's critical that I get the error values because it tells the user something different than "False". False should mean that column C (see picture of calculation tab below) doesn't pass the rule. The error values mean that either column B is not found with VLookup in the Rules column or the rule was written incorrectly.
Here's what I have so far:
Sub Build_Formulas_v2()
Application.Calculation = xlManual
Range("a2", Range("a65536").End(xlUp)).Offset(0, 6).Select
Selection.Value = _
Evaluate("(""=""&SUBSTITUTE(VLOOKUP(B2,'Logic Statements'!A:E,4,FALSE),""ZZZ"",""c""&ROW()))")
End Sub
Any help would be tremendously appreciated - my VBA knowledge is still growing and is too basic to understand what I'm up against.
I believe you are using Excel 2003. You should never hard code values like A65536. You can get undesirable results in xl2007+ as they have 1048576 rows.
Is this what you are trying?
Sub Build_Formulas_v2()
Dim lRow As Long
Dim ws As Worksheet
Application.Calculation = xlManual
'~~> Change this to the relevant sheet
Set ws = ThisWorkbook.Sheets("Sheet1")
With ws
lRow = .Range("A" & .Rows.Count).End(xlUp).Row
.Range("A2:A" & lRow).Offset(0, 6).Formula = _
"=SUBSTITUTE(VLOOKUP(B2,'Logic Statements'!A:E,4,FALSE),""ZZZ"",""c""&ROW())"
'~~> Uncomment the below in case you want to replace formulas with values
'.Range("A2:A" & lRow).Offset(0, 6).Value = .Range("A2:A" & lRow).Offset(0, 6).Value
End With
End Sub
Or if you do not want to use .Offset, then you can directly address Column G
Sub Build_Formulas_v2()
Dim lRow As Long
Dim ws As Worksheet
Application.Calculation = xlManual
'~~> Change this to the relevant sheet
Set ws = ThisWorkbook.Sheets("Sheet1")
With ws
lRow = .Range("A" & .Rows.Count).End(xlUp).Row
.Range("G2:G" & lRow).Formula = _
"=SUBSTITUTE(VLOOKUP(B2,'Logic Statements'!A:E,4,FALSE),""ZZZ"",""C""&ROW())"
'~~> Uncomment the below in case you want to replace formulas with values
'.Range("A2:A" & lRow).Offset(0, 6).Value = _
.Range("A2:A" & lRow).Offset(0, 6).Value
End With
End Sub

Resources