← Macro & add-in library

Drawings & BOM · 1.4

SolidWorks BOM to Round-Bar Planner Bridge

A SolidWorks VBA bridge that reads a drawing BOM, populates a round-bar nesting workbook, recalculates Excel, runs ProcessJob, and saves the result.

ENGINEERING CONTRIBUTION

Mapped BOM headers, preserved item numbers, populated the workbook table, and orchestrated Excel recalculation and processing.

Prerequisites

  • SolidWorks VBA, a saved drawing with a BOM, and Microsoft Excel on Windows.
  • Visible BOM headers ITEM NO., PART NUMBER, DESCRIPTION, MATERIAL, LENGTH and QTY.; optional extra-stock/end-cleanup columns.
  • A locally configured XLSM template containing INPUT_PARTS / tblParts and public ProcessJob.

Additional setup

  • Set TEMPLATE_PATH to a workbook containing INPUT_PARTS / tblParts and the public ProcessJob procedure.

SOURCE WALKTHROUGH

How the workflow fits together.

  1. 01

    Locate the BOM

    main checks a saved drawing and searches visible table headers, preferring the current sheet before other sheets.

  2. 02

    Normalize usable rows

    Blank, dash and N/A lengths are filtered out; length/fraction and quantity parsers coerce data for the workbook input table.

  3. 03

    Copy and populate the workbook

    The configured template is copied beside the drawing and INPUT_PARTS / tblParts is populated. An existing destination triggers a replace-or-reuse choice.

  4. 04

    Run the Excel job and restore state

    The macro automatically calls ProcessJob through Excel automation, calculates and saves. Cleanup restores Excel calculation/events/display state and the original drawing sheet where possible.

Output & model changes

  • Copies or replaces a workbook after an explicit overwrite choice; populates its input table and saves it.
  • Automatically executes the workbook's ProcessJob macro, which has its own dependencies and side effects.
  • Temporarily changes Excel events, screen updating and calculation mode; may start an Excel instance.

CODE & ENTRY POINTS

Read the implementation.

Find a procedure, follow an API call or download the module for your SolidWorks setup.

BomToRoundBarNesting.bas

Drawing BOM extraction and header matching, length/quantity coercion, template copy, Excel table population and automatic ProcessJob invocation.

VBA · 996 lines

Download .bas

Entry points
API & integration calls
Selected line 1
Attribute VB_Name = "modSolidWorksBomToRoundBarNesting"
Option Explicit

'===============================================================================
' SOLIDWORKS DRAWING BOM -> ROUND BAR NESTING WORKBOOK
'
' Workflow:
'   1. Run this macro from a saved SOLIDWORKS drawing.
'   2. The macro finds the BOM by its visible column headers.
'   3. It copies the Excel template into the drawing folder.
'   4. It writes the BOM into INPUT_PARTS / tblParts.
'   5. It runs the Excel macro ProcessJob and saves the workbook.
'
' Required BOM columns:
'   ITEM NO. | PART NUMBER | DESCRIPTION | MATERIAL | LENGTH | QTY.
'
' Optional BOM columns:
'   EXTRA STOCK | BAR END CLEANUP
'
' Revision 1.4:
'   - Uses the verified stable V.04 workbook template.
'   - Ignores BOM rows whose LENGTH is blank, dash, or N/A.
'   - Fixes Excel runtime error 438 by calculating through Excel.Application.
'   - PDF drag/drop is provided by the Excel task-pane add-in and does not alter this macro interface.
'===============================================================================

Private Const TEMPLATE_PATH As String = _
    "C:\PublicUser\OneDrive - Employer Name Withheld\Desktop\ROUND_BAR_Nesting_Template_V.04_FIXED.xlsm"

' True  -> <drawing name>_ROUND_BAR_NESTING.xlsm
' False -> keeps the template file name in the drawing folder
Private Const USE_DRAWING_NAME_FOR_OUTPUT As Boolean = True
Private Const OUTPUT_SUFFIX As String = "_ROUND_BAR_NESTING.xlsm"

Private Const INPUT_SHEET_NAME As String = "INPUT_PARTS"
Private Const INPUT_TABLE_NAME As String = "tblParts"
Private Const EXCEL_PROCESS_MACRO As String = "ProcessJob"

' SOLIDWORKS document type constant, defined locally so the macro does not
' depend on a separate swconst reference for this one value.
Private Const SW_DOC_DRAWING As Long = 3

' Excel constants for late-bound Excel automation.
Private Const XL_CALCULATION_MANUAL As Long = -4135

Private swApp As SldWorks.SldWorks

