diff --git a/VBA/ClassModules/clsBOMManager.cls b/VBA/ClassModules/clsBOMManager.cls
index bdbda7a..8a0a6aa 100644
--- a/VBA/ClassModules/clsBOMManager.cls
+++ b/VBA/ClassModules/clsBOMManager.cls
@@ -692,6 +692,7 @@ End Function
' Collection中包含两个元素:
' (1) "Materials" - Collection对象,包含所有匹配的 clsMaterialItem 对象
' (2) "IsComplete" - Boolean值,指示物料是否完整
+' (3) "MissingCategories" - Collection对象, 包含所有缺失的类别名称字符串
'
' 完整性判断规则:
' - 对于每个需要领料的类别(顶层类别或其需要领取的子类别)
@@ -709,6 +710,7 @@ End Function
Public Function GetValidMaterialsByModel(modelStr As String) As collection
Dim result As New collection
Dim allMaterials As New collection
+ Dim allMissingCats As New collection ' 用于存储所有缺失的类别
Dim isComplete As Boolean
' 解析型号并提取条件
@@ -718,6 +720,7 @@ Public Function GetValidMaterialsByModel(modelStr As String) As collection
isComplete = False
result.Add allMaterials, "Materials"
result.Add isComplete, "IsComplete"
+ result.Add allMissingCats, "MissingCategories"
Set GetValidMaterialsByModel = result
Exit Function
End If
@@ -726,23 +729,11 @@ Public Function GetValidMaterialsByModel(modelStr As String) As collection
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
@@ -753,7 +744,7 @@ Public Function GetValidMaterialsByModel(modelStr As String) As collection
Dim catResult As Object
Set catResult = CheckCategoryCompleteness(rootCat, matcher, conditions)
- ' 合并物料
+ ' 1. 合并匹配到的物料
Dim mat As clsMaterialItem
Dim matCollection As collection
Set matCollection = catResult("Materials")
@@ -761,29 +752,25 @@ Public Function GetValidMaterialsByModel(modelStr As String) As collection
allMaterials.Add mat
Next mat
- ' 检查完整性
+ ' 2. 合并缺失的类别 (新增)
+ Dim missingCollection As collection
+ Set missingCollection = catResult("Missing")
+ Dim missingCatName As Variant
+ For Each missingCatName In missingCollection
+ allMissingCats.Add missingCatName
+ Next missingCatName
+
+ ' 3. 检查完整性状态
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"
+ result.Add allMissingCats, "MissingCategories" ' 新增返回项
+
Set GetValidMaterialsByModel = result
End Function
@@ -797,6 +784,7 @@ End Function
' 返回: Dictionary对象
' "Materials" - Collection,包含该类别匹配的物料
' "IsComplete" - Boolean,该类别是否完整
+' "Missing" - Collection, 缺失的类别名称列表
'
' 完整性判断逻辑:
' 1. 如果类别没有子类别(叶子类别):
@@ -817,6 +805,7 @@ Private Function CheckCategoryCompleteness(cat As clsCategory, _
Set result = CreateObject("Scripting.Dictionary")
Dim materials As New collection
+ Dim missingCats As New collection ' 本层级及子层级缺失的类别
Dim isComplete As Boolean
' 首先在当前类别查找匹配的物料
@@ -837,36 +826,31 @@ Private Function CheckCategoryCompleteness(cat As clsCategory, _
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个)"
+ missingCats.Add cat.categoryName
End If
+ ' 注: matchCount > 1 属于配置错误(重复匹配),算不完整,但通常不算"Missing"
End If
Else
' ========================================
' 情况2: 有子类别
- ' 先检查父类别,如果父类别满足则使用父类别
- ' 否则降级到子类别,每个子类别都必须满足
' ========================================
If matchCount = 1 Then
- ' 父类别有且仅有1个物料,使用父类别
+ ' 父类别有且仅有1个物料, 使用父类别 -> 完整
isComplete = True
- Debug.Print "【完整】父类别 """ & cat.categoryName & """ 有1个匹配物料,使用父类别"
+ ' 不需要检查子类别了,missingCats 保持为空
ElseIf matchCount = 0 Then
- ' 父类别无匹配物料,降级到子类别
- Debug.Print "【降级】父类别 """ & cat.categoryName & """ 无匹配物料,检查子类别..."
+ ' 父类别无匹配物料, 必须降级检查所有子类别
- ' 清空物料集合,准备收集子类别物料
+ ' 清空当前物料集合(确保没东西), 准备收集子类别结果
Set materials = New collection
- isComplete = True ' 假设完整,如果任一子类别不完整则设为False
+ isComplete = True ' 先假设完整, 若任一子类别不完整则置错
Dim subCat As clsCategory
Dim j As Long
@@ -877,7 +861,7 @@ Private Function CheckCategoryCompleteness(cat As clsCategory, _
Dim subResult As Object
Set subResult = CheckCategoryCompleteness(subCat, matcher, conditions)
- ' 合并子类别物料
+ ' a) 合并子类别物料
Dim subMaterials As collection
Set subMaterials = subResult("Materials")
Dim k As Long
@@ -885,26 +869,28 @@ Private Function CheckCategoryCompleteness(cat As clsCategory, _
materials.Add subMaterials(k)
Next k
- ' 检查子类别是否完整
+ ' b) 合并子类别缺失列表 (关键步骤)
+ Dim subMissing As collection
+ Set subMissing = subResult("Missing")
+ Dim item As Variant
+ For Each item In subMissing
+ missingCats.Add item
+ Next item
+
+ ' c) 更新完整性状态
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
- ' 父类别有多个匹配物料,不完整
+ ' 父类别有 >1 个匹配, 视为不完整(冲突)
isComplete = False
- Debug.Print "【不完整】父类别 """ & cat.categoryName & """ 有" & matchCount & "个匹配物料(应为0或1个)"
End If
End If
- ' 返回结果
+ ' 封装返回结果
Set result("Materials") = materials
result("IsComplete") = isComplete
+ Set result("Missing") = missingCats
Set CheckCategoryCompleteness = result
-End Function
\ No newline at end of file
+End Function
diff --git a/VBA/Modules/modModelParserExamples.bas b/VBA/Modules/modModelParserExamples.bas
index d27ec8f..bb360e5 100644
--- a/VBA/Modules/modModelParserExamples.bas
+++ b/VBA/Modules/modModelParserExamples.bas
@@ -502,7 +502,7 @@ Sub Example6_CompareAutoFallback()
End Sub
' ========================================
-' 示例7: 测试GetValidMaterialsByModel方法
+' 示例7: 测试GetValidMaterialsByModel方法 (使用内置的缺失列表)
' ========================================
Sub Example7_TestGetValidMaterialsByModel()
Dim bomMgr As New clsBOMManager
@@ -533,13 +533,11 @@ Sub Example7_TestGetValidMaterialsByModel()
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", "可能不完整的型号") _
+ Array("YTHN-100.A0.532.M203.M17.Y3", "缺失子类别测试") _
)
Dim i As Long
@@ -549,18 +547,22 @@ Sub Example7_TestGetValidMaterialsByModel()
modelStr = testModels(i)(0)
testName = testModels(i)(1)
- ' 调用GetValidMaterialsByModel
+ ' 1. 调用方法获取结果
Dim result As collection
Set result = bomMgr.GetValidMaterialsByModel(modelStr)
Dim materials As collection
+ Dim missingCats As collection
Dim isComplete As Boolean
+
Set materials = result("Materials")
+ Set missingCats = result("MissingCategories") ' 直接获取缺失列表
isComplete = result("IsComplete")
- ' 写入测试名称和型号
+ ' 2. 写入头部信息
wsTest.Cells(row, 1).value = "测试 " & (i + 1) & ": " & testName
wsTest.Cells(row, 1).Font.Bold = True
+ wsTest.Cells(row, 1).Interior.Color = RGB(240, 240, 240)
row = row + 1
wsTest.Cells(row, 1).value = "型号:"
@@ -568,41 +570,58 @@ Sub Example7_TestGetValidMaterialsByModel()
row = row + 1
wsTest.Cells(row, 1).value = "完整性:"
- wsTest.Cells(row, 2).value = IIf(isComplete, "?? 完整", "?? 不完整")
+ wsTest.Cells(row, 2).value = IIf(isComplete, "✓ 完整", "✕ 不完整")
If isComplete Then
- wsTest.Cells(row, 2).Font.Color = RGB(0, 128, 0) ' 绿色
+ wsTest.Cells(row, 2).Font.Color = RGB(0, 128, 0)
Else
- wsTest.Cells(row, 2).Font.Color = RGB(255, 0, 0) ' 红色
+ 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
+
+ ' 3. 打印匹配到的物料
+ wsTest.Cells(row, 1).value = "【已匹配物料】"
+ wsTest.Cells(row, 1).Font.Bold = True
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)
+ wsTest.Range(wsTest.Cells(row, 1), wsTest.Cells(row, 4)).Font.Bold = True
+ wsTest.Range(wsTest.Cells(row, 1), wsTest.Cells(row, 4)).Interior.Color = RGB(220, 220, 220)
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)
+ If materials.count > 0 Then
+ For Each mat In materials
+ 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
+ row = row + 1
+ Next mat
+ Else
+ wsTest.Cells(row, 1).value = "(无)"
row = row + 1
- Next j
+ End If
+
+ ' 4. 打印缺失的类别 (新增核心功能)
+ If missingCats.count > 0 Then
+ row = row + 1
+ wsTest.Cells(row, 1).value = "【缺失类别】"
+ wsTest.Cells(row, 1).Font.Color = RGB(255, 0, 0)
+ wsTest.Cells(row, 1).Font.Bold = True
+ row = row + 1
+
+ Dim catName As Variant
+ For Each catName In missingCats
+ wsTest.Cells(row, 1).value = "❌ " & catName
+ wsTest.Cells(row, 1).Font.Color = RGB(255, 0, 0)
+ wsTest.Cells(row, 2).value = "未找到匹配物料"
+ row = row + 1
+ Next catName
+ End If
' 添加分隔行
row = row + 1
@@ -610,45 +629,6 @@ Sub Example7_TestGetValidMaterialsByModel()
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
+ wsTest.Columns("A:D").AutoFit
+ MsgBox "测试完成!", vbInformation
End Sub
\ No newline at end of file
diff --git a/VBA/combined_output.md b/VBA/combined_output.md
new file mode 100644
index 0000000..f095227
--- /dev/null
+++ b/VBA/combined_output.md
@@ -0,0 +1,2876 @@
+# 目录
+
+- [ClassModules\clsBOMManager.cls](#classmodules-clsbommanager-cls)
+- [ClassModules\clsCategory.cls](#classmodules-clscategory-cls)
+- [ClassModules\clsConditionExtractor.cls](#classmodules-clsconditionextractor-cls)
+- [ClassModules\clsConditionMatcher.cls](#classmodules-clsconditionmatcher-cls)
+- [ClassModules\clsMaterialItem.cls](#classmodules-clsmaterialitem-cls)
+- [ClassModules\clsModelParser.cls](#classmodules-clsmodelparser-cls)
+- [Modules\modBOMProcessor.bas](#modules-modbomprocessor-bas)
+- [Modules\modModelParserExamples.bas](#modules-modmodelparserexamples-bas)
+- [Modules\modModelParserTest.bas](#modules-modmodelparsertest-bas)
+
+---
+
+## 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
+```