Files
AutoBOM/reference_docs/combined_output.md
Misaka_Company fd414f7673 docs: reorganize documentation structure and add analysis guidelines
- Move existing documentation files to reference_docs/ directory
- Add documentation guidelines section to CLAUDE.md
- Add clsBOMManager LoadData flow analysis with Mermaid diagrams

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-29 09:30:29 +08:00

98 KiB
Raw Blame History

目录


ClassModules\clsBOMManager.cls

' ========================================
' 类模块: clsBOMManager
' 用途: 管理整个BOM数据结构
' 功能:
'   1. 加载和组织BOM数据
'   2. 建立类别层级关系
'   3. 提供物料查询接口
'   4. 支持领料逻辑处理
' ========================================
Option Explicit

' ========================================
' 私有成员变量
' ========================================
Private dictCategories As Object    ' Dictionary对象: 类别名称 -> clsCategory对象
                                    ' 作用: 快速查找任意类别
Private dictAllMaterials As Object  ' Dictionary对象: 物料代号 -> clsMaterialItem对象
                                    ' 作用: 快速查找任意物料
Private rootCategories As collection ' Collection: 存储所有顶层类别(无父类别的类别)
                                    ' 作用: 遍历完整的类别树结构

' ========================================
' 类初始化
' 说明: 创建BOMManager实例时自动调用
' ========================================
Private Sub Class_Initialize()
    Set dictCategories = CreateObject("Scripting.Dictionary")
    Set dictAllMaterials = CreateObject("Scripting.Dictionary")
    Set rootCategories = New collection
End Sub

' ========================================
' LoadData 方法
' 功能: 从Excel工作表加载BOM数据并构建数据结构
' 参数:
'   wsConfig   - [领料配置]工作表对象,包含类别层级和需要领料的物料
'   wsPlatform - [平台配置清单]工作表对象,包含完整的物料信息(代号/名称/数量/条件)
' 处理步骤:
'   1. 从[平台配置清单]加载所有物料的基础信息到dictAllMaterials
'   2. 从[领料配置]加载类别信息,筛选需要领料的物料
'   3. 建立类别的父子关系,构建层级树
' 注意:
'   - 只有出现在[领料配置]中的物料才会被添加到类别中
'   - 未在[领料配置]中的物料表示不需要领料
' ========================================
Public Sub LoadData(wsConfig As Worksheet, wsPlatform As Worksheet)
    Dim i As Long, lastRow As Long
    Dim mat As clsMaterialItem
    Dim cat As clsCategory

    ' ========================================
    ' 第一步: 从平台配置清单加载所有物料的基础信息
    ' 说明:
    '   - 读取C列(代号)、D列(名称)、E列(数量)、F列(选择条件)
    '   - 从第4行开始读取(前3行是标题)
    '   - 所有物料存入dictAllMaterials字典,以代号为键
    ' 目的: 建立完整的物料信息库,供后续按代号查询
    ' ========================================
    lastRow = wsPlatform.Cells(wsPlatform.Rows.Count, "C").End(xlUp).row
    For i = 4 To lastRow ' 从第4行开始(跳过标题)
        Set mat = New clsMaterialItem
        mat.code = Trim(wsPlatform.Cells(i, "C").value & "")       ' 物料代号
        mat.Name = Trim(wsPlatform.Cells(i, "D").value & "")       ' 物料名称
        On Error Resume Next
        mat.Quantity = CDbl(wsPlatform.Cells(i, "E").value)        ' 物料数量
        On Error GoTo 0
        mat.Condition = Trim(wsPlatform.Cells(i, "F").value & "")  ' 选择条件

        ' 只保存代号非空的物料
        If mat.code <> "" Then
            Set dictAllMaterials(mat.code) = mat
        End If
    Next i

    ' ========================================
    ' 第二步: 从领料配置加载类别信息并建立层级
    ' 说明:
    '   - 读取A列(代号)、C列(类别)、D列(上层类别)
    '   - 从第2行开始读取(第1行是标题)
    '   - 只有出现在此表中的物料才需要领料
    ' 处理逻辑:
    '   1. 为每个类别创建clsCategory对象
    '   2. 从dictAllMaterials中查找对应的物料信息
    '   3. 将物料添加到对应的类别中
    ' ========================================
    lastRow = wsConfig.Cells(wsConfig.Rows.Count, "A").End(xlUp).row
    For i = 2 To lastRow ' 从第2行开始
        Dim code As String, catName As String, parentCatName As String
        code = Trim(wsConfig.Cells(i, "A").value & "")           ' 物料代号
        catName = Trim(wsConfig.Cells(i, "C").value & "")        ' 类别名称
        parentCatName = Trim(wsConfig.Cells(i, "D").value & "")  ' 上层类别名称

        ' 跳过空行
        If code = "" Then GoTo NextRow

        ' 确保类别对象存在(如果类别不存在则创建)
        If Not dictCategories.Exists(catName) Then
            Set cat = New clsCategory
            cat.categoryName = catName
            cat.ParentCategoryName = parentCatName
            Set dictCategories(catName) = cat
        End If

        ' 将物料添加到类别
        ' 注意: 必须先在dictAllMaterials中查找到完整的物料信息
        If dictAllMaterials.Exists(code) Then
            Set mat = dictAllMaterials(code)
            mat.Category = catName          ' 设置物料所属类别
            mat.ParentCategory = parentCatName  ' 设置物料的上层类别
            dictCategories(catName).AddMaterial mat  ' 将物料添加到类别对象中
        End If

NextRow:
    Next i

    ' ========================================
    ' 第三步: 建立类别层级关系
    ' 说明:
    '   - 遍历所有类别,根据ParentCategoryName建立父子关系
    '   - 如果类别有父类别,将自己添加到父类别的SubCategories中
    '   - 如果类别没有父类别,则为根类别,添加到rootCategories中
    ' 结果:
    '   - 构建完整的树形结构
    '   - rootCategories包含所有顶层类别
    '   - 每个类别的SubCategories包含其直接子类别
    ' ========================================
    Dim key As Variant
    For Each key In dictCategories.Keys
        Set cat = dictCategories(key)
        If cat.ParentCategoryName <> "" Then
            ' 有父类别,建立父子关系
            If dictCategories.Exists(cat.ParentCategoryName) Then
                Dim parentCat As clsCategory
                Set parentCat = dictCategories(cat.ParentCategoryName)
                parentCat.AddSubCategory cat  ' 将当前类别添加为父类别的子类别
            End If
        Else
            ' 无父类别,是根类别
            rootCategories.Add cat, cat.categoryName
        End If
    Next key
End Sub

' ========================================
' GetRootCategories 方法
' 功能: 获取所有顶层类别的集合
' 返回: Collection对象,包含所有无父类别的clsCategory对象
' 用途:
'   - 遍历整个BOM结构时的入口点
'   - 生成领料清单时遍历所有根类别
' 示例:
'   Dim cats As Collection
'   Set cats = bomMgr.GetRootCategories()
'   For i = 1 To cats.Count
'       Debug.Print cats(i).CategoryName
'   Next i
' ========================================
Public Function GetRootCategories() As collection
    Set GetRootCategories = rootCategories
End Function

' ========================================
' GetCategory 方法
' 功能: 根据类别名称获取类别对象
' 参数:
'   categoryName - 要查询的类别名称(字符串)
' 返回:
'   clsCategory对象 - 如果找到
'   Nothing         - 如果未找到
' 用途: 快速查找特定类别及其下的物料
' 示例:
'   Dim cat As clsCategory
'   Set cat = bomMgr.GetCategory("部件")
'   If Not cat Is Nothing Then
'       Debug.Print cat.Materials.Count & " 个物料"
'   End If
' ========================================
Public Function GetCategory(categoryName As String) As clsCategory
    If dictCategories.Exists(categoryName) Then
        Set GetCategory = dictCategories(categoryName)
    Else
        Set GetCategory = Nothing
    End If
End Function

' ========================================
' GetMaterialsForPicking 方法
' 功能: 获取某类别下需要领料的物料清单(考虑层级逻辑)
' 参数:
'   categoryName - 类别名称
'   useParent    - 可选参数,默认True
'                  True:  使用父类别物料(默认领料方式)
'                  False: 使用子类别物料(库存不足时的替代方案)
' 返回: Collection对象,包含clsMaterialItem对象
'
' 业务逻辑说明:
'   1. 默认领取父类别的物料(如"低压接头部件")
'   2. 当父类别库存不足时,才领取子类别的物料(如"接头"+"弹性元件")
'   3. 如果useParent=True但类别有子类别,仍返回父类别物料
'   4. 如果useParent=False,递归获取所有子类别的物料
'
' 示例1: 获取"部件"类别的物料(父类别)
'   Set mats = bomMgr.GetMaterialsForPicking("部件", True)
'   ' 返回: 低压接头部件、高压接头部件等组装好的部件
'
' 示例2: 获取"部件"类别的物料(子类别展开)
'   Set mats = bomMgr.GetMaterialsForPicking("部件", False)
'   ' 返回: 径向低压接头、弹簧管、螺旋管等零件
' ========================================
Public Function GetMaterialsForPicking(categoryName As String, _
                                      Optional useParent As Boolean = True) As collection
    Dim result As collection
    Set result = New collection

    ' 查找指定类别
    Dim cat As clsCategory
    Set cat = GetCategory(categoryName)
    If cat Is Nothing Then
        ' 类别不存在,返回空集合
        Set GetMaterialsForPicking = result
        Exit Function
    End If

    Dim i As Long

    If useParent Then
        ' ========================================
        ' 使用父类别物料(默认领料方式)
        ' 说明:
        '   - 直接返回当前类别下的所有物料
        '   - 即使该类别有子类别,也仍然返回父类别物料
        '   - 这是正常情况下的领料方式(领取组装好的部件)
        ' ========================================
        Dim m As clsMaterialItem
        For i = 1 To cat.materials.Count
            Set m = cat.materials(i)
            result.Add m
        Next i
    Else
        ' ========================================
        ' 使用子类别物料(库存不足时的替代方案)
        ' 说明:
        '   - 如果当前类别有子类别,递归获取所有子类别的物料
        '   - 如果当前类别是叶子类别(无子类别),返回本类别物料
        '   - 这用于父类别库存不足,需要领取零件自行组装的情况
        ' 示例:
        '   当"低压接头部件"库存不足时
        '   改为领取"径向低压接头"+"弹簧管"零件
        ' ========================================
        If cat.HasSubCategories Then
            ' 有子类别,递归获取所有子类别的物料
            Dim subCat As clsCategory
            Dim j As Long
            For j = 1 To cat.SubCategories.Count
                Set subCat = cat.SubCategories(j)
                Dim subMats As collection
                ' 递归调用,继续展开子类别
                Set subMats = GetMaterialsForPicking(subCat.categoryName, False)
                Dim k As Long
                For k = 1 To subMats.Count
                    result.Add subMats(k)
                Next k
            Next j
        Else
            ' 叶子类别,返回本类别物料
            For i = 1 To cat.materials.Count
                Set m = cat.materials(i)
                result.Add m
            Next i
        End If
    End If

    Set GetMaterialsForPicking = result
End Function

' ========================================
' PrintCategoryTree 方法
' 功能: 将类别树结构打印到工作表(用于调试和查看)
' 参数:
'   ws - 输出的目标工作表对象
' 输出格式:
'   - 第一列: 类别名称(带缩进显示层级)
'   - 第二列: 物料数量信息
'   - 第三列: 选择条件
' 说明:
'   - 使用缩进显示类别层级(每层2个空格)
'   - 递归打印所有子类别和物料
'   - 便于验证数据结构是否正确
' 示例输出:
'   表壳 (物料数:1)
'     - 01091004312 表壳(本色)  数量:1  条件:
'   部件 (物料数:20)
'     - 01011019001 低压接头部件  数量:1  条件:lcfw=M02...
'     接头 (物料数:18)
'       - 01081013833 径向低压接头  数量:1  条件:gclj=Z12...
' ========================================
Public Sub PrintCategoryTree(ws As Worksheet)
    Dim row As Long
    row = 1
    ws.Cells(row, 1).value = "类别层级结构"
    row = row + 1

    ' 遍历所有根类别,递归打印整个树
    Dim rootCat As clsCategory
    Dim i As Long
    For i = 1 To rootCategories.Count
        Set rootCat = rootCategories(i)
        Call PrintCategory(ws, rootCat, row, 0)
    Next i
End Sub

' ========================================
' PrintCategory 方法 (私有方法)
' 功能: 递归打印单个类别及其子类别(供PrintCategoryTree调用)
' 参数:
'   ws    - 输出的工作表对象
'   cat   - 要打印的类别对象
'   row   - 当前输出行号(ByRef,会被修改)
'   level - 当前层级深度(0=根类别,1=一级子类别...)
' 说明:
'   - 使用递归方式遍历整个类别树
'   - 根据level参数计算缩进空格数
'   - 先打印类别名,再打印该类别的所有物料,最后递归打印子类别
' ========================================
Private Sub PrintCategory(ws As Worksheet, cat As clsCategory, _
                         ByRef row As Long, level As Integer)
    ' 计算缩进(每层2个空格)
    Dim indent As String
    indent = String(level * 2, " ")

    ' 打印类别名称和物料数量统计
    ws.Cells(row, 1).value = indent & cat.categoryName & _
                            " (物料数:" & cat.materials.Count & ")"
    row = row + 1

    ' 打印该类别下的所有物料
    Dim mat As clsMaterialItem
    Dim i As Long
    For i = 1 To cat.materials.Count
        Set mat = cat.materials(i)
        ' 物料行额外缩进2个空格,并加上"- "前缀
        ws.Cells(row, 1).value = indent & "  - " & mat.code & " " & mat.Name
        ws.Cells(row, 2).value = "数量:" & mat.Quantity
        ws.Cells(row, 3).value = "条件:" & mat.Condition
        row = row + 1
    Next i

    ' 递归打印所有子类别
    Dim subCat As clsCategory
    Dim j As Long
    For j = 1 To cat.SubCategories.Count
        Set subCat = cat.SubCategories(j)
        ' 递归调用,层级加1
        Call PrintCategory(ws, subCat, row, level + 1)
    Next j
End Sub
' ========================================
' GetMaterialsByModel 方法
'
' 【功能概述】
'   根据产品型号字符串自动解析规格参数,并返回符合这些规格的所有物料清单。
'   这是BOM系统的核心方法,实现了从"产品型号"到"物料清单"的智能转换。
'
' 【工作流程】
'   1. 型号解析 (clsModelParser) → 将产品型号字符串拆解为结构化参数
'   2. 条件提取 (clsConditionExtractor) → 从型号参数中提取匹配条件变量
'   3. 物料筛选 (clsConditionMatcher) → 根据条件从物料库中筛选符合条件的物料
'
' 【参数说明】
'   modelStr - 产品型号字符串
'              格式: [型号]-[口径].[安装].[表壳].[连接].[量程]|[表盘]|[附件]|[法兰]
'              示例: "YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3"
'              说明: 必须是完整的型号字符串,不能为空
'
'   categoryName - 类别名称(可选参数,默认为空字符串)
'              为空字符串 "": 返回所有类别的符合条件物料(默认行为)
'              指定类别名:  仅返回该类别下的符合条件物料
'              示例: "部件"、"表壳"、"接头"、"机芯"
'
'   autoFallback - 是否自动降级到子类别(可选参数,默认为True)
'              True (默认): 当某个类别无匹配物料时,自动降级到其子类别继续查找
'              False:       仅在当前类别查找,不降级到子类别
'              示例场景:
'                型号 "YTHN-100.A0.532.M203.M17.Y3" 的量程是M17
'                类别"部件"下的物料条件是 "lcfw=M02"
'                autoFallback=True:  自动降级到子类别"接头"、"弹性元件"等查找
'                autoFallback=False: "部件"类别返回空集合
'
' 【返回值】
'   返回类型: Collection对象
'   元素类型: Collection中的每个元素都是 clsMaterialItem 对象
'
'   clsMaterialItem 对象属性:
'     .code      - String  - 物料代号(如: "01011019001")
'     .Name      - String  - 物料名称(如: "低压接头部件")
'     .Quantity  - Double  - 物料数量(如: 1, 2, 0.5)
'     .Condition - String  - 选择条件(如: "lcfw=M02 AND gclj=Z12")
'     .Category  - String  - 所属类别名称
'     .ParentCategory - String - 上层类别名称
'
' 【注意事项】
'   1. 调用前必须先执行 LoadData() 方法加载BOM数据
'   2. 型号字符串必须完整且符合格式要求
'   3. 返回的Collection可能为空(没有符合条件的物料),需要判断Count属性
'   4. 调试信息会输出到VBA的"立即窗口"(Ctrl+G查看)
'   5. 物料的Condition属性为空表示该物料无条件限制(所有型号都使用)
'   6. autoFallback 参数影响查找范围:
'      - True (默认): 会返回父类别和子类别的物料,更全面但可能包含不需要的物料
'      - False: 仅返回指定类别的物料,更精确但可能遗漏子类别的替代物料
'
' 【相关方法】
'   - GetMaterialsByCategoryAndModel: 结合了类别层级逻辑的物料获取
'   - GetMaterialsForPicking: 纯粹按类别获取物料(不进行型号匹配)
'   - ParseModelAndExtractConditions: 仅解析型号并返回条件字典
' ========================================
Public Function GetMaterialsByModel(modelStr As String, _
                                   Optional categoryName As String = "", _
                                   Optional autoFallback As Boolean = True) As collection
    Dim result As collection
    Set result = New collection

    ' ========================================
    ' 第1步: 解析型号并提取条件
    ' ========================================
    Dim parser As New clsModelParser
    If Not parser.ParseModel(modelStr) Then
        Debug.Print "型号解析失败: " & parser.ErrorMessage
        Set GetMaterialsByModel = result
        Exit Function
    End If

    Dim extractor As New clsConditionExtractor
    Dim conditions As Object
    Set conditions = extractor.ExtractConditions(parser)

    ' 调试输出
    Debug.Print "【型号】: " & modelStr
    Debug.Print "【提取的条件】:"
    Dim key As Variant
    For Each key In conditions.Keys
        Debug.Print "  " & key & " = " & conditions(key)
    Next key
    Debug.Print ""

    ' ========================================
    ' 第2步: 按类别筛选物料(根据参数决定是否自动降级到子类别)
    ' ========================================
    Dim matcher As New clsConditionMatcher
    Dim totalMatchCount As Long
    Dim catResult As collection  ' 提前声明避免在If/Else中重复声明
    Dim cat As clsCategory
    Dim rootCat As clsCategory
    Dim i As Long, j As Long
    totalMatchCount = 0

    If autoFallback Then
        Debug.Print "【降级模式】启用自动降级到子类别"
    Else
        Debug.Print "【降级模式】禁用自动降级,仅查找当前类别"
    End If
    Debug.Print ""

    If categoryName <> "" Then
        ' ========================================
        ' 情况A: 指定了类别名称
        ' 只在该类别及其子类别中查找
        ' ========================================
        Set cat = GetCategory(categoryName)

        If Not cat Is Nothing Then
            ' 从该类别开始查找(根据参数决定是否启用子类别降级逻辑)
            Set catResult = FilterCategoryWithSubcategories(cat, matcher, conditions, autoFallback)

            ' 合并结果
            For i = 1 To catResult.Count
                result.Add catResult(i)
            Next i
            totalMatchCount = catResult.Count
        End If
    Else
        ' ========================================
        ' 情况B: 未指定类别名称
        ' 遍历所有根类别,对每个类别应用子类别降级逻辑
        ' ========================================
        For j = 1 To rootCategories.Count
            Set rootCat = rootCategories(j)

            ' 对每个根类别应用筛选(根据参数决定是否启用子类别降级逻辑)
            Set catResult = FilterCategoryWithSubcategories(rootCat, matcher, conditions, autoFallback)

            ' 合并结果
            For i = 1 To catResult.Count
                result.Add catResult(i)
            Next i
            totalMatchCount = totalMatchCount + catResult.Count
        Next j
    End If

    Debug.Print "共匹配 " & totalMatchCount & " 个物料"
    Debug.Print String(60, "=")

    Set GetMaterialsByModel = result
End Function