Public Sub main()
    Dim swModel As SldWorks.ModelDoc2
    Dim swDraw As SldWorks.DrawingDoc
    Dim swSheet As SldWorks.Sheet

    Dim drawingPath As String
    Dim drawingFolder As String
    Dim originalSheetName As String
    Dim sourceSheetName As String
    Dim sourceViewName As String
    Dim destinationPath As String
    Dim extractionWarnings As String

    Dim bomData As Variant
    Dim bomRowCount As Long

    Dim xlApp As Object
    Dim xlWb As Object
    Dim createdExcel As Boolean
    Dim excelStateCaptured As Boolean
    Dim oldEnableEvents As Boolean
    Dim oldScreenUpdating As Boolean
    Dim oldCalculation As Variant
    Dim completionMessage As String
    Dim errorText As String
    Dim savedErrorNumber As Long
    Dim savedErrorDescription As String
    Dim workflowStep As String

    On Error GoTo FatalError

    Set swApp = Application.SldWorks
    Set swModel = swApp.ActiveDoc

    If swModel Is Nothing Then
        Err.Raise vbObjectError + 1000, , _
            "No SOLIDWORKS document is active. Open the drawing and run the macro again."
    End If

    If swModel.GetType <> SW_DOC_DRAWING Then
        Err.Raise vbObjectError + 1001, , _
            "The active SOLIDWORKS document is not a drawing."
    End If

    drawingPath = Trim$(swModel.GetPathName)
    If Len(drawingPath) = 0 Then
        Err.Raise vbObjectError + 1002, , _
            "Save the SOLIDWORKS drawing before running this macro."
    End If

    Set swDraw = swModel
    Set swSheet = swDraw.GetCurrentSheet
    If Not swSheet Is Nothing Then originalSheetName = swSheet.GetName

    drawingFolder = Left$(drawingPath, InStrRev(drawingPath, "\"))

    workflowStep = "Reading and filtering the SOLIDWORKS BOM"
    bomRowCount = ExtractBomFromDrawing( _
        swDraw, originalSheetName, bomData, sourceSheetName, sourceViewName, extractionWarnings)

    If bomRowCount <= 0 Then
        Err.Raise vbObjectError + 1003, , _
            "No usable bar/cut-list rows were found." & vbCrLf & vbCrLf & _
            "The BOM must contain these visible headers:" & vbCrLf & _
            "ITEM NO., PART NUMBER, DESCRIPTION, MATERIAL, LENGTH, and QTY." & _
            vbCrLf & vbCrLf & _
            "Rows with a blank, dash, or N/A LENGTH are intentionally ignored."
    End If

    workflowStep = "Copying the Excel nesting template"
    destinationPath = BuildDestinationPath(drawingPath, drawingFolder)
    PrepareDestinationWorkbook TEMPLATE_PATH, destinationPath

    workflowStep = "Opening the copied Excel workbook"
    Set xlApp = GetRunningExcelApplication()
    If xlApp Is Nothing Then
        Set xlApp = CreateObject("Excel.Application")
        createdExcel = True
    End If

    xlApp.Visible = True

    Set xlWb = GetOpenWorkbookByFullName(xlApp, destinationPath)
    If xlWb Is Nothing Then
        Set xlWb = xlApp.Workbooks.Open(destinationPath)
    End If

    oldEnableEvents = xlApp.EnableEvents
    oldScreenUpdating = xlApp.ScreenUpdating
    oldCalculation = xlApp.Calculation
    excelStateCaptured = True

    xlApp.EnableEvents = False
    xlApp.ScreenUpdating = False
    xlApp.Calculation = XL_CALCULATION_MANUAL

    workflowStep = "Writing filtered BOM rows to INPUT_PARTS"
    PopulateInputTable xlWb, bomData, bomRowCount

    ' Restore Excel's normal operating state before running ProcessJob because
    ' the workbook's own VBA may rely on calculation, events, or screen updates.
    xlApp.Calculation = oldCalculation
    xlApp.EnableEvents = oldEnableEvents
    xlApp.ScreenUpdating = oldScreenUpdating
    excelStateCaptured = False

    xlWb.Activate

    ' Workbook.Calculate is not supported by Excel's Workbook COM object and
    ' causes runtime error 438. Calculate through the Excel Application instead.
    workflowStep = "Recalculating Excel before ProcessJob"
    xlApp.Calculate
    xlWb.Save

    workflowStep = "Running the Excel ProcessJob macro"
    RunExcelProcessJob xlApp, xlWb

    workflowStep = "Saving the processed nesting workbook"
    xlWb.Save
    xlApp.Visible = True

    If Len(originalSheetName) > 0 Then swDraw.ActivateSheet originalSheetName

    completionMessage = CStr(bomRowCount) & " BOM row(s) copied and Process Job completed." & _
                        vbCrLf & vbCrLf & _
                        "BOM source: " & sourceSheetName

    If Len(sourceViewName) > 0 Then
        completionMessage = completionMessage & " / " & sourceViewName
    End If

    completionMessage = completionMessage & vbCrLf & _
                        "Workbook: " & destinationPath

    If Len(extractionWarnings) > 0 Then
        completionMessage = completionMessage & vbCrLf & vbCrLf & _
                            "Input warnings copied to Excel:" & vbCrLf & extractionWarnings
    End If

    MsgBox completionMessage, vbInformation, "Round Bar Nesting"
    Exit Sub

FatalError:
    savedErrorNumber = Err.Number
    savedErrorDescription = Err.Description

    On Error Resume Next

    If excelStateCaptured And Not xlApp Is Nothing Then
        xlApp.Calculation = oldCalculation
        xlApp.EnableEvents = oldEnableEvents
        xlApp.ScreenUpdating = oldScreenUpdating
    End If

    If Not xlApp Is Nothing Then xlApp.Visible = True
    If Len(originalSheetName) > 0 And Not swDraw Is Nothing Then
        swDraw.ActivateSheet originalSheetName
    End If

    ' Do not close a populated workbook after an error; leaving it visible lets
    ' the user inspect the copied data. Quit only a newly created, empty Excel
    ' instance where no workbook was opened.
    If createdExcel And xlWb Is Nothing And Not xlApp Is Nothing Then xlApp.Quit

    errorText = "The BOM-to-nesting workflow did not complete." & vbCrLf & vbCrLf

    If Len(workflowStep) > 0 Then
        errorText = errorText & "Failed step: " & workflowStep & vbCrLf & vbCrLf
    End If

    errorText = errorText & _
                "Error " & CStr(savedErrorNumber) & ": " & savedErrorDescription

    If Not xlWb Is Nothing Then
        errorText = errorText & vbCrLf & vbCrLf & _
                    "The Excel workbook has been left open for inspection."
    End If

    MsgBox errorText, vbCritical, "Round Bar Nesting"
End Sub

'===============================================================================
' SOLIDWORKS BOM EXTRACTION
'===============================================================================

Private Function ExtractBomFromDrawing( _
    ByVal swDraw As SldWorks.DrawingDoc, _
    ByVal originalSheetName As String, _
    ByRef bomData As Variant, _
    ByRef sourceSheetName As String, _
    ByRef sourceViewName As String, _
    ByRef warnings As String) As Long

    Dim sheetNames As Variant
    Dim sheetName As Variant
    Dim candidateData As Variant
    Dim candidateView As String
    Dim candidateWarnings As String
    Dim candidateCount As Long

    ' First scan the active sheet. This is the least surprising behavior when a
    ' drawing contains more than one BOM.
    candidateCount = ExtractBestBomOnCurrentSheet( _
        swDraw, candidateData, candidateView, candidateWarnings)

    If candidateCount > 0 Then
        bomData = candidateData
        sourceSheetName = originalSheetName
        sourceViewName = candidateView
        warnings = candidateWarnings
        ExtractBomFromDrawing = candidateCount
        Exit Function
    End If

    ' If the active sheet has no matching BOM, scan the remaining sheets.
    sheetNames = swDraw.GetSheetNames

    If IsArray(sheetNames) Then
        For Each sheetName In sheetNames
            If StrComp(CStr(sheetName), originalSheetName, vbTextCompare) <> 0 Then
                If swDraw.ActivateSheet(CStr(sheetName)) Then
                    candidateData = Empty
                    candidateView = vbNullString
                    candidateWarnings = vbNullString

                    candidateCount = ExtractBestBomOnCurrentSheet( _
                        swDraw, candidateData, candidateView, candidateWarnings)

                    If candidateCount > 0 Then
                        bomData = candidateData
                        sourceSheetName = CStr(sheetName)
                        sourceViewName = candidateView
                        warnings = candidateWarnings
                        ExtractBomFromDrawing = candidateCount

                        If Len(originalSheetName) > 0 Then
                            swDraw.ActivateSheet originalSheetName
                        End If
                        Exit Function
                    End If
                End If
            End If
        Next sheetName
    End If

    If Len(originalSheetName) > 0 Then swDraw.ActivateSheet originalSheetName
End Function

Private Function ExtractBestBomOnCurrentSheet( _
    ByVal swDraw As SldWorks.DrawingDoc, _
    ByRef bestData As Variant, _
    ByRef bestViewName As String, _
    ByRef bestWarnings As String) As Long

    Dim swView As SldWorks.View
    Dim swTable As SldWorks.TableAnnotation

    Dim candidateData As Variant
    Dim candidateWarnings As String
    Dim candidateCount As Long
    Dim bestCount As Long

    Set swView = swDraw.GetFirstView

    Do While Not swView Is Nothing
        Set swTable = swView.GetFirstTableAnnotation

        Do While Not swTable Is Nothing
            candidateData = Empty
            candidateWarnings = vbNullString

            candidateCount = ReadBomTable(swTable, candidateData, candidateWarnings)

            If candidateCount > bestCount Then
                bestCount = candidateCount
                bestData = candidateData
                bestViewName = swView.GetName2
                bestWarnings = candidateWarnings
            End If

            Set swTable = swTable.GetNext
        Loop

        Set swView = swView.GetNextView
    Loop

    ExtractBestBomOnCurrentSheet = bestCount
End Function

Private Function ReadBomTable( _
    ByVal swTable As SldWorks.TableAnnotation, _
    ByRef outputData As Variant, _
    ByRef warnings As String) As Long

    Dim headerRow As Long
    Dim headerMap() As Long
    Dim rowIndex As Long
    Dim fieldIndex As Long
    Dim rowCount As Long

    Dim rows As Collection
    Dim rowValues As Variant
    Dim finalData() As Variant
    Dim currentRow As Variant

    Dim lengthParsed As Boolean
    Dim quantityParsed As Boolean

    ReDim headerMap(1 To 8)

    If Not FindRequiredHeaderRow(swTable, headerRow, headerMap) Then Exit Function

    Set rows = New Collection

    For rowIndex = 0 To swTable.RowCount - 1
        If rowIndex <> headerRow Then
            If Not IsTableRowHidden(swTable, rowIndex) Then
                ReDim rowValues(1 To 8)

                For fieldIndex = 1 To 8
                    If headerMap(fieldIndex) >= 0 Then
                        rowValues(fieldIndex) = CleanCellText( _
                            GetTableCellText(swTable, rowIndex, headerMap(fieldIndex)))
                    Else
                        rowValues(fieldIndex) = vbNullString
                    End If
                Next fieldIndex

                If IsPotentialBomDataRow(rowValues) Then
                    If Not RowLooksLikeHeader(rowValues) Then
                        If Not RowLooksLikeTotal(rowValues) Then
                            ' Detailed weldment BOMs can include the top-level weldment,
                            ' machined parts, and waterjet parts. Only rows with a real
                            ' LENGTH belong in the bar-nesting input.
                            If HasPopulatedLengthValue(CStr(rowValues(5))) Then
                                rowValues(5) = CoerceLengthForExcel( _
                                    CStr(rowValues(5)), lengthParsed)
                                rowValues(6) = CoerceQuantityForExcel( _
                                    CStr(rowValues(6)), quantityParsed)

                                If Not lengthParsed Then
                                    AppendWarning warnings, _
                                        "BOM row " & CStr(rowIndex + 1) & _
                                        ": LENGTH was copied as text (" & CStr(rowValues(5)) & ")."
                                End If

                                If Len(Trim$(CStr(rowValues(6)))) = 0 Then
                                    AppendWarning warnings, _
                                        "BOM row " & CStr(rowIndex + 1) & ": QTY. is blank."
                                ElseIf Not quantityParsed Then
                                    AppendWarning warnings, _
                                        "BOM row " & CStr(rowIndex + 1) & _
                                        ": QTY. was copied as text (" & CStr(rowValues(6)) & ")."
                                End If

                                rows.Add rowValues
                            End If
                        End If
                    End If
                End If
            End If
        End If
    Next rowIndex

    rowCount = rows.Count
    If rowCount = 0 Then Exit Function

    ReDim finalData(1 To rowCount, 1 To 8)

    For rowIndex = 1 To rowCount
        currentRow = rows(rowIndex)
        For fieldIndex = 1 To 8
            finalData(rowIndex, fieldIndex) = currentRow(fieldIndex)
        Next fieldIndex
    Next rowIndex

    outputData = finalData
    ReadBomTable = rowCount
End Function

Private Function FindRequiredHeaderRow( _
    ByVal swTable As SldWorks.TableAnnotation, _
    ByRef headerRow As Long, _
    ByRef headerMap() As Long) As Boolean

    Dim rowIndex As Long
    Dim columnIndex As Long
    Dim fieldIndex As Long
    Dim detectedField As Long
    Dim cellText As String

    For rowIndex = 0 To swTable.RowCount - 1
        For fieldIndex = 1 To 8
            headerMap(fieldIndex) = -1
        Next fieldIndex

        For columnIndex = 0 To swTable.ColumnCount - 1
            cellText = GetTableCellText(swTable, rowIndex, columnIndex)
            detectedField = HeaderFieldNumber(cellText)

            If detectedField > 0 Then
                If headerMap(detectedField) < 0 Then
                    headerMap(detectedField) = columnIndex
                End If
            End If
        Next columnIndex

        If headerMap(1) >= 0 And _
           headerMap(2) >= 0 And _
           headerMap(3) >= 0 And _
           headerMap(4) >= 0 And _
           headerMap(5) >= 0 And _
           headerMap(6) >= 0 Then

            headerRow = rowIndex
            FindRequiredHeaderRow = True
            Exit Function
        End If
    Next rowIndex
End Function

Private Function HeaderFieldNumber(ByVal headerText As String) As Long
    Dim normalized As String
    normalized = NormalizeHeaderText(headerText)

    Select Case normalized
        Case "ITEM", "ITEM NO", "ITEM NUMBER", "FIND NO", "FIND NUMBER"
            HeaderFieldNumber = 1

        Case "PART", "PART NO", "PART NUMBER"
            HeaderFieldNumber = 2

        Case "DESCRIPTION", "DESC"
            HeaderFieldNumber = 3

        Case "MATERIAL", "MATL", "MAT L"
            HeaderFieldNumber = 4

        Case "LENGTH", "LENGTH IN", "CUT LENGTH", "CUT LENGTH IN", _
             "FINISH LENGTH", "FINISHED LENGTH"
            HeaderFieldNumber = 5

        Case "QTY", "QUANTITY", "QTY REQUIRED", "REQUIRED QTY"
            HeaderFieldNumber = 6

        Case "EXTRA STOCK", "EXTRA STOCK IN", "EXTRA LENGTH"
            HeaderFieldNumber = 7

        Case "BAR END CLEANUP", "BAR END CLEAN UP", "END CLEANUP", _
             "END CLEAN UP"
            HeaderFieldNumber = 8
    End Select
End Function

Private Function GetTableCellText( _
    ByVal swTable As SldWorks.TableAnnotation, _
    ByVal rowIndex As Long, _
    ByVal columnIndex As Long) As String

    On Error Resume Next

    GetTableCellText = CStr(swTable.DisplayedText2(rowIndex, columnIndex, True))

    If Err.Number <> 0 Then
        Err.Clear
        GetTableCellText = CStr(swTable.Text2(rowIndex, columnIndex, True))
    End If

    On Error GoTo 0
End Function

Private Function IsTableRowHidden( _
    ByVal swTable As SldWorks.TableAnnotation, _
    ByVal rowIndex As Long) As Boolean

    On Error Resume Next
    IsTableRowHidden = CBool(swTable.RowHidden(rowIndex))
    On Error GoTo 0
End Function

Private Function IsPotentialBomDataRow(ByVal rowValues As Variant) As Boolean
    ' A table title can occupy the ITEM NO. column in a merged cell. Requiring
    ' content in at least one of the other core fields avoids importing titles.
    IsPotentialBomDataRow = _
        Len(Trim$(CStr(rowValues(2)))) > 0 Or _
        Len(Trim$(CStr(rowValues(3)))) > 0 Or _
        Len(Trim$(CStr(rowValues(4)))) > 0 Or _
        Len(Trim$(CStr(rowValues(5)))) > 0 Or _
        Len(Trim$(CStr(rowValues(6)))) > 0
End Function

Private Function HasPopulatedLengthValue(ByVal rawValue As String) As Boolean
    Dim normalized As String

    normalized = UCase$(CleanCellText(rawValue))
    normalized = Replace(normalized, ChrW(&H2013), "-")
    normalized = Replace(normalized, ChrW(&H2014), "-")

    Select Case normalized
        Case vbNullString, "-", "--", "N/A", "NA", "NONE"
            HasPopulatedLengthValue = False
        Case Else
            HasPopulatedLengthValue = True
    End Select
End Function

Private Function RowLooksLikeHeader(ByVal rowValues As Variant) As Boolean
    Dim matches As Long
    Dim fieldIndex As Long

    For fieldIndex = 1 To 6
        If HeaderFieldNumber(CStr(rowValues(fieldIndex))) = fieldIndex Then
            matches = matches + 1
        End If
    Next fieldIndex

    RowLooksLikeHeader = (matches >= 4)
End Function

Private Function RowLooksLikeTotal(ByVal rowValues As Variant) As Boolean
    Dim itemText As String
    Dim partText As String
    Dim descriptionText As String

    itemText = UCase$(Trim$(CStr(rowValues(1))))
    partText = UCase$(Trim$(CStr(rowValues(2))))
    descriptionText = UCase$(Trim$(CStr(rowValues(3))))

    RowLooksLikeTotal = _
        (itemText = "TOTAL" Or Left$(itemText, 6) = "TOTAL ") Or _
        (partText = "TOTAL" Or Left$(partText, 6) = "TOTAL ") Or _
        (descriptionText = "TOTAL" Or Left$(descriptionText, 6) = "TOTAL ")
End Function

'===============================================================================
' EXCEL WORKBOOK CREATION AND POPULATION
'===============================================================================

Private Function BuildDestinationPath( _
    ByVal drawingPath As String, _
    ByVal drawingFolder As String) As String

    Dim fso As Object
    Dim fileName As String

    Set fso = CreateObject("Scripting.FileSystemObject")

    If USE_DRAWING_NAME_FOR_OUTPUT Then
        fileName = SanitizeFileName(fso.GetBaseName(drawingPath)) & OUTPUT_SUFFIX
    Else
        fileName = fso.GetFileName(TEMPLATE_PATH)
    End If

    BuildDestinationPath = drawingFolder & fileName
End Function

Private Sub PrepareDestinationWorkbook( _
    ByVal sourcePath As String, _
    ByVal destinationPath As String)

    Dim fso As Object
    Dim overwriteChoice As VbMsgBoxResult

    Set fso = CreateObject("Scripting.FileSystemObject")

    If Not fso.FileExists(sourcePath) Then
        Err.Raise vbObjectError + 1100, , _
            "Excel template not found:" & vbCrLf & sourcePath
    End If

    If StrComp(sourcePath, destinationPath, vbTextCompare) = 0 Then
        Err.Raise vbObjectError + 1101, , _
            "The template and destination paths are the same. " & _
            "Keep USE_DRAWING_NAME_FOR_OUTPUT set to True or move the template."
    End If

    If fso.FileExists(destinationPath) Then
        overwriteChoice = MsgBox( _
            "A nesting workbook already exists:" & vbCrLf & _
            destinationPath & vbCrLf & vbCrLf & _
            "Yes = replace it with a fresh template" & vbCrLf & _
            "No = reuse and repopulate the existing workbook" & vbCrLf & _
            "Cancel = stop", _
            vbYesNoCancel + vbQuestion, _
            "Existing Nesting Workbook")

        If overwriteChoice = vbCancel Then
            Err.Raise vbObjectError + 1102, , "Operation cancelled by user."
        ElseIf overwriteChoice = vbNo Then
            Exit Sub
        End If
    End If

    On Error GoTo CopyFailed
    fso.CopyFile sourcePath, destinationPath, True
    Exit Sub

CopyFailed:
    Err.Raise vbObjectError + 1103, , _
        "The Excel template could not be copied to the drawing folder." & vbCrLf & _
        "Close any workbook using the destination file and verify folder permissions." & _
        vbCrLf & vbCrLf & _
        "Destination: " & destinationPath & vbCrLf & _
        "Windows error: " & Err.Description
End Sub

Private Function GetRunningExcelApplication() As Object
    On Error Resume Next
    Set GetRunningExcelApplication = GetObject(, "Excel.Application")
    On Error GoTo 0
End Function

Private Function GetOpenWorkbookByFullName( _
    ByVal xlApp As Object, _
    ByVal fullName As String) As Object

    Dim wb As Object

    For Each wb In xlApp.Workbooks
        If StrComp(CStr(wb.FullName), fullName, vbTextCompare) = 0 Then
            Set GetOpenWorkbookByFullName = wb
            Exit Function
        End If
    Next wb
End Function

Private Sub PopulateInputTable( _
    ByVal xlWb As Object, _
    ByVal bomData As Variant, _
    ByVal bomRowCount As Long)

    Dim ws As Object
    Dim inputTable As Object
    Dim targetRange As Object
    Dim inputColumn As Object

    Dim headerRow As Long
    Dim firstColumn As Long
    Dim lastColumn As Long
    Dim existingDataRows As Long
    Dim requiredDataRows As Long
    Dim columnIndex As Long
    Dim maxPartRows As Long

    On Error Resume Next
    Set ws = xlWb.Worksheets(INPUT_SHEET_NAME)
    On Error GoTo 0

    If ws Is Nothing Then
        Err.Raise vbObjectError + 1200, , _
            "Worksheet '" & INPUT_SHEET_NAME & "' was not found in the copied workbook."
    End If

    On Error Resume Next
    Set inputTable = ws.ListObjects(INPUT_TABLE_NAME)
    On Error GoTo 0

    If inputTable Is Nothing Then
        Err.Raise vbObjectError + 1201, , _
            "Excel table '" & INPUT_TABLE_NAME & "' was not found on " & _
            INPUT_SHEET_NAME & "."
    End If

    On Error Resume Next
    maxPartRows = CLng(xlWb.Names("MaxPartRows").RefersToRange.Value2)
    On Error GoTo 0

    If maxPartRows > 0 And bomRowCount > maxPartRows Then
        Err.Raise vbObjectError + 1202, , _
            "The BOM contains " & CStr(bomRowCount) & " rows, but the nesting " & _
            "workbook is configured for a maximum of " & CStr(maxPartRows) & "."
    End If

    headerRow = inputTable.HeaderRowRange.Row
    firstColumn = inputTable.Range.Column
    lastColumn = firstColumn + inputTable.ListColumns.Count - 1
    existingDataRows = inputTable.ListRows.Count

    requiredDataRows = existingDataRows
    If requiredDataRows < bomRowCount Then requiredDataRows = bomRowCount
    If requiredDataRows < 1 Then requiredDataRows = 1

    If requiredDataRows <> existingDataRows Then
        inputTable.Resize ws.Range( _
            ws.Cells(headerRow, firstColumn), _
            ws.Cells(headerRow + requiredDataRows, lastColumn))
    End If

    ' Clear only the eight editable input columns. Formula/helper columns I:N are
    ' preserved, and Excel extends their calculated-column formulas when needed.
    For columnIndex = 1 To 8
        Set inputColumn = inputTable.ListColumns(columnIndex)
        If Not inputColumn.DataBodyRange Is Nothing Then
            inputColumn.DataBodyRange.ClearContents
        End If
    Next columnIndex

    Set targetRange = ws.Cells(headerRow + 1, firstColumn).Resize(bomRowCount, 8)

    ' Preserve item and part numbers exactly as displayed, including leading zeroes.
    targetRange.Columns(1).NumberFormat = "@"
    targetRange.Columns(2).NumberFormat = "@"
    targetRange.Columns(3).NumberFormat = "@"
    targetRange.Columns(4).NumberFormat = "@"
    targetRange.Columns(5).NumberFormat = "0.000"
    targetRange.Columns(6).NumberFormat = "0"

    targetRange.Value2 = bomData

    ws.Activate
    targetRange.Cells(1, 1).Select
End Sub

Private Sub RunExcelProcessJob(ByVal xlApp As Object, ByVal xlWb As Object)
    Dim qualifiedMacroName As String
    Dim savedErrorNumber As Long
    Dim savedErrorDescription As String

    qualifiedMacroName = "'" & Replace(CStr(xlWb.Name), "'", "''") & _
                         "'!" & EXCEL_PROCESS_MACRO

    On Error GoTo ProcessFailed
    xlApp.Run qualifiedMacroName
    Exit Sub

ProcessFailed:
    savedErrorNumber = Err.Number
    savedErrorDescription = Err.Description

    Err.Raise vbObjectError + 1300, , _
        "The BOM was copied into Excel, but the workbook macro '" & _
        EXCEL_PROCESS_MACRO & "' could not be run." & vbCrLf & vbCrLf & _
        "Confirm that Excel macros are enabled, the file is trusted/unblocked, " & _
        "and the template still contains the public ProcessJob procedure." & _
        vbCrLf & vbCrLf & _
        "Excel error " & CStr(savedErrorNumber) & ": " & savedErrorDescription
End Sub

'===============================================================================
' DATA CLEANING AND NUMERIC CONVERSION
'===============================================================================

Private Function CleanCellText(ByVal value As String) As String
    Dim cleaned As String

    cleaned = Replace(value, vbCr, " ")
    cleaned = Replace(cleaned, vbLf, " ")
    cleaned = Replace(cleaned, Chr$(160), " ")

    CleanCellText = Trim$(CollapseSpaces(cleaned))
End Function

Private Function NormalizeHeaderText(ByVal value As String) As String
    Dim normalized As String

    normalized = UCase$(CleanCellText(value))
    normalized = Replace(normalized, ".", " ")
    normalized = Replace(normalized, "/", " ")
    normalized = Replace(normalized, "\", " ")
    normalized = Replace(normalized, "-", " ")
    normalized = Replace(normalized, "_", " ")
    normalized = Replace(normalized, "#", " ")
    normalized = Replace(normalized, "(", " ")
    normalized = Replace(normalized, ")", " ")
    normalized = Replace(normalized, ":", " ")

    NormalizeHeaderText = Trim$(CollapseSpaces(normalized))
End Function

Private Function CollapseSpaces(ByVal value As String) As String
    Dim result As String

    result = value
    Do While InStr(result, "  ") > 0
        result = Replace(result, "  ", " ")
    Loop

    CollapseSpaces = result
End Function

Private Function CoerceLengthForExcel( _
    ByVal rawValue As String, _
    ByRef parsedSuccessfully As Boolean) As Variant

    Dim inches As Double

    If Len(Trim$(rawValue)) = 0 Then
        parsedSuccessfully = False
        CoerceLengthForExcel = Empty
    ElseIf TryParseLengthInches(rawValue, inches) Then
        parsedSuccessfully = True
        CoerceLengthForExcel = inches
    Else
        parsedSuccessfully = False
        CoerceLengthForExcel = rawValue
    End If
End Function

Private Function CoerceQuantityForExcel( _
    ByVal rawValue As String, _
    ByRef parsedSuccessfully As Boolean) As Variant

    Dim cleaned As String
    Dim numericValue As Double

    cleaned = UCase$(Trim$(rawValue))
    cleaned = Replace(cleaned, "PCS", "")
    cleaned = Replace(cleaned, "PC", "")
    cleaned = Replace(cleaned, "EA", "")
    cleaned = Trim$(cleaned)

    If Len(cleaned) = 0 Then
        parsedSuccessfully = False
        CoerceQuantityForExcel = Empty
    ElseIf IsNumeric(cleaned) Then
        numericValue = CDbl(cleaned)
        parsedSuccessfully = True
        CoerceQuantityForExcel = numericValue
    Else
        parsedSuccessfully = False
        CoerceQuantityForExcel = rawValue
    End If
End Function

Private Function TryParseLengthInches( _
    ByVal rawValue As String, _
    ByRef resultInches As Double) As Boolean

    Dim cleaned As String
    Dim isMillimetres As Boolean
    Dim slashPosition As Long
    Dim separatorPosition As Long
    Dim wholeText As String
    Dim fractionText As String
    Dim fractionValue As Double
    Dim numericValue As Double

    cleaned = UCase$(Trim$(rawValue))
    cleaned = Replace(cleaned, ChrW(&HBC), " 1/4")
    cleaned = Replace(cleaned, ChrW(&HBD), " 1/2")
    cleaned = Replace(cleaned, ChrW(&HBE), " 3/4")
    cleaned = Replace(cleaned, ChrW(&H215B), " 1/8")
    cleaned = Replace(cleaned, ChrW(&H215C), " 3/8")
    cleaned = Replace(cleaned, ChrW(&H215D), " 5/8")
    cleaned = Replace(cleaned, ChrW(&H215E), " 7/8")
    cleaned = Replace(cleaned, Chr$(34), "")
    cleaned = Replace(cleaned, ",", "")

    If InStr(1, cleaned, "MM", vbTextCompare) > 0 Then
        isMillimetres = True
        cleaned = Replace(cleaned, "MILLIMETRES", "")
        cleaned = Replace(cleaned, "MILLIMETERS", "")
        cleaned = Replace(cleaned, "MM", "")
    Else
        cleaned = Replace(cleaned, "INCHES", "")
        cleaned = Replace(cleaned, "INCH", "")

        If Right$(Trim$(cleaned), 2) = "IN" Then
            cleaned = Left$(Trim$(cleaned), Len(Trim$(cleaned)) - 2)
        End If
    End If

    cleaned = Trim$(CollapseSpaces(cleaned))

    Do While InStr(cleaned, " /") > 0
        cleaned = Replace(cleaned, " /", "/")
    Loop

    Do While InStr(cleaned, "/ ") > 0
        cleaned = Replace(cleaned, "/ ", "/")
    Loop

    If Len(cleaned) = 0 Then Exit Function

    If IsNumeric(cleaned) Then
        numericValue = CDbl(cleaned)
    Else
        slashPosition = InStrRev(cleaned, "/")
        If slashPosition = 0 Then Exit Function

        separatorPosition = InStrRev(Left$(cleaned, slashPosition - 1), " ")
        If separatorPosition = 0 Then
            separatorPosition = InStrRev(Left$(cleaned, slashPosition - 1), "-")
        End If

        If separatorPosition > 0 Then
            wholeText = Trim$(Left$(cleaned, separatorPosition - 1))
            fractionText = Trim$(Mid$(cleaned, separatorPosition + 1))

            If Not IsNumeric(wholeText) Then Exit Function
            If Not TryParseSimpleFraction(fractionText, fractionValue) Then Exit Function

            numericValue = CDbl(wholeText) + fractionValue
        Else
            If Not TryParseSimpleFraction(cleaned, numericValue) Then Exit Function
        End If
    End If

    If isMillimetres Then numericValue = numericValue / 25.4

    resultInches = numericValue
    TryParseLengthInches = True
End Function

Private Function TryParseSimpleFraction( _
    ByVal fractionText As String, _
    ByRef fractionValue As Double) As Boolean

    Dim slashPosition As Long
    Dim numeratorText As String
    Dim denominatorText As String
    Dim denominator As Double

    slashPosition = InStr(1, fractionText, "/")
    If slashPosition <= 1 Then Exit Function

    numeratorText = Trim$(Left$(fractionText, slashPosition - 1))
    denominatorText = Trim$(Mid$(fractionText, slashPosition + 1))

    If Not IsNumeric(numeratorText) Then Exit Function
    If Not IsNumeric(denominatorText) Then Exit Function

    denominator = CDbl(denominatorText)
    If denominator = 0 Then Exit Function

    fractionValue = CDbl(numeratorText) / denominator
    TryParseSimpleFraction = True
End Function

Private Sub AppendWarning(ByRef warningText As String, ByVal newWarning As String)
    If Len(warningText) > 0 Then warningText = warningText & vbCrLf
    warningText = warningText & newWarning
End Sub

Private Function SanitizeFileName(ByVal fileName As String) As String
    Dim invalidCharacters As Variant
    Dim invalidCharacter As Variant
    Dim result As String

    result = fileName
    invalidCharacters = Array("<", ">", ":", Chr$(34), "/", "\", "|", "?", "*")

    For Each invalidCharacter In invalidCharacters
        result = Replace(result, CStr(invalidCharacter), "_")
    Next invalidCharacter

    SanitizeFileName = Trim$(result)
End Function
Procedure index · 27 declarations
File checksum

SHA-256: 3a11a2f47012db1dcd9849a5a4075619173f5b07a267e649cb83f44925f41166