' ========================================
' FilterCategoryWithSubcategories 方法 (私有)
' 功能: 对指定类别进行筛选,根据参数决定是否自动降级到子类别
' 参数:
'   cat          - 类别对象
'   matcher      - 条件匹配器对象
'   conditions   - 提取的条件字典
'   autoFallback - 是否自动降级到子类别默认True
'                  True:  当前类别无匹配时,自动降级到子类别查找
'                  False: 仅在当前类别查找,不降级到子类别
' 返回: Collection对象包含匹配的物料
'
' 工作逻辑:
'   1. 先尝试在当前类别下筛选物料
'   2. 如果当前类别有匹配结果,直接返回
'   3. 如果当前类别无匹配结果且 autoFallback=True检查是否有子类别
'   4. 如果有子类别且允许降级,递归对所有子类别进行筛选
'   5. 如果无子类别或不允许降级,返回当前结果
'
' 示例场景:
'   型号: "YTHN-100.A0.532.M203.M16.Y3"
'   类别"部件"下有物料"低压接头部件"(条件: lcfw=M02
'   如果该型号的量程不是M02则"部件"类别无匹配
'   当 autoFallback=True 时,自动降级到子类别"接头"、"弹性元件"等查找
'   当 autoFallback=False 时,返回空集合,不查找子类别
' ========================================
Private Function FilterCategoryWithSubcategories(ByVal cat As clsCategory, _
                                                 ByVal matcher As clsConditionMatcher, _
                                                 ByVal conditions As Object, _
                                                 ByVal autoFallback As Boolean) As collection
    Dim result As collection
    Set result = New collection

    ' 类别不存在,返回空集合
    If cat Is Nothing Then
        Set FilterCategoryWithSubcategories = result
        Exit Function
    End If

    ' ========================================
    ' 第一阶段: 尝试在当前类别下筛选
    ' ========================================
    Dim mat As clsMaterialItem
    Dim i As Long
    Dim matchCount As Long
    matchCount = 0

    ' 遍历当前类别的所有物料进行筛选
    For i = 1 To cat.materials.Count
        Set mat = cat.materials(i)
        If matcher.IsMatch(mat.Condition, conditions) Then
            result.Add mat
            matchCount = matchCount + 1

            ' 调试输出
            Debug.Print "【匹配】" & cat.categoryName & " > " & _
                       mat.code & " - " & mat.Name & _
                       " | 条件: " & IIf(mat.Condition = "", "(无)", mat.Condition)
        End If
    Next i

    ' ========================================
    ' 第二阶段: 如果当前类别无匹配且允许降级,检查子类别
    ' ========================================
    If matchCount = 0 And cat.HasSubCategories And autoFallback Then
        Debug.Print "【降级】类别 """ & cat.categoryName & """ 无匹配物料,降级到子类别查找..."

        ' 递归处理所有子类别
        Dim subCat As clsCategory
        Dim j As Long
        For j = 1 To cat.SubCategories.Count
            Set subCat = cat.SubCategories(j)

            ' 递归调用,获取子类别的匹配结果(传递相同的 autoFallback 参数)
            Dim subResult As collection
            Set subResult = FilterCategoryWithSubcategories(subCat, matcher, conditions, autoFallback)

            ' 合并子类别的结果
            Dim k As Long
            For k = 1 To subResult.Count
                result.Add subResult(k)
            Next k
        Next j
    ElseIf matchCount = 0 And cat.HasSubCategories And Not autoFallback Then
        Debug.Print "【跳过】类别 """ & cat.categoryName & """ 无匹配物料,但降级已禁用,不查找子类别"
    ElseIf matchCount > 0 Then
        Debug.Print "【成功】类别 """ & cat.categoryName & """ 匹配 " & matchCount & " 个物料"
    End If

    Set FilterCategoryWithSubcategories = result
End Function

' ========================================
' CollectAllMaterials 方法 (私有)
' 功能: 递归收集类别及其所有子类别的物料
' 参数:
'   cat - 类别对象
'   collection - 用于存储物料的Collection
' ========================================
Private Sub CollectAllMaterials(cat As clsCategory, collection As collection)
    Dim i As Long

    ' 添加当前类别的所有物料
    For i = 1 To cat.materials.Count
        collection.Add cat.materials(i)
    Next i

    ' 递归处理子类别
    Dim subCat As clsCategory
    Dim j As Long
    For j = 1 To cat.SubCategories.Count
        Set subCat = cat.SubCategories(j)
        Call CollectAllMaterials(subCat, collection)
    Next j
End Sub

' ========================================
' GetMaterialsByCategoryAndModel 方法
' 功能: 根据类别和型号获取物料(使用父类别或子类别逻辑)
' 参数:
'   modelStr - 产品型号字符串
'   categoryName - 类别名称
'   useParent - True=使用父类别物料False=使用子类别物料
' 返回: Collection对象包含符合条件的clsMaterialItem对象
' 说明: 这是 GetMaterialsForPicking 和 GetMaterialsByModel 的结合
' ========================================
Public Function GetMaterialsByCategoryAndModel(modelStr As String, _
                                              categoryName As String, _
                                              Optional useParent As Boolean = True) As collection
    Dim result As collection
    Set result = New collection

    ' 1. 解析型号并提取条件
    Dim parser As New clsModelParser
    If Not parser.ParseModel(modelStr) Then
        Set GetMaterialsByCategoryAndModel = result
        Exit Function
    End If

    Dim extractor As New clsConditionExtractor
    Dim conditions As Object
    Set conditions = extractor.ExtractConditions(parser)

    ' 2. 获取类别的物料(根据 useParent 参数)
    Dim materialsToFilter As collection
    Set materialsToFilter = GetMaterialsForPicking(categoryName, useParent)

    ' 3. 筛选符合条件的物料
    Dim matcher As New clsConditionMatcher
    Dim mat As clsMaterialItem

    For Each mat In materialsToFilter
        If matcher.IsMatch(mat.Condition, conditions) Then
            result.Add mat
        End If
    Next mat

    Set GetMaterialsByCategoryAndModel = result
End Function

' ========================================
' ParseModelAndExtractConditions 方法
' 功能: 解析型号并返回条件字典(工具方法)
' 参数: modelStr - 产品型号字符串
' 返回: Dictionary对象包含提取的条件
' 用途: 供外部调用,用于查看提取的条件
' ========================================
Public Function ParseModelAndExtractConditions(modelStr As String) As Object
    Dim parser As New clsModelParser
    Dim extractor As New clsConditionExtractor
    Dim conditions As Object

    If parser.ParseModel(modelStr) Then
        Set conditions = extractor.ExtractConditions(parser)
    Else
        Set conditions = CreateObject("Scripting.Dictionary")
    End If

    Set ParseModelAndExtractConditions = conditions
End Function

' ========================================
' GetValidMaterialsByModel 方法
' 功能: 根据产品型号获取物料,并判断是否为完整的物料集合
' 参数:
'   modelStr - 产品型号字符串
' 返回: Collection对象
'   Collection中包含两个元素:
'     (1) "Materials" - Collection对象,包含所有匹配的 clsMaterialItem 对象
'     (2) "IsComplete" - Boolean值,指示物料是否完整
'
' 完整性判断规则:
'   - 对于每个需要领料的类别(顶层类别或其需要领取的子类别)
'   - 必须有且仅有一个物料被匹配
'   - 如果某个类别有0个或多于1个物料,则视为不完整
'
' 示例:
'   Dim result As Collection
'   Set result = bomMgr.GetValidMaterialsByModel("YTHN-100.A0.532.M203.M16.Y3")
'   Dim materials As Collection
'   Dim isComplete As Boolean
'   Set materials = result("Materials")
'   isComplete = result("IsComplete")
' ========================================
Public Function GetValidMaterialsByModel(modelStr As String) As collection
    Dim result As New collection
    Dim allMaterials As New collection
    Dim isComplete As Boolean
    
    ' 解析型号并提取条件
    Dim parser As New clsModelParser
    If Not parser.ParseModel(modelStr) Then
        Debug.Print "型号解析失败: " & parser.ErrorMessage
        isComplete = False
        result.Add allMaterials, "Materials"
        result.Add isComplete, "IsComplete"
        Set GetValidMaterialsByModel = result
        Exit Function
    End If
    
    Dim extractor As New clsConditionExtractor
    Dim conditions As Object
    Set conditions = extractor.ExtractConditions(parser)
    
    ' 调试输出
    Debug.Print "【GetValidMaterialsByModel】"
    Debug.Print "型号: " & modelStr
    Debug.Print "提取的条件:"
    Dim key As Variant
    For Each key In conditions.Keys
        Debug.Print "  " & key & " = " & conditions(key)
    Next key
    Debug.Print ""
    
    ' 条件匹配器
    Dim matcher As New clsConditionMatcher
    
    ' 遍历所有根类别,检查完整性
    isComplete = True
    Dim categoryCheckResults As Object
    Set categoryCheckResults = CreateObject("Scripting.Dictionary")
    
    Dim rootCat As clsCategory
    Dim i As Long
    For i = 1 To rootCategories.count
        Set rootCat = rootCategories(i)
        
        ' 检查该类别及其子类别的完整性
        Dim catResult As Object
        Set catResult = CheckCategoryCompleteness(rootCat, matcher, conditions)
        
        ' 合并物料
        Dim mat As clsMaterialItem
        Dim matCollection As collection
        Set matCollection = catResult("Materials")
        For Each mat In matCollection
            allMaterials.Add mat
        Next mat
        
        ' 检查完整性
        If Not catResult("IsComplete") Then
            isComplete = False
            Debug.Print "【不完整】类别 """ & rootCat.categoryName & """ 物料不完整"
        End If
        
        ' 保存类别检查结果(用于调试)
        categoryCheckResults(rootCat.categoryName) = catResult("IsComplete")
    Next i
    
    ' 调试输出总结
    Debug.Print ""
    Debug.Print "【完整性检查结果】"
    Debug.Print "总物料数: " & allMaterials.count
    Debug.Print "是否完整: " & IIf(isComplete, "是", "否")
    For Each key In categoryCheckResults.Keys
        Debug.Print "  " & key & ": " & IIf(categoryCheckResults(key), "完整", "不完整")
    Next key
    Debug.Print String(60, "=")
    
    ' 返回结果
    result.Add allMaterials, "Materials"
    result.Add isComplete, "IsComplete"
    Set GetValidMaterialsByModel = result
End Function

' ========================================
' CheckCategoryCompleteness 方法 (私有)
' 功能: 检查单个类别的完整性(递归处理子类别)
' 参数:
'   cat - 类别对象
'   matcher - 条件匹配器
'   conditions - 提取的条件字典
' 返回: Dictionary对象
'   "Materials" - Collection,包含该类别匹配的物料
'   "IsComplete" - Boolean,该类别是否完整
'
' 完整性判断逻辑:
'   1. 如果类别没有子类别(叶子类别):
'      - 必须有且仅有1个物料匹配 → 完整
'      - 0个或多于1个物料 → 不完整
'
'   2. 如果类别有子类别:
'      a) 先尝试在父类别查找物料
'      b) 如果父类别有且仅有1个匹配物料 → 使用父类别,完整
'      c) 如果父类别没有匹配物料 → 降级到所有子类别
'         - 每个子类别都必须有且仅有1个匹配物料 → 完整
'         - 任一子类别不满足 → 不完整
' ========================================
Private Function CheckCategoryCompleteness(cat As clsCategory, _
                                          matcher As clsConditionMatcher, _
                                          conditions As Object) As Object
    Dim result As Object
    Set result = CreateObject("Scripting.Dictionary")
    
    Dim materials As New collection
    Dim isComplete As Boolean
    
    ' 首先在当前类别查找匹配的物料
    Dim mat As clsMaterialItem
    Dim matchCount As Long
    matchCount = 0
    
    Dim i As Long
    For i = 1 To cat.materials.count
        Set mat = cat.materials(i)
        If matcher.IsMatch(mat.Condition, conditions) Then
            materials.Add mat
            matchCount = matchCount + 1
        End If
    Next i
    
    ' 判断完整性
    If Not cat.HasSubCategories Then
        ' ========================================
        ' 情况1: 叶子类别(无子类别)
        ' 必须有且仅有1个物料
        ' ========================================
        If matchCount = 1 Then
            isComplete = True
            Debug.Print "【完整】叶子类别 """ & cat.categoryName & """ 有1个匹配物料"
        Else
            isComplete = False
            If matchCount = 0 Then
                Debug.Print "【不完整】叶子类别 """ & cat.categoryName & """ 无匹配物料"
            Else
                Debug.Print "【不完整】叶子类别 """ & cat.categoryName & """ 有" & matchCount & "个匹配物料(应为1个)"
            End If
        End If
    Else
        ' ========================================
        ' 情况2: 有子类别
        ' 先检查父类别,如果父类别满足则使用父类别
        ' 否则降级到子类别,每个子类别都必须满足
        ' ========================================
        If matchCount = 1 Then
            ' 父类别有且仅有1个物料,使用父类别
            isComplete = True
            Debug.Print "【完整】父类别 """ & cat.categoryName & """ 有1个匹配物料,使用父类别"
        ElseIf matchCount = 0 Then
            ' 父类别无匹配物料,降级到子类别
            Debug.Print "【降级】父类别 """ & cat.categoryName & """ 无匹配物料,检查子类别..."
            
            ' 清空物料集合,准备收集子类别物料
            Set materials = New collection
            isComplete = True  ' 假设完整,如果任一子类别不完整则设为False
            
            Dim subCat As clsCategory
            Dim j As Long
            For j = 1 To cat.SubCategories.count
                Set subCat = cat.SubCategories(j)
                
                ' 递归检查子类别
                Dim subResult As Object
                Set subResult = CheckCategoryCompleteness(subCat, matcher, conditions)
                
                ' 合并子类别物料
                Dim subMaterials As collection
                Set subMaterials = subResult("Materials")
                Dim k As Long
                For k = 1 To subMaterials.count
                    materials.Add subMaterials(k)
                Next k
                
                ' 检查子类别是否完整
                If Not subResult("IsComplete") Then
                    isComplete = False
                End If
            Next j
            
            If isComplete Then
                Debug.Print "【完整】类别 """ & cat.categoryName & """ 所有子类别都完整"
            Else
                Debug.Print "【不完整】类别 """ & cat.categoryName & """ 存在不完整的子类别"
            End If
        Else
            ' 父类别有多个匹配物料,不完整
            isComplete = False
            Debug.Print "【不完整】父类别 """ & cat.categoryName & """ 有" & matchCount & "个匹配物料(应为0或1个)"
        End If
    End If
    
    ' 返回结果
    Set result("Materials") = materials
    result("IsComplete") = isComplete
    Set CheckCategoryCompleteness = result
End Function

ClassModules\clsCategory.cls

' ========================================
' 类模块: clsCategory
' 用途: 表示物料类别及其层级关系
' ========================================
Option Explicit

Public categoryName As String
Public ParentCategoryName As String
Public materials As collection      ' 存储 clsMaterialItem 对象
Public SubCategories As collection  ' 存储子类别 clsCategory 对象
Public IsLeafCategory As Boolean    ' 是否叶子类别(无子类别)

Private Sub Class_Initialize()
    Set materials = New collection
    Set SubCategories = New collection
    IsLeafCategory = True
End Sub

' 添加物料
Public Sub AddMaterial(mat As clsMaterialItem)
    materials.Add mat, mat.code
End Sub

' 添加子类别
Public Sub AddSubCategory(cat As clsCategory)
    SubCategories.Add cat, cat.categoryName
    IsLeafCategory = False
End Sub

' 获取物料(按代号)
Public Function GetMaterial(code As String) As clsMaterialItem
    On Error Resume Next
    Set GetMaterial = materials(code)
    On Error GoTo 0
End Function

' 检查是否有子类别
Public Function HasSubCategories() As Boolean
    HasSubCategories = (SubCategories.Count > 0)
End Function

ClassModules\clsConditionExtractor.cls

' ========================================
' 类模块: clsConditionExtractor
' 用途: 从型号解析器中提取物料选择条件
' ========================================
Option Explicit

' ========================================
' 私有成员变量
' ========================================
Private m_Conditions As Object      ' Dictionary: 变量名 -> 条件值
Private m_ExtractionRules As Object ' Dictionary: 提取规则配置

' ========================================
' 类初始化
' ========================================
Private Sub Class_Initialize()
    Set m_Conditions = CreateObject("Scripting.Dictionary")
    Set m_ExtractionRules = CreateObject("Scripting.Dictionary")

    ' 初始化提取规则
    InitializeRules
End Sub

' ========================================
' InitializeRules 方法 (私有)
' 功能: 初始化条件提取规则
' 说明: 这里配置所有需要提取的条件及其提取方法
' ========================================
Private Sub InitializeRules()
    ' 规则格式: Dictionary(变量名) = Array(源字段, 提取方法)

    ' 规则1: 过程连接 (gclj)
    ' 从 ConnectionCode 中提取,去掉最后一位数字
    m_ExtractionRules("gclj") = Array("ConnectionCode", "RemoveLastDigit")

    ' 规则2: 接液材质 (jycz)
    ' 从 ConnectionCode 中提取,取最后一位数字
    m_ExtractionRules("jycz") = Array("ConnectionCode", "GetLastDigit")

    ' 规则3: 量程范围 (lcfw)
    ' 从 RangeCode 中直接提取
    m_ExtractionRules("lcfw") = Array("RangeCode", "Direct")

    ' 未来可以在这里添加更多提取规则...
    ' 例如:
    ' m_ExtractionRules("bplx") = Array("DialCode", "Direct")  ' 表盘类型
End Sub

' ========================================
' ExtractConditions 方法
' 功能: 从型号解析器中提取所有条件
' 参数: parser - clsModelParser对象
' 返回: Dictionary对象包含所有提取的条件
' ========================================
Public Function ExtractConditions(parser As clsModelParser) As Object
    ' 清空现有条件
    Set m_Conditions = CreateObject("Scripting.Dictionary")

    ' 验证解析器有效性
    If Not parser.IsValid Then
        Set ExtractConditions = m_Conditions
        Exit Function
    End If

    ' 遍历所有提取规则
    Dim varName As Variant
    For Each varName In m_ExtractionRules.Keys
        Dim ruleInfo As Variant
        ruleInfo = m_ExtractionRules(varName)

        Dim sourceField As String
        Dim extractMethod As String
        sourceField = ruleInfo(0)
        extractMethod = ruleInfo(1)

        ' 提取条件值
        Dim conditionValue As String
        conditionValue = ExtractValue(parser, sourceField, extractMethod)

        ' 添加到条件字典
        If conditionValue <> "" Then
            m_Conditions(CStr(varName)) = conditionValue
        End If
    Next varName

    Set ExtractConditions = m_Conditions
End Function

' ========================================
' ExtractValue 方法 (私有)
' 功能: 根据规则从解析器中提取单个值
' 参数:
'   parser - clsModelParser对象
'   sourceField - 源字段名称
'   extractMethod - 提取方法名称
' 返回: 提取的条件值
' ========================================
Private Function ExtractValue(parser As clsModelParser, _
                              sourceField As String, _
                              extractMethod As String) As String
    Dim sourceValue As String

    ' 获取源字段值
    Select Case sourceField
        Case "ConnectionCode"
            sourceValue = parser.ConnectionCode
        Case "RangeCode"
            sourceValue = parser.RangeCode
        Case "ModelType"
            sourceValue = parser.ModelType
        Case "Diameter"
            sourceValue = parser.Diameter
        Case "InstallForm"
            sourceValue = parser.InstallForm
        Case "ShellForm"
            sourceValue = parser.ShellForm
        Case "Characteristics"
            sourceValue = parser.Characteristics
        Case Else
            sourceValue = ""
    End Select

    ' 应用提取方法
    Select Case extractMethod
        Case "Direct"
            ' 直接使用
            ExtractValue = sourceValue

        Case "RemoveLastDigit"
            ' 去掉最后一位字符
            If Len(sourceValue) > 1 Then
                ExtractValue = Left(sourceValue, Len(sourceValue) - 1)
            Else
                ExtractValue = sourceValue
            End If

        Case "GetLastDigit"
            ' 取最后一位字符
            If Len(sourceValue) > 0 Then
                ExtractValue = Right(sourceValue, 1)
            Else
                ExtractValue = ""
            End If

        Case "GetFirstChar"
            ' 取第一个字符
            If Len(sourceValue) > 0 Then
                ExtractValue = Left(sourceValue, 1)
            Else
                ExtractValue = ""
            End If

        Case Else
            ' 未知方法,返回空
            ExtractValue = ""
    End Select
End Function

' ========================================
' GetConditionValue 方法
' 功能: 获取单个条件值
' 参数: varName - 变量名
' 返回: 条件值,如果不存在返回空字符串
' ========================================
Public Function GetConditionValue(varName As String) As String
    If m_Conditions.Exists(varName) Then
        GetConditionValue = m_Conditions(varName)
    Else
        GetConditionValue = ""
    End If
End Function

' ========================================
' AddCondition 方法
' 功能: 手动添加条件 (用于特殊情况)
' 参数:
'   varName - 变量名
'   value - 条件值
' ========================================
Public Sub AddCondition(varName As String, value As String)
    m_Conditions(varName) = value
End Sub

' ========================================
' GetConditions 属性
' 功能: 获取所有条件的Dictionary对象
' ========================================
Public Property Get conditions() As Object
    Set conditions = m_Conditions
End Property

' ========================================
' ToString 方法
' 功能: 返回条件的字符串表示 (用于调试)
' ========================================
Public Function ToString() As String
    Dim result As String
    result = "【提取的条件】" & vbCrLf

    If m_Conditions.Count = 0 Then
        result = result & "  (无条件)" & vbCrLf
    Else
        Dim key As Variant
        For Each key In m_Conditions.Keys
            result = result & "  " & key & " = " & m_Conditions(key) & vbCrLf
        Next key
    End If

    ToString = result
End Function

' ========================================
' AddExtractionRule 方法
' 功能: 动态添加新的提取规则 (用于扩展)
' 参数:
'   varName - 变量名
'   sourceField - 源字段名称
'   extractMethod - 提取方法名称
' 示例: extractor.AddExtractionRule "bplx", "DialCode", "Direct"
' ========================================
Public Sub AddExtractionRule(varName As String, _
                             sourceField As String, _
                             extractMethod As String)
    m_ExtractionRules(varName) = Array(sourceField, extractMethod)
End Sub

' ========================================
' GetExtractionRules 方法
' 功能: 获取当前所有提取规则 (用于调试)
' 返回: Dictionary对象
' ========================================
Public Function GetExtractionRules() As Object
    Set GetExtractionRules = m_ExtractionRules
End Function

ClassModules\clsConditionMatcher.cls

' ========================================
' 类模块: clsConditionMatcher
' 用途: 解析物料的选择条件表达式,并判断是否匹配
' 支持: AND, OR, NOT(!=), 括号优先级
' ========================================
Option Explicit

' ========================================
' IsMatch 方法
' 功能: 判断条件表达式是否匹配
' 参数:
'   conditionExpr - 条件表达式字符串
'   conditions - Dictionary对象包含变量名->值的映射
' 返回: Boolean - 是否匹配
' 示例:
'   IsMatch("lcfw=M16 AND gclj=M20", conditions) -> True/False
' ========================================
Public Function IsMatch(conditionExpr As String, conditions As Object) As Boolean
    On Error GoTo ErrorHandler

    ' 空条件表示无条件,始终匹配
    If Trim(conditionExpr) = "" Then
        IsMatch = True
        Exit Function
    End If

    ' 解析并计算表达式
    IsMatch = EvaluateExpression(Trim(conditionExpr), conditions)
    Exit Function

ErrorHandler:
    ' 出错时返回False保守处理
    Debug.Print "条件匹配出错: " & conditionExpr & " - " & Err.description
    IsMatch = False
End Function

' ========================================
' EvaluateExpression 方法 (私有)
' 功能: 递归计算逻辑表达式
' 优先级: 括号 > NOT(!=) > AND > OR
' ========================================
Private Function EvaluateExpression(expr As String, conditions As Object) As Boolean
    expr = Trim(expr)

    ' 处理括号 (最高优先级)
    If InStr(expr, "(") > 0 Then
        EvaluateExpression = EvaluateWithParentheses(expr, conditions)
        Exit Function
    End If

    ' 处理 OR (最低优先级)
    If InStr(expr, " OR ") > 0 Then
        EvaluateExpression = EvaluateOR(expr, conditions)
        Exit Function
    End If

    ' 处理 AND (中等优先级)
    If InStr(expr, " AND ") > 0 Then
        EvaluateExpression = EvaluateAND(expr, conditions)
        Exit Function
    End If

    ' 处理单个条件 (最高优先级)
    EvaluateExpression = EvaluateSimpleCondition(expr, conditions)
End Function

' ========================================
' EvaluateWithParentheses 方法 (私有)
' 功能: 处理包含括号的表达式
' 策略: 找到最内层括号,递归计算,然后替换为结果
' ========================================
Private Function EvaluateWithParentheses(expr As String, conditions As Object) As Boolean
    Dim pos As Long, level As Long, startPos As Long
    Dim i As Long
    Dim innerExpr As String
    Dim innerResult As Boolean
    Dim newExpr As String

    ' 查找最内层的括号对
    startPos = 0
    level = 0

    For i = 1 To Len(expr)
        If Mid(expr, i, 1) = "(" Then
            If level = 0 Then startPos = i
            level = level + 1
        ElseIf Mid(expr, i, 1) = ")" Then
            level = level - 1
            If level = 0 And startPos > 0 Then
                ' 找到一对括号
                innerExpr = Mid(expr, startPos + 1, i - startPos - 1)
                innerResult = EvaluateExpression(innerExpr, conditions)

                ' 替换括号部分为结果
                newExpr = Left(expr, startPos - 1) & _
                         IIf(innerResult, "TRUE", "FALSE") & _
                         Mid(expr, i + 1)

                ' 递归处理剩余部分
                EvaluateWithParentheses = EvaluateExpression(newExpr, conditions)
                Exit Function
            End If
        End If
    Next i

    ' 如果没有找到有效括号,直接计算
    EvaluateWithParentheses = EvaluateExpression(expr, conditions)
End Function

' ========================================
' EvaluateOR 方法 (私有)
' 功能: 处理 OR 逻辑运算
' 规则: 任一为真则为真
' ========================================
Private Function EvaluateOR(expr As String, conditions As Object) As Boolean
    Dim parts() As String
    Dim part As Variant

    ' 按 OR 分割
    parts = Split(expr, " OR ")

    ' 任一部分为真则返回真
    For Each part In parts
        If EvaluateExpression(Trim(CStr(part)), conditions) Then
            EvaluateOR = True
            Exit Function
        End If
    Next part

    EvaluateOR = False
End Function

' ========================================
' EvaluateAND 方法 (私有)
' 功能: 处理 AND 逻辑运算
' 规则: 全部为真才为真
' ========================================
Private Function EvaluateAND(expr As String, conditions As Object) As Boolean
    Dim parts() As String
    Dim part As Variant

    ' 按 AND 分割
    parts = Split(expr, " AND ")

    ' 全部部分为真才返回真
    For Each part In parts
        If Not EvaluateExpression(Trim(CStr(part)), conditions) Then
            EvaluateAND = False
            Exit Function
        End If
    Next part

    EvaluateAND = True
End Function

' ========================================
' EvaluateSimpleCondition 方法 (私有)
' 功能: 计算单个条件表达式
' 支持: = (等于), != (不等于)
' 格式: varName=value 或 varName!=value
' ========================================
Private Function EvaluateSimpleCondition(cond As String, conditions As Object) As Boolean
    cond = Trim(cond)

    ' 处理特殊值 TRUE/FALSE (括号计算的结果)
    If UCase(cond) = "TRUE" Then
        EvaluateSimpleCondition = True
        Exit Function
    ElseIf UCase(cond) = "FALSE" Then
        EvaluateSimpleCondition = False
        Exit Function
    End If

    Dim varName As String
    Dim expectedValue As String
    Dim actualValue As String
    Dim isNotEqual As Boolean

    ' 判断是 != 还是 =
    If InStr(cond, "!=") > 0 Then
        isNotEqual = True
        Dim parts1() As String
        parts1 = Split(cond, "!=")
        If UBound(parts1) < 1 Then
            EvaluateSimpleCondition = False
            Exit Function
        End If
        varName = Trim(parts1(0))
        expectedValue = Trim(parts1(1))
    ElseIf InStr(cond, "=") > 0 Then
        isNotEqual = False
        Dim parts2() As String
        parts2 = Split(cond, "=")
        If UBound(parts2) < 1 Then
            EvaluateSimpleCondition = False
            Exit Function
        End If
        varName = Trim(parts2(0))
        expectedValue = Trim(parts2(1))
    Else
        ' 无效的条件格式
        EvaluateSimpleCondition = False
        Exit Function
    End If

    ' 获取实际值
    If conditions.Exists(varName) Then
        actualValue = Trim(CStr(conditions(varName)))
    Else
        actualValue = ""
    End If

    ' 比较值 (不区分大小写)
    Dim isEqual As Boolean
    isEqual = (UCase(actualValue) = UCase(expectedValue))

    ' 返回结果
    If isNotEqual Then
        EvaluateSimpleCondition = Not isEqual
    Else
        EvaluateSimpleCondition = isEqual
    End If
End Function

' ========================================
' TestExpression 方法
' 功能: 测试表达式是否有效 (用于调试)
' 参数: expr - 表达式字符串
' 返回: String - "有效" 或 错误信息
' ========================================
Public Function TestExpression(expr As String) As String
    On Error GoTo ErrorHandler

    ' 创建测试条件
    Dim testConditions As Object
    Set testConditions = CreateObject("Scripting.Dictionary")
    testConditions("gclj") = "M20"
    testConditions("jycz") = "3"
    testConditions("lcfw") = "M16"

    ' 尝试计算
    Dim result As Boolean
    result = IsMatch(expr, testConditions)

    TestExpression = "有效 (结果: " & IIf(result, "True", "False") & ")"
    Exit Function

ErrorHandler:
    TestExpression = "无效: " & Err.description
End Function

ClassModules\clsMaterialItem.cls

' ========================================
' 类模块: clsMaterialItem
' 用途: 表示单个物料项
' ========================================
Option Explicit

Public code As String           ' 代号
Public Name As String           ' 名称
Public Quantity As Double       ' 数量
Public Condition As String      ' 选择条件
Public Category As String       ' 类别
Public ParentCategory As String ' 上层类别

ClassModules\clsModelParser.cls

' ========================================
' 类模块: clsModelParser
' 用途: 解析产品型号,提取各部分代码
' 示例: YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3
' ========================================
Option Explicit

' ========================================
' 公共属性
' ========================================
Public RawModel As String           ' 原始完整型号
Public HeaderModel As String        ' 表头型号部分
Public DialModel As String          ' 表盘型号部分
Public AccessoryModel As String     ' 附件型号部分
Public FlangeModel As String        ' 法兰隔膜型号部分

' 表头各部分
Public ModelType As String          ' 型号 (如 YTHN)
Public Diameter As String           ' 公称外径 (如 100)
Public InstallForm As String        ' 安装形式 (如 A0)
Public ShellForm As String          ' 壳体形式 (如 532)
Public ConnectionCode As String     ' 过程连接&材质代码 (如 M203)
Public RangeCode As String          ' 量程范围代码 (如 M16)
Public Characteristics As String    ' 仪表特性 (如 Y3)

' ========================================
' 私有变量
' ========================================
Private m_IsValid As Boolean        ' 解析是否成功
Private m_ErrorMessage As String    ' 错误信息

' ========================================
' ParseModel 方法
' 功能: 解析产品型号字符串
' 参数: modelStr - 完整的产品型号字符串
' 返回: Boolean - 解析是否成功
' 示例: parser.ParseModel("YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3")
' ========================================
Public Function ParseModel(modelStr As String) As Boolean
    On Error GoTo ErrorHandler

    ' 初始化
    m_IsValid = False
    m_ErrorMessage = ""
    RawModel = Trim(modelStr)

    ' 验证输入
    If RawModel = "" Then
        m_ErrorMessage = "型号字符串为空"
        ParseModel = False
        Exit Function
    End If

    ' 第一步: 按 | 分割各部分
    Dim parts() As String
    parts = Split(RawModel, "|")

    If UBound(parts) >= 0 Then HeaderModel = Trim(parts(0))
    If UBound(parts) >= 1 Then DialModel = Trim(parts(1))
    If UBound(parts) >= 2 Then AccessoryModel = Trim(parts(2))
    If UBound(parts) >= 3 Then FlangeModel = Trim(parts(3))

    ' 第二步: 解析表头部分 (必须存在)
    If HeaderModel = "" Then
        m_ErrorMessage = "表头型号为空"
        ParseModel = False
        Exit Function
    End If

    ' 解析表头
    If Not ParseHeader(HeaderModel) Then
        ParseModel = False
        Exit Function
    End If

    m_IsValid = True
    ParseModel = True
    Exit Function

ErrorHandler:
    m_ErrorMessage = "解析出错: " & Err.description
    m_IsValid = False
    ParseModel = False
End Function

' ========================================
' ParseHeader 方法 (私有)
' 功能: 解析表头型号部分
' 格式: [型号]-[公称外径].[安装形式].[壳体形式].[过程连接&材质].[量程范围].[仪表特性]
' 示例: YTHN-100.A0.532.M203.M16.Y3
' ========================================
Private Function ParseHeader(headerStr As String) As Boolean
    On Error GoTo ErrorHandler

    ' 按 - 分割型号和参数部分
    Dim mainParts() As String
    mainParts = Split(headerStr, "-")

    If UBound(mainParts) < 1 Then
        m_ErrorMessage = "表头格式错误: 缺少 - 分隔符"
        ParseHeader = False
        Exit Function
    End If

    ' 提取型号
    ModelType = Trim(mainParts(0))

    ' 按 . 分割参数部分
    Dim params() As String
    params = Split(mainParts(1), ".")

    ' 验证参数数量 (至少应该有5个部分)
    If UBound(params) < 4 Then
        m_ErrorMessage = "表头参数不足: 需要至少5个参数段"
        ParseHeader = False
        Exit Function
    End If

    ' 提取各参数
    Diameter = Trim(params(0))              ' 公称外径
    InstallForm = Trim(params(1))           ' 安装形式
    ShellForm = Trim(params(2))             ' 壳体形式
    ConnectionCode = Trim(params(3))        ' 过程连接&材质
    RangeCode = Trim(params(4))             ' 量程范围

    ' 仪表特性 (可选)
    If UBound(params) >= 5 Then
        Characteristics = Trim(params(5))
    Else
        Characteristics = ""
    End If

    ParseHeader = True
    Exit Function

ErrorHandler:
    m_ErrorMessage = "解析表头出错: " & Err.description
    ParseHeader = False
End Function

' ========================================
' GetThreadCode 方法
' 功能: 从过程连接代码中提取螺纹代码
' 规则: 去掉最后一位数字
' 示例: M203 -> M20
' ========================================
Public Function GetThreadCode() As String
    If ConnectionCode = "" Then
        GetThreadCode = ""
        Exit Function
    End If

    ' 去掉最后一位字符 (假设最后一位是材质代码)
    If Len(ConnectionCode) > 1 Then
        GetThreadCode = Left(ConnectionCode, Len(ConnectionCode) - 1)
    Else
        GetThreadCode = ConnectionCode
    End If
End Function

' ========================================
' GetMaterialCode 方法
' 功能: 从过程连接代码中提取材质代码
' 规则: 取最后一位数字
' 示例: M203 -> 3
' ========================================
Public Function GetMaterialCode() As String
    If ConnectionCode = "" Then
        GetMaterialCode = ""
        Exit Function
    End If

    ' 取最后一位字符
    GetMaterialCode = Right(ConnectionCode, 1)
End Function

' ========================================
' GetRangeCode 方法
' 功能: 获取量程代码
' 规则: 直接返回
' 示例: M16 -> M16
' ========================================
Public Function GetRangeCode() As String
    GetRangeCode = RangeCode
End Function

' ========================================
' IsValid 属性
' 功能: 返回解析是否成功
' ========================================
Public Property Get IsValid() As Boolean
    IsValid = m_IsValid
End Property

' ========================================
' ErrorMessage 属性
' 功能: 返回错误信息
' ========================================
Public Property Get ErrorMessage() As String
    ErrorMessage = m_ErrorMessage
End Property

' ========================================
' ToString 方法
' 功能: 返回解析结果的字符串表示 (用于调试)
' ========================================
Public Function ToString() As String
    Dim result As String
    result = "【型号解析结果】" & vbCrLf
    result = result & "原始型号: " & RawModel & vbCrLf
    result = result & "表头型号: " & HeaderModel & vbCrLf
    result = result & "表盘型号: " & DialModel & vbCrLf
    result = result & vbCrLf
    result = result & "【表头各部分】" & vbCrLf
    result = result & "  型号: " & ModelType & vbCrLf
    result = result & "  公称外径: " & Diameter & vbCrLf
    result = result & "  安装形式: " & InstallForm & vbCrLf
    result = result & "  壳体形式: " & ShellForm & vbCrLf
    result = result & "  过程连接&材质: " & ConnectionCode & vbCrLf
    result = result & "  量程范围: " & RangeCode & vbCrLf
    result = result & "  仪表特性: " & Characteristics & vbCrLf
    result = result & vbCrLf
    result = result & "【提取代码】" & vbCrLf
    result = result & "  螺纹代码: " & GetThreadCode() & vbCrLf
    result = result & "  材质代码: " & GetMaterialCode() & vbCrLf
    result = result & "  量程代码: " & GetRangeCode() & vbCrLf

    ToString = result
End Function

Modules\modBOMProcessor.bas

' ========================================
' 模块: modBOMTest
' 用途: 测试和使用BOM数据结构
' ========================================
Option Explicit

Sub TestBOMStructure()
    ' 初始化BOM管理器
    Dim bomMgr As New clsBOMManager

    ' 加载数据(假设工作表名称)
    Dim wsConfig As Worksheet
    Dim wsPlatform As Worksheet

    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")

    ' 加载数据
    bomMgr.LoadData wsConfig, wsPlatform

    ' 打印类别树结构到新工作表
    Dim wsOutput As Worksheet
    On Error Resume Next
    Application.DisplayAlerts = False
    ThisWorkbook.Worksheets("BOM结构").Delete
    Application.DisplayAlerts = True
    On Error GoTo 0

    Set wsOutput = ThisWorkbook.Worksheets.Add
    wsOutput.Name = "BOM结构"

    'bomMgr.PrintCategoryTree wsOutput

    Dim cats As collection
    Dim i As Integer
    Set cats = bomMgr.GetRootCategories()
    For i = 1 To cats.Count
        Debug.Print cats(i).categoryName
    Next i



    MsgBox "BOM数据结构加载完成!" & vbCrLf & _
           "请查看 'BOM结构' 工作表", vbInformation
End Sub

' 示例: 获取特定类别的物料
Sub GetCategoryMaterials()
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet, wsPlatform As Worksheet

    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")

    bomMgr.LoadData wsConfig, wsPlatform

    ' 获取"部件"类别的物料(使用父类别)
    Dim materials As collection
    Set materials = bomMgr.GetMaterialsForPicking("部件", True)

    Debug.Print "部件类别物料数量(父类别): " & materials.Count

    ' 获取"部件"类别的物料(使用子类别)
    Set materials = bomMgr.GetMaterialsForPicking("部件", False)

    Debug.Print "部件类别物料数量(子类别展开): " & materials.Count

    ' 遍历物料
    Dim mat As clsMaterialItem
    For Each mat In materials
        Debug.Print mat.code & " - " & mat.Name & _
                   " | 数量:" & mat.Quantity & _
                   " | 条件:" & mat.Condition
    Next mat
End Sub

' 示例: 根据产品型号生成领料清单
Sub GeneratePickingList()
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet, wsPlatform As Worksheet

    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")

    bomMgr.LoadData wsConfig, wsPlatform

    ' 创建领料清单工作表
    Dim wsPickList As Worksheet
    On Error Resume Next
    Application.DisplayAlerts = False
    ThisWorkbook.Worksheets("领料清单").Delete
    Application.DisplayAlerts = True
    On Error GoTo 0

    Set wsPickList = ThisWorkbook.Worksheets.Add
    wsPickList.Name = "领料清单"

    ' 写入表头
    Dim row As Long
    row = 1
    wsPickList.Cells(row, 1).value = "代号"
    wsPickList.Cells(row, 2).value = "名称"
    wsPickList.Cells(row, 3).value = "类别"
    wsPickList.Cells(row, 4).value = "数量"
    wsPickList.Cells(row, 5).value = "选择条件"
    wsPickList.Cells(row, 6).value = "领料方式"
    row = row + 1

    ' 遍历所有根类别
    Dim rootCats As collection
    Set rootCats = bomMgr.GetRootCategories

    Dim cat As clsCategory
    Dim materials As collection
    Dim mat As clsMaterialItem
    Dim i As Long, j As Long

    For i = 1 To rootCats.Count
        Set cat = rootCats(i)
        ' 默认使用父类别物料
        Set materials = bomMgr.GetMaterialsForPicking(cat.categoryName, True)

        For j = 1 To materials.Count
            Set mat = materials(j)
            wsPickList.Cells(row, 1).value = mat.code
            wsPickList.Cells(row, 2).value = mat.Name
            wsPickList.Cells(row, 3).value = mat.Category
            wsPickList.Cells(row, 4).value = mat.Quantity
            wsPickList.Cells(row, 5).value = mat.Condition
            wsPickList.Cells(row, 6).value = "父类别"
            row = row + 1
        Next j
    Next i

    ' 格式化表格
    wsPickList.Range("A1:F1").Font.Bold = True
    wsPickList.Columns("A:F").AutoFit

    MsgBox "领料清单生成完成!", vbInformation
End Sub

' 示例: 查询特定物料信息
Sub QueryMaterialInfo()
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet, wsPlatform As Worksheet

    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")

    bomMgr.LoadData wsConfig, wsPlatform

    ' 查询特定类别
    Dim cat As clsCategory
    Set cat = bomMgr.GetCategory("部件")

    If Not cat Is Nothing Then
        Debug.Print "类别: " & cat.categoryName
        Debug.Print "父类别: " & cat.ParentCategoryName
        Debug.Print "物料数: " & cat.materials.Count
        Debug.Print "子类别数: " & cat.SubCategories.Count
        Debug.Print "是否叶子类别: " & cat.IsLeafCategory

        ' 列出所有物料
        Dim mat As clsMaterialItem
        Dim i As Long
        For i = 1 To cat.materials.Count
            Set mat = cat.materials(i)
            Debug.Print "  - " & mat.code & ": " & mat.Name
        Next i

        ' 列出所有子类别
        Dim subCat As clsCategory
        Dim j As Long
        For j = 1 To cat.SubCategories.Count
            Set subCat = cat.SubCategories(j)
            Debug.Print "  子类别: " & subCat.categoryName & _
                       " (物料数:" & subCat.materials.Count & ")"
        Next j
    End If
End Sub

Modules\modModelParserExamples.bas

' ========================================
' 模块: modModelParserExamples
' 用途: 型号解析与物料匹配的实际应用示例
' ========================================
Option Explicit

' ========================================
' 示例1: 根据型号生成完整的领料清单
' ========================================
Sub Example1_GeneratePickingListByModel()
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet
    Dim wsPlatform As Worksheet

    ' 加载配置
    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")
    bomMgr.LoadData wsConfig, wsPlatform

    ' 产品型号
    Dim modelStr As String
    modelStr = "YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3"

    ' 创建领料清单工作表
    Dim wsPickList As Worksheet
    On Error Resume Next
    Application.DisplayAlerts = False
    ThisWorkbook.Worksheets("型号领料清单").Delete
    Application.DisplayAlerts = True
    On Error GoTo 0

    Set wsPickList = ThisWorkbook.Worksheets.Add
    wsPickList.Name = "型号领料清单"

    ' 写入表头
    Dim row As Long
    row = 1
    wsPickList.Cells(row, 1).value = "产品型号"
    wsPickList.Cells(row, 2).value = modelStr
    row = row + 1

    ' 提取条件并显示
    Dim conditions As Object
    Set conditions = bomMgr.ParseModelAndExtractConditions(modelStr)
    wsPickList.Cells(row, 1).value = "提取条件"
    Dim condStr As String
    Dim key As Variant
    For Each key In conditions.Keys
        condStr = condStr & key & "=" & conditions(key) & "; "
    Next key
    wsPickList.Cells(row, 2).value = condStr
    row = row + 2

    ' 表头
    wsPickList.Cells(row, 1).value = "类别"
    wsPickList.Cells(row, 2).value = "代号"
    wsPickList.Cells(row, 3).value = "名称"
    wsPickList.Cells(row, 4).value = "数量"
    wsPickList.Cells(row, 5).value = "选择条件"
    wsPickList.Cells(row, 6).value = "匹配状态"
    wsPickList.Range("A" & row & ":F" & row).Font.Bold = True
    row = row + 1

    ' 遍历所有根类别
    Dim rootCats As collection
    Set rootCats = bomMgr.GetRootCategories

    Dim cat As clsCategory
    Dim materials As collection
    Dim mat As clsMaterialItem
    Dim i As Long

    For i = 1 To rootCats.Count
        Set cat = rootCats(i)

        ' 获取该类别符合条件的物料
        Set materials = bomMgr.GetMaterialsByModel(modelStr, cat.categoryName)

        ' 写入物料
        Dim j As Long
        For j = 1 To materials.Count
            Set mat = materials(j)
            ' 使用物料自己的Category属性而不是外层循环的类别名称
            ' 这样当GetMaterialsByModel降级到子类别查找时能正确显示子类别名称
            wsPickList.Cells(row, 1).value = mat.Category
            wsPickList.Cells(row, 2).value = mat.code
            wsPickList.Cells(row, 3).value = mat.Name
            wsPickList.Cells(row, 4).value = mat.Quantity
            wsPickList.Cells(row, 5).value = IIf(mat.Condition = "", "(无条件)", mat.Condition)
            wsPickList.Cells(row, 6).value = "✓"
            row = row + 1
        Next j
    Next i

    ' 格式化
    wsPickList.Columns("A:F").AutoFit

    MsgBox "领料清单生成完成!" & vbCrLf & _
           "请查看工作表: 型号领料清单", vbInformation
End Sub

' ========================================
' 示例2: 批量处理多个型号
' ========================================
Sub Example2_BatchProcessModels()
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet
    Dim wsPlatform As Worksheet

    ' 加载配置
    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")
    bomMgr.LoadData wsConfig, wsPlatform

    ' 型号列表
    Dim models() As String
    models = Split("YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3," & _
                   "YTHN-100.A0.532.M203.M02.Y3|BP-095.2312.M02.PA3," & _
                   "YTHN-100.A0.532.M203.M12.Y3|BP-095.2312.M12.PA3", ",")

    ' 创建汇总表
    Dim wsReport As Worksheet
    On Error Resume Next
    Application.DisplayAlerts = False
    ThisWorkbook.Worksheets("批量型号汇总").Delete
    Application.DisplayAlerts = True
    On Error GoTo 0

    Set wsReport = ThisWorkbook.Worksheets.Add
    wsReport.Name = "批量型号汇总"

    ' 表头
    Dim row As Long
    row = 1
    wsReport.Cells(row, 1).value = "型号"
    wsReport.Cells(row, 2).value = "提取条件"
    wsReport.Cells(row, 3).value = "匹配物料数"
    wsReport.Cells(row, 4).value = "部件物料"
    wsReport.Range("A1:D1").Font.Bold = True
    row = row + 1

    ' 处理每个型号
    Dim modelStr As String
    Dim i As Long

    For i = LBound(models) To UBound(models)
        modelStr = Trim(models(i))
        If modelStr <> "" Then
            ' 提取条件
            Dim conditions As Object
            Set conditions = bomMgr.ParseModelAndExtractConditions(modelStr)

            Dim condStr As String
            condStr = ""
            Dim key As Variant
            For Each key In conditions.Keys
                condStr = condStr & key & "=" & conditions(key) & "; "
            Next key

            ' 获取物料
            Dim materials As collection
            Set materials = bomMgr.GetMaterialsByModel(modelStr, "部件")

            ' 写入结果
            wsReport.Cells(row, 1).value = modelStr
            wsReport.Cells(row, 2).value = condStr
            wsReport.Cells(row, 3).value = materials.Count

            ' 列出部件物料
            Dim matList As String
            matList = ""
            Dim mat As clsMaterialItem
            For Each mat In materials
                matList = matList & mat.code & "(" & mat.Name & "); "
            Next mat
            wsReport.Cells(row, 4).value = matList

            row = row + 1
        End If
    Next i

    ' 格式化
    wsReport.Columns("A:D").AutoFit

    MsgBox "批量处理完成!", vbInformation
End Sub

' ========================================
' 示例3: 查询并显示某个型号的详细信息
' ========================================
Sub Example3_ShowModelDetails()
    ' 弹出输入框
    Dim modelStr As String
    modelStr = InputBox("请输入产品型号:", "型号查询", _
                       "YTHN-100.A0.532.M203.M16.Y3")

    If modelStr = "" Then Exit Sub

    ' 解析型号
    Dim parser As New clsModelParser
    If Not parser.ParseModel(modelStr) Then
        MsgBox "型号解析失败: " & parser.ErrorMessage, vbCritical
        Exit Sub
    End If

    ' 提取条件
    Dim extractor As New clsConditionExtractor
    Dim conditions As Object
    Set conditions = extractor.ExtractConditions(parser)

    ' 显示详细信息
    Dim msg As String
    msg = "【型号解析结果】" & vbCrLf & vbCrLf
    msg = msg & "原始型号: " & parser.RawModel & vbCrLf
    msg = msg & "表头型号: " & parser.HeaderModel & vbCrLf
    msg = msg & "表盘型号: " & parser.DialModel & vbCrLf & vbCrLf

    msg = msg & "【表头各部分】" & vbCrLf
    msg = msg & "型号: " & parser.ModelType & vbCrLf
    msg = msg & "公称外径: " & parser.Diameter & vbCrLf
    msg = msg & "安装形式: " & parser.InstallForm & vbCrLf
    msg = msg & "壳体形式: " & parser.ShellForm & vbCrLf
    msg = msg & "过程连接&材质: " & parser.ConnectionCode & vbCrLf
    msg = msg & "量程范围: " & parser.RangeCode & vbCrLf
    msg = msg & "仪表特性: " & parser.Characteristics & vbCrLf & vbCrLf

    msg = msg & "【提取的物料选择条件】" & vbCrLf
    Dim key As Variant
    For Each key In conditions.Keys
        msg = msg & key & " = " & conditions(key) & vbCrLf
    Next key

    MsgBox msg, vbInformation, "型号详细信息"
End Sub

' ========================================
' 示例4: 对比两个型号的差异
' ========================================
Sub Example4_CompareModels()
    Dim model1 As String, model2 As String

    model1 = InputBox("请输入第一个型号:", "型号对比", _
                     "YTHN-100.A0.532.M203.M16.Y3")
    If model1 = "" Then Exit Sub

    model2 = InputBox("请输入第二个型号:", "型号对比", _
                     "YTHN-100.A0.532.M203.M02.Y3")
    If model2 = "" Then Exit Sub

    ' 解析两个型号
    Dim parser1 As New clsModelParser
    Dim parser2 As New clsModelParser
    Dim extractor As New clsConditionExtractor

    parser1.ParseModel model1
    parser2.ParseModel model2

    Dim cond1 As Object, cond2 As Object
    Set cond1 = extractor.ExtractConditions(parser1)

    Set extractor = New clsConditionExtractor
    Set cond2 = extractor.ExtractConditions(parser2)

    ' 对比
    Dim msg As String
    msg = "【型号对比】" & vbCrLf & vbCrLf
    msg = msg & "型号1: " & model1 & vbCrLf
    msg = msg & "型号2: " & model2 & vbCrLf & vbCrLf

    msg = msg & "【条件差异】" & vbCrLf
    Dim key As Variant
    Dim allKeys As Object
    Set allKeys = CreateObject("Scripting.Dictionary")

    For Each key In cond1.Keys
        allKeys(key) = True
    Next key
    For Each key In cond2.Keys
        allKeys(key) = True
    Next key

    For Each key In allKeys.Keys
        Dim val1 As String, val2 As String
        val1 = ""
        val2 = ""

        If cond1.Exists(key) Then val1 = cond1(key)
        If cond2.Exists(key) Then val2 = cond2(key)

        If val1 <> val2 Then
            msg = msg & key & ": " & val1 & " → " & val2 & " ?" & vbCrLf
        Else
            msg = msg & key & ": " & val1 & " ?" & vbCrLf
        End If
    Next key

    MsgBox msg, vbInformation, "型号对比结果"
End Sub

' ========================================
' 示例5: 验证物料选择条件的有效性
' ========================================
Sub Example5_ValidateMaterialConditions()
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet
    Dim wsPlatform As Worksheet

    ' 加载配置
    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")
    bomMgr.LoadData wsConfig, wsPlatform

    ' 创建验证结果表
    Dim wsValidation As Worksheet
    On Error Resume Next
    Application.DisplayAlerts = False
    ThisWorkbook.Worksheets("条件验证结果").Delete
    Application.DisplayAlerts = True
    On Error GoTo 0

    Set wsValidation = ThisWorkbook.Worksheets.Add
    wsValidation.Name = "条件验证结果"

    ' 表头
    Dim row As Long
    row = 1
    wsValidation.Cells(row, 1).value = "代号"
    wsValidation.Cells(row, 2).value = "名称"
    wsValidation.Cells(row, 3).value = "选择条件"
    wsValidation.Cells(row, 4).value = "验证结果"
    wsValidation.Range("A1:D1").Font.Bold = True
    row = row + 1

    ' 获取所有根类别
    Dim rootCats As collection
    Set rootCats = bomMgr.GetRootCategories

    Dim cat As clsCategory
    Dim materials As collection
    Dim mat As clsMaterialItem
    Dim matcher As New clsConditionMatcher
    Dim i As Long

    ' 遍历所有物料
    For i = 1 To rootCats.Count
        Set cat = rootCats(i)
        Set materials = New collection

        ' 收集该类别的所有物料
        Dim j As Long
        For j = 1 To cat.materials.Count
            materials.Add cat.materials(j)
        Next j

        ' 验证每个物料的条件
        For Each mat In materials
            wsValidation.Cells(row, 1).value = mat.code
            wsValidation.Cells(row, 2).value = mat.Name
            wsValidation.Cells(row, 3).value = IIf(mat.Condition = "", "(无)", mat.Condition)

            If mat.Condition = "" Then
                wsValidation.Cells(row, 4).value = "? 无条件"
            Else
                Dim validResult As String
                validResult = matcher.TestExpression(mat.Condition)
                wsValidation.Cells(row, 4).value = validResult
            End If

            row = row + 1
        Next mat
    Next i

    ' 格式化
    wsValidation.Columns("A:D").AutoFit

    MsgBox "条件验证完成!", vbInformation
End Sub

' ========================================
' 示例6: 对比启用/禁用自动降级的效果
' ========================================
Sub Example6_CompareAutoFallback()
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet
    Dim wsPlatform As Worksheet

    ' 加载配置
    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")
    bomMgr.LoadData wsConfig, wsPlatform

    ' 产品型号(使用可能导致类别缺失的型号)
    Dim modelStr As String
    modelStr = "YTHN-100.A0.532.M203.M17.Y3|BP-095.2312.M16.PA3"

    ' 创建对比结果表
    Dim wsCompare As Worksheet
    On Error Resume Next
    Application.DisplayAlerts = False
    ThisWorkbook.Worksheets("降级模式对比").Delete
    Application.DisplayAlerts = True
    On Error GoTo 0

    Set wsCompare = ThisWorkbook.Worksheets.Add
    wsCompare.Name = "降级模式对比"

    ' 表头
    Dim row As Long
    row = 1
    wsCompare.Cells(row, 1).value = "型号"
    wsCompare.Cells(row, 2).value = modelStr
    row = row + 2

    ' 测试1: 启用自动降级
    wsCompare.Cells(row, 1).value = "模式1: 启用自动降级 (autoFallback=True)"
    wsCompare.Cells(row, 1).Font.Bold = True
    row = row + 1

    wsCompare.Cells(row, 1).value = "类别"
    wsCompare.Cells(row, 2).value = "代号"
    wsCompare.Cells(row, 3).value = "名称"
    wsCompare.Cells(row, 4).value = "数量"
    wsCompare.Cells(row, 5).value = "选择条件"
    wsCompare.Range("A" & row & ":E" & row).Font.Bold = True
    row = row + 1

    Dim materials As collection
    Dim mat As clsMaterialItem
    Dim i As Long

    ' 获取启用自动降级的物料
    Set materials = bomMgr.GetMaterialsByModel(modelStr, "部件", True)

    If materials.Count = 0 Then
        wsCompare.Cells(row, 1).value = "(无物料)"
        row = row + 1
    Else
        For i = 1 To materials.Count
            Set mat = materials(i)
            wsCompare.Cells(row, 1).value = mat.Category
            wsCompare.Cells(row, 2).value = mat.code
            wsCompare.Cells(row, 3).value = mat.Name
            wsCompare.Cells(row, 4).value = mat.Quantity
            wsCompare.Cells(row, 5).value = IIf(mat.Condition = "", "(无)", mat.Condition)
            row = row + 1
        Next i
    End If

    row = row + 1

    ' 测试2: 禁用自动降级
    wsCompare.Cells(row, 1).value = "模式2: 禁用自动降级 (autoFallback=False)"
    wsCompare.Cells(row, 1).Font.Bold = True
    row = row + 1

    wsCompare.Cells(row, 1).value = "类别"
    wsCompare.Cells(row, 2).value = "代号"
    wsCompare.Cells(row, 3).value = "名称"
    wsCompare.Cells(row, 4).value = "数量"
    wsCompare.Cells(row, 5).value = "选择条件"
    wsCompare.Range("A" & row & ":E" & row).Font.Bold = True
    row = row + 1

    ' 获取禁用自动降级的物料
    Set materials = bomMgr.GetMaterialsByModel(modelStr, "部件", False)

    If materials.Count = 0 Then
        wsCompare.Cells(row, 1).value = "(无物料 - 因为禁用降级)"
        wsCompare.Cells(row, 2).value = "说明"
        wsCompare.Cells(row, 3).value = "当父类别无匹配物料时,不会降级到子类别查找"
        row = row + 1
    Else
        For i = 1 To materials.Count
            Set mat = materials(i)
            wsCompare.Cells(row, 1).value = mat.Category
            wsCompare.Cells(row, 2).value = mat.code
            wsCompare.Cells(row, 3).value = mat.Name
            wsCompare.Cells(row, 4).value = mat.Quantity
            wsCompare.Cells(row, 5).value = IIf(mat.Condition = "", "(无)", mat.Condition)
            row = row + 1
        Next i
    End If

    ' 添加说明
    row = row + 1
    wsCompare.Cells(row, 1).value = "说明:"
    wsCompare.Cells(row, 1).Font.Bold = True
    row = row + 1
    wsCompare.Cells(row, 1).value = "• autoFallback=True (默认): 当""部件""类别无匹配物料时,自动降级到子类别""接头""、""弹性元件""等查找"
    row = row + 1
    wsCompare.Cells(row, 1).value = "• autoFallback=False: 仅在""部件""类别查找,即使有子类别也不降级,可能返回空集合"

    ' 格式化
    wsCompare.Columns("A:E").AutoFit
    wsCompare.Range("A1").Font.Bold = True

    MsgBox "降级模式对比完成!" & vbCrLf & _
           "请查看工作表: 降级模式对比" & vbCrLf & vbCrLf & _
           "启用降级: " & bomMgr.GetMaterialsByModel(modelStr, "部件", True).Count & " 个物料" & vbCrLf & _
           "禁用降级: " & bomMgr.GetMaterialsByModel(modelStr, "部件", False).Count & " 个物料", _
           vbInformation
End Sub

' ========================================
' 示例7: 测试GetValidMaterialsByModel方法
' ========================================
Sub Example7_TestGetValidMaterialsByModel()
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet
    Dim wsPlatform As Worksheet

    ' 加载配置
    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")
    bomMgr.LoadData wsConfig, wsPlatform

    ' 创建测试结果工作表
    Dim wsTest As Worksheet
    On Error Resume Next
    Application.DisplayAlerts = False
    ThisWorkbook.Worksheets("物料完整性测试").Delete
    Application.DisplayAlerts = True
    On Error GoTo 0

    Set wsTest = ThisWorkbook.Worksheets.Add
    wsTest.Name = "物料完整性测试"

    ' 写入标题
    Dim row As Long
    row = 1
    wsTest.Cells(row, 1).value = "物料完整性测试报告"
    wsTest.Cells(row, 1).Font.Bold = True
    wsTest.Cells(row, 1).Font.Size = 14
    row = row + 2

    ' 测试多个型号
    Dim testModels() As Variant
    testModels = Array( _
        Array("YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3", "完整型号测试1"), _
        Array("YTHN-100.A0.532.M203.M02.Y3|BP-095.2312.M02.PA3", "完整型号测试2"), _
        Array("YTHN-100.A0.532.M203.M12.Y3|BP-095.2312.M12.PA3", "完整型号测试3"), _
        Array("YTHN-100.A0.532.M203.M17.Y3", "可能不完整的型号") _
    )

    Dim i As Long
    For i = LBound(testModels) To UBound(testModels)
        Dim modelStr As String
        Dim testName As String
        modelStr = testModels(i)(0)
        testName = testModels(i)(1)

        ' 调用GetValidMaterialsByModel
        Dim result As collection
        Set result = bomMgr.GetValidMaterialsByModel(modelStr)

        Dim materials As collection
        Dim isComplete As Boolean
        Set materials = result("Materials")
        isComplete = result("IsComplete")

        ' 写入测试名称和型号
        wsTest.Cells(row, 1).value = "测试 " & (i + 1) & ": " & testName
        wsTest.Cells(row, 1).Font.Bold = True
        row = row + 1

        wsTest.Cells(row, 1).value = "型号:"
        wsTest.Cells(row, 2).value = modelStr
        row = row + 1

        wsTest.Cells(row, 1).value = "完整性:"
        wsTest.Cells(row, 2).value = IIf(isComplete, "?? 完整", "?? 不完整")
        If isComplete Then
            wsTest.Cells(row, 2).Font.Color = RGB(0, 128, 0) ' 绿色
        Else
            wsTest.Cells(row, 2).Font.Color = RGB(255, 0, 0) ' 红色
        End If
        wsTest.Cells(row, 2).Font.Bold = True
        row = row + 1

        wsTest.Cells(row, 1).value = "物料数量:"
        wsTest.Cells(row, 2).value = materials.count
        row = row + 1

        ' 写入物料明细表头
        wsTest.Cells(row, 1).value = "类别"
        wsTest.Cells(row, 2).value = "代号"
        wsTest.Cells(row, 3).value = "名称"
        wsTest.Cells(row, 4).value = "数量"
        wsTest.Cells(row, 5).value = "选择条件"
        wsTest.Range(wsTest.Cells(row, 1), wsTest.Cells(row, 5)).Font.Bold = True
        wsTest.Range(wsTest.Cells(row, 1), wsTest.Cells(row, 5)).Interior.Color = RGB(200, 200, 200)
        row = row + 1

        ' 写入每个物料
        Dim mat As clsMaterialItem
        Dim j As Long
        For j = 1 To materials.count
            Set mat = materials(j)
            wsTest.Cells(row, 1).value = mat.Category
            wsTest.Cells(row, 2).value = mat.code
            wsTest.Cells(row, 3).value = mat.Name
            wsTest.Cells(row, 4).value = mat.Quantity
            wsTest.Cells(row, 5).value = IIf(mat.Condition = "", "(无条件)", mat.Condition)
            row = row + 1
        Next j

        ' 添加分隔行
        row = row + 1
        wsTest.Cells(row, 1).value = String(80, "-")
        row = row + 2
    Next i

    ' 添加统计汇总
    wsTest.Cells(row, 1).value = "测试汇总"
    wsTest.Cells(row, 1).Font.Bold = True
    wsTest.Cells(row, 1).Font.Size = 12
    row = row + 1

    Dim completeCount As Long
    Dim incompleteCount As Long
    completeCount = 0
    incompleteCount = 0

    For i = LBound(testModels) To UBound(testModels)
        modelStr = testModels(i)(0)
        Set result = bomMgr.GetValidMaterialsByModel(modelStr)
        If result("IsComplete") Then
            completeCount = completeCount + 1
        Else
            incompleteCount = incompleteCount + 1
        End If
    Next i

    wsTest.Cells(row, 1).value = "总测试数:"
    wsTest.Cells(row, 2).value = UBound(testModels) - LBound(testModels) + 1
    row = row + 1

    wsTest.Cells(row, 1).value = "完整:"
    wsTest.Cells(row, 2).value = completeCount
    wsTest.Cells(row, 2).Font.Color = RGB(0, 128, 0)
    row = row + 1

    wsTest.Cells(row, 1).value = "不完整:"
    wsTest.Cells(row, 2).value = incompleteCount
    wsTest.Cells(row, 2).Font.Color = RGB(255, 0, 0)

    ' 格式化
    wsTest.Columns("A:E").AutoFit

    MsgBox "物料完整性测试完成!" & vbCrLf & _
           "完整: " & completeCount & " 个" & vbCrLf & _
           "不完整: " & incompleteCount & " 个" & vbCrLf & vbCrLf & _
           "请查看工作表: 物料完整性测试", vbInformation
End Sub

Modules\modModelParserTest.bas

' ========================================
' 模块: modModelParserTest
' 用途: 测试型号解析和物料匹配功能
' ========================================
Option Explicit

' ========================================
' 测试1: 型号解析器基础功能
' ========================================
Sub Test1_ModelParser()
    Debug.Print String(80, "=")
    Debug.Print "测试1: 型号解析器基础功能"
    Debug.Print String(80, "=")

    Dim parser As New clsModelParser
    Dim modelStr As String

    ' 测试用例1: 完整型号
    modelStr = "YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3"
    Debug.Print "【测试用例1】完整型号"
    Debug.Print "输入: " & modelStr

    If parser.ParseModel(modelStr) Then
        Debug.Print parser.ToString()
        Debug.Print "? 解析成功"
    Else
        Debug.Print "? 解析失败: " & parser.ErrorMessage
    End If

    Debug.Print ""

    ' 测试用例2: 仅表头
    modelStr = "YTHN-100.A0.532.M203.M16.Y3"
    Debug.Print "【测试用例2】仅表头"
    Debug.Print "输入: " & modelStr

    If parser.ParseModel(modelStr) Then
        Debug.Print "  螺纹代码: " & parser.GetThreadCode()
        Debug.Print "  材质代码: " & parser.GetMaterialCode()
        Debug.Print "  量程代码: " & parser.GetRangeCode()
        Debug.Print "? 解析成功"
    Else
        Debug.Print "? 解析失败: " & parser.ErrorMessage
    End If

    Debug.Print String(80, "=")
    Debug.Print ""
End Sub

' ========================================
' 测试2: 条件提取器
' ========================================
Sub Test2_ConditionExtractor()
    Debug.Print String(80, "=")
    Debug.Print "测试2: 条件提取器"
    Debug.Print String(80, "=")

    Dim parser As New clsModelParser
    Dim extractor As New clsConditionExtractor
    Dim modelStr As String

    modelStr = "YTHN-100.A0.532.M203.M16.Y3"
    Debug.Print "输入型号: " & modelStr

    If parser.ParseModel(modelStr) Then
        Dim conditions As Object
        Set conditions = extractor.ExtractConditions(parser)

        Debug.Print extractor.ToString()

        ' 验证提取结果
        Debug.Print "【验证】"
        Debug.Print "  gclj = " & extractor.GetConditionValue("gclj") & _
                   IIf(extractor.GetConditionValue("gclj") = "M20", " ?", " ?")
        Debug.Print "  jycz = " & extractor.GetConditionValue("jycz") & _
                   IIf(extractor.GetConditionValue("jycz") = "3", " ?", " ?")
        Debug.Print "  lcfw = " & extractor.GetConditionValue("lcfw") & _
                   IIf(extractor.GetConditionValue("lcfw") = "M16", " ?", " ?")
    Else
        Debug.Print "? 型号解析失败"
    End If

    Debug.Print String(80, "=")
    Debug.Print ""
End Sub

' ========================================
' 测试3: 条件匹配器
' ========================================
Sub Test3_ConditionMatcher()
    Debug.Print String(80, "=")
    Debug.Print "测试3: 条件匹配器"
    Debug.Print String(80, "=")

    Dim matcher As New clsConditionMatcher
    Dim conditions As Object
    Set conditions = CreateObject("Scripting.Dictionary")
    conditions("gclj") = "M20"
    conditions("jycz") = "3"
    conditions("lcfw") = "M16"

    Debug.Print "【测试条件】"
    Debug.Print "  gclj = M20"
    Debug.Print "  jycz = 3"
    Debug.Print "  lcfw = M16"
    Debug.Print ""

    ' 测试用例
    Dim testCases As Variant
    testCases = Array( _
        Array("", True, "空条件"), _
        Array("lcfw=M16", True, "简单等于"), _
        Array("lcfw=M17", False, "简单不匹配"), _
        Array("lcfw=M16 AND gclj=M20", True, "AND 全真"), _
        Array("lcfw=M16 AND gclj=M10", False, "AND 一假"), _
        Array("lcfw=M16 OR lcfw=M17", True, "OR 一真"), _
        Array("lcfw=M15 OR lcfw=M17", False, "OR 全假"), _
        Array("gclj!=M10", True, "不等于 真"), _
        Array("gclj!=M20", False, "不等于 假"), _
        Array("lcfw=M02 AND gclj!=M20", False, "复合条件1"), _
        Array("lcfw=M16 AND gclj!=M10", True, "复合条件2"), _
        Array("gclj=M20 AND (lcfw=M16 OR lcfw=M17)", True, "括号优先级1"), _
        Array("gclj=M20 AND (lcfw=M15 OR lcfw=M17)", False, "括号优先级2") _
    )

    Dim i As Long
    Dim testCase As Variant
    Dim expr As String
    Dim expected As Boolean
    Dim actual As Boolean
    Dim description As String
    Dim passCount As Long
    Dim failCount As Long

    passCount = 0
    failCount = 0

    Debug.Print "【测试用例】"
    For i = LBound(testCases) To UBound(testCases)
        testCase = testCases(i)
        expr = testCase(0)
        expected = testCase(1)
        description = testCase(2)

        actual = matcher.IsMatch(expr, conditions)

        If actual = expected Then
            Debug.Print "  ? " & description & ": " & IIf(expr = "", "(空)", expr)
            passCount = passCount + 1
        Else
            Debug.Print "  ? " & description & ": " & expr
            Debug.Print "    预期: " & expected & ", 实际: " & actual
            failCount = failCount + 1
        End If
    Next i

    Debug.Print ""
    Debug.Print "【统计】"
    Debug.Print "  通过: " & passCount
    Debug.Print "  失败: " & failCount

    Debug.Print String(80, "=")
    Debug.Print ""
End Sub

' ========================================
' 测试4: 完整流程 - 根据型号获取物料
' ========================================
Sub Test4_GetMaterialsByModel()
    Debug.Print String(80, "=")
    Debug.Print "测试4: 根据型号获取物料(完整流程)"
    Debug.Print String(80, "=")

    ' 加载BOM数据
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet
    Dim wsPlatform As Worksheet

    On Error Resume Next
    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")
    On Error GoTo 0

    If wsConfig Is Nothing Or wsPlatform Is Nothing Then
        Debug.Print "? 错误: 找不到必需的工作表"
        Exit Sub
    End If

    bomMgr.LoadData wsConfig, wsPlatform
    Debug.Print "? BOM数据加载完成"
    Debug.Print ""

    ' 测试型号
    Dim modelStr As String
    modelStr = "YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3"

    ' 获取"部件"类别的符合条件的物料
    Dim materials As collection
    Set materials = bomMgr.GetMaterialsByModel(modelStr, "部件")

    Debug.Print ""
    Debug.Print "【结果验证】"
    If materials.Count = 1 Then
        Dim mat As clsMaterialItem
        Set mat = materials(1)
        If mat.code = "01011019018" And mat.Name = "高压接头部件" Then
            Debug.Print "? 测试通过!成功匹配到正确的物料"
            Debug.Print "  代号: " & mat.code
            Debug.Print "  名称: " & mat.Name
            Debug.Print "  条件: " & mat.Condition
        Else
            Debug.Print "? 匹配到的物料不正确"
        End If
    Else
        Debug.Print "? 匹配数量不正确预期1个实际" & materials.Count & "个"
    End If

    Debug.Print String(80, "=")
    Debug.Print ""
End Sub

' ========================================
' 测试5: 获取所有类别的符合条件的物料
' ========================================
Sub Test5_GetAllMaterialsByModel()
    Debug.Print String(80, "=")
    Debug.Print "测试5: 获取所有类别的符合条件的物料"
    Debug.Print String(80, "=")

    ' 加载BOM数据
    Dim bomMgr As New clsBOMManager
    Dim wsConfig As Worksheet
    Dim wsPlatform As Worksheet

    On Error Resume Next
    Set wsConfig = ThisWorkbook.Worksheets("领料配置")
    Set wsPlatform = ThisWorkbook.Worksheets("平台配置清单")
    On Error GoTo 0

    If wsConfig Is Nothing Or wsPlatform Is Nothing Then
        Debug.Print "? 错误: 找不到必需的工作表"
        Exit Sub
    End If

    bomMgr.LoadData wsConfig, wsPlatform

    ' 测试型号
    Dim modelStr As String
    modelStr = "YTHN-100.A0.532.M203.M16.Y3"

    ' 获取所有类别的符合条件的物料
    Dim materials As collection
    Set materials = bomMgr.GetMaterialsByModel(modelStr)

    Debug.Print ""
    Debug.Print "【匹配结果汇总】"
    Debug.Print "  共匹配 " & materials.Count & " 个物料"

    Debug.Print String(80, "=")
    Debug.Print ""
End Sub

' ========================================
' 运行所有测试
' ========================================
Sub RunAllTests()
    Debug.Print vbCrLf & vbCrLf
    Debug.Print "╔" & String(78, "═") & "╗"
    Debug.Print "║" & Space(20) & "型号解析与物料匹配 - 完整测试套件" & Space(20) & "║"
    Debug.Print "╚" & String(78, "═") & "╝"
    Debug.Print ""

    Test1_ModelParser
    Test2_ConditionExtractor
    Test3_ConditionMatcher
    Test4_GetMaterialsByModel
    Test5_GetAllMaterialsByModel

    Debug.Print "╔" & String(78, "═") & "╗"
    Debug.Print "║" & Space(30) & "所有测试完成" & Space(30) & "║"
    Debug.Print "╚" & String(78, "═") & "╝"
End Sub

' ========================================
' 测试6: 条件提取规则展示
' ========================================
Sub Test6_ShowExtractionRules()
    Debug.Print String(80, "=")
    Debug.Print "测试6: 当前配置的条件提取规则"
    Debug.Print String(80, "=")

    Dim extractor As New clsConditionExtractor
    Dim rules As Object
    Set rules = extractor.GetExtractionRules()

    Debug.Print "【提取规则配置】"
    Dim key As Variant
    For Each key In rules.Keys
        Dim ruleInfo As Variant
        ruleInfo = rules(key)
        Debug.Print "  变量名: " & key
        Debug.Print "    源字段: " & ruleInfo(0)
        Debug.Print "    提取方法: " & ruleInfo(1)
        Debug.Print ""
    Next key

    Debug.Print String(80, "=")
    Debug.Print ""
End Sub