热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
根据用户指定的具体业务场景与操作目标,自动生成可直接使用的Excel VBA宏代码。专注于财务、数据分析等业务领域,确保代码的实用性与准确性,提升办公自动化效率。
Option Explicit
' 入口宏 Public Sub 生成2025年03月费用汇总与透视() Dim t0 As Date, t1 As Date t0 = Now
Dim appCalc As XlCalculation, scrn As Boolean, alerts As Boolean
appCalc = Application.Calculation
scrn = Application.ScreenUpdating
alerts = Application.DisplayAlerts
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.Calculation = xlCalculationManual
On Error GoTo SAFE_EXIT ' 全局保护
Dim logLines As Collection: Set logLines = New Collection
Dim processedSheets As Long, processedRows As Long, skippedRows As Long, errorCount As Long
Dim backupPath As String
' 1) 备份
backupPath = 进行工作簿备份(logLines)
' 2) 构建字典:汇率与预算
Dim dictRate As Object: Set dictRate = CreateObject("Scripting.Dictionary")
Dim dictBudget As Object: Set dictBudget = CreateObject("Scripting.Dictionary")
载入汇率字典 dictRate, logLines
载入预算字典 dictBudget, logLines
' 3) 遍历部门表,聚合到内存
Dim dictAgg As Object: Set dictAgg = CreateObject("Scripting.Dictionary") ' key: 部门|费用类别|成本中心 -> Double
Dim dictCCMap As Object: Set dictCCMap = CreateObject("Scripting.Dictionary") ' key: 部门|费用类别 -> dict(成本中心)=True
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
If 应视为部门表(ws.Name) Then
On Error GoTo SHEET_ERR
processedSheets = processedSheets + 1
处理部门工作表 ws, dictRate, dictBudget, dictAgg, dictCCMap, processedRows, skippedRows, errorCount, logLines
On Error GoTo 0
End If
SHEET_NEXT: Next ws
' 4) 输出到“汇总”工作表(结构化表 tblSummary)
Dim wsSum As Worksheet: Set wsSum = 获取或创建工作表("汇总")
Dim tbl As ListObject
设置汇总表 wsSum, dictAgg, dictBudget, dictCCMap, tbl, logLines
' 5) 生成 / 刷新透视表 pvtDeptCost(分部门、费用类别)
生成或刷新透视表 tbl, logLines
' 6) 冻结首行与格式
冻结首行 wsSum
If WorksheetExists("透视") Then 冻结首行 ThisWorkbook.Worksheets("透视")
' 7) 汇总页底部时间戳与计数
写入时间戳与计数 wsSum, t0, processedSheets, processedRows, skippedRows, errorCount, backupPath
' 8) 生成日志文件
t1 = Now
追加日志 logLines, "开始时间: " & Format(t0, "yyyy-mm-dd HH:NN:SS")
追加日志 logLines, "结束时间: " & Format(t1, "yyyy-mm-dd HH:NN:SS")
写入日志到文件 logLines
SAFE_EXIT: Application.ScreenUpdating = scrn Application.DisplayAlerts = alerts Application.Calculation = appCalc Exit Sub
SHEET_ERR: errorCount = errorCount + 1 追加日志 logLines, "【错误】工作表[" & ws.Name & "] 处理失败,已跳过。错误: " & Err.Number & " - " & Err.Description MsgBox "处理工作表 [" & ws.Name & "] 时出错,已记录并继续。错误:" & Err.Description, vbExclamation Err.Clear Resume SHEET_NEXT End Sub
' 判断是否为部门表(排除固定页) Private Function 应视为部门表(ByVal sheetName As String) As Boolean Dim ex As Variant For Each ex In Array("汇总", "字典", "预算", "透视") If StrComp(sheetName, CStr(ex), vbTextCompare) = 0 Then 应视为部门表 = False Exit Function End If Next ex 应视为部门表 = True End Function
' 处理部门工作表 Private Sub 处理部门工作表(ByVal ws As Worksheet, _ ByVal dictRate As Object, ByVal dictBudget As Object, _ ByVal dictAgg As Object, ByVal dictCCMap As Object, _ ByRef processedRows As Long, ByRef skippedRows As Long, ByRef errorCount As Long, _ ByRef logLines As Collection)
Const YR As Integer = 2025
Const MTH As Integer = 3
Dim dStart As Date: dStart = DateSerial(YR, MTH, 1)
Dim dEnd As Date: dEnd = DateSerial(YR, MTH + 1, 0)
Dim listObj As ListObject
If ws.ListObjects.Count > 0 Then
Set listObj = ws.ListObjects(1)
Else
' 若不存在表,尝试以UsedRange的B:F作为数据区
' 但仍优先遵循“结构化表格”前提
End If
Dim visibleRows As Range
Dim dataRng As Range
Dim hdrRow As Range
Dim hasTable As Boolean: hasTable = Not listObj Is Nothing
Dim curColIdx As Long: curColIdx = 0 ' 币种列(可选)
If hasTable Then
On Error Resume Next
Set hdrRow = listObj.HeaderRowRange
On Error GoTo 0
If Not hdrRow Is Nothing Then
Dim c As Range
For Each c In hdrRow.Cells
Select Case Trim$(UCase$(CStr(c.Value)))
Case "币种", "貨幣", "货币", "CURRENCY", "CUR", "FX"
curColIdx = c.Column
Exit For
End Select
Next c
End If
End If
' 仅处理可见行
On Error Resume Next
If hasTable Then
Set dataRng = listObj.DataBodyRange
Else
' 回退到UsedRange
Set dataRng = Intersect(ws.UsedRange, ws.Range("B:F"))
End If
If dataRng Is Nothing Then Exit Sub
Set visibleRows = dataRng.SpecialCells(xlCellTypeVisible)
On Error GoTo 0
If visibleRows Is Nothing Then Exit Sub
' 合并单元格被遮挡字段的向下填充(逻辑上用“上一有效值”)
Dim lastC As String, lastD As String, lastE As String
Dim area As Range, r As Range
Dim rowN As Long
Dim cntInSheet As Long, skippedInSheet As Long
For Each area In visibleRows.Areas
For Each r In area.Columns(1).Cells
rowN = r.Row
If ws.Rows(rowN).Hidden Then GoTo NEXTROW
Dim valB, valC, valD, valE, valF, vDate As Variant
Dim sDept As String, sCat As String, sCC As String
Dim amt As Double, amtCNY As Double
Dim sCur As String
Dim okDate As Boolean
' 部门取工作表名称
sDept = Trim$(ws.Name)
' 日期 B
valB = 取合并或本格值(ws.Cells(rowN, "B"))
If IsError(valB) Or IsEmpty(valB) Then
skippedInSheet = skippedInSheet + 1
GoTo NEXTROW
End If
vDate = 解析日期(valB)
If IsDate(vDate) Then
If vDate >= dStart And vDate <= dEnd Then
okDate = True
End If
End If
If Not okDate Then
skippedInSheet = skippedInSheet + 1
GoTo NEXTROW
End If
' 科目 C(仅用于占位;分类字段向下填充)
valC = 取合并或本格值(ws.Cells(rowN, "C"))
' 费用类别 D
valD = 取合并或本格值(ws.Cells(rowN, "D"))
' 成本中心 E
valE = 取合并或本格值(ws.Cells(rowN, "E"))
sCat = 规范文本(valD, lastD)
sCC = 规范文本(valE, lastE)
' lastC 可不使用,但保持逻辑完整
lastC = 规范文本(valC, lastC)
lastD = sCat
lastE = sCC
If LenB(sCat) = 0 Or LenB(sCC) = 0 Then
skippedInSheet = skippedInSheet + 1
GoTo NEXTROW
End If
' 金额 F
valF = 取合并或本格值(ws.Cells(rowN, "F"))
On Error GoTo ROW_ERR
amt = 规范金额为数值(valF)
On Error GoTo 0
' 币种(若存在币种列)
If curColIdx > 0 Then
sCur = Trim$(UCase$(CStr(取合并或本格值(ws.Cells(rowN, curColIdx)))))
If sCur = "" Then sCur = 猜测币种自金额文本(valF)
Else
sCur = 猜测币种自金额文本(valF)
End If
If sCur = "" Then sCur = "CNY"
Dim rate As Double
rate = 获取汇率(dictRate, sCur)
amtCNY = amt * rate
' 聚合
Dim key As String: key = sDept & "|" & sCat & "|" & sCC
If dictAgg.Exists(key) Then
dictAgg(key) = CDbl(dictAgg(key)) + amtCNY
Else
dictAgg.Add key, amtCNY
End If
' 记录成本中心集合
Dim kc As String: kc = sDept & "|" & sCat
If Not dictCCMap.Exists(kc) Then
Dim d As Object: Set d = CreateObject("Scripting.Dictionary")
d.Add sCC, True
dictCCMap.Add kc, d
Else
If Not dictCCMap(kc).Exists(sCC) Then dictCCMap(kc).Add sCC, True
End If
cntInSheet = cntInSheet + 1
NEXTROW: Next r Next area
processedRows = processedRows + cntInSheet
skippedRows = skippedRows + skippedInSheet
追加日志 logLines, "工作表[" & ws.Name & "] 已处理: " & cntInSheet & " 行,可见且在2025-03;跳过: " & skippedInSheet
Exit Sub
ROW_ERR: errorCount = errorCount + 1 追加日志 logLines, "【错误】工作表[" & ws.Name & "] 第" & rowN & "行 金额解析失败: " & Err.Description Err.Clear Resume NEXTROW End Sub
' 解析日期 Private Function 解析日期(ByVal v As Variant) As Variant On Error GoTo EH If IsDate(v) Then 解析日期 = CDate(v) Exit Function End If Dim s As String: s = Trim$(CStr(v)) s = Replace$(s, ".", "-") s = Replace$(s, "/", "-") s = Replace$(s, "年", "-") s = Replace$(s, "月", "-") s = Replace$(s, "日", "") If IsDate(s) Then 解析日期 = CDate(s) Exit Function End If EH: End Function
' 从单元格获取值,若合并则取合并区域首格 Private Function 取合并或本格值(ByVal c As Range) As Variant On Error GoTo EH If c.MergeCells Then 取合并或本格值 = c.MergeArea.Cells(1, 1).Value Else 取合并或本格值 = c.Value End If Exit Function EH: 取合并或本格值 = c.Value End Function
' 文本规范 + 空白回退至上一值 Private Function 规范文本(ByVal v As Variant, ByVal lastVal As String) As String Dim s As String s = CStr(v) s = Replace$(s, ChrW(12288), " ") ' 全角空格 s = Trim$(s) If LenB(s) = 0 Then 规范文本 = lastVal Else 规范文本 = s End If End Function
' 金额规范化(去千分位、括号负数、文本转数值,支持中文括号) Private Function 规范金额为数值(ByVal v As Variant) As Double Dim s As String If IsNumeric(v) Then 规范金额为数值 = CDbl(v) Exit Function End If s = CStr(v) s = Replace$(s, ",", "") s = Replace$(s, ",", "") s = Trim$(s) ' 括号负数 s = Replace$(s, "(", "-") s = Replace$(s, ")", "") s = Replace$(s, "(", "-") s = Replace$(s, ")", "") ' 去掉可能的货币符号 s = Replace$(s, "¥", "") s = Replace$(s, "¥", "") s = Replace$(s, "$", "") s = Replace$(s, "USD", "", , , vbTextCompare) s = Replace$(s, "CNY", "", , , vbTextCompare) s = Replace$(s, "RMB", "", , , vbTextCompare) s = Trim$(s) If s = "" Or s = "-" Then s = "0" If Not IsNumeric(s) Then Err.Raise 5, , "金额非数值: " & v 规范金额为数值 = CDbl(s) End Function
' 猜测币种(从金额文本前缀/后缀中尝试提取) Private Function 猜测币种自金额文本(ByVal v As Variant) As String Dim s As String: s = Trim$(CStr(v)) If s Like "USD" Then 猜测币种自金额文本 = "USD": Exit Function If s Like "RMB" Or s Like "CNY" Or InStr(1, s, "¥") > 0 Or InStr(1, s, "¥") > 0 Then 猜测币种自金额文本 = "CNY": Exit Function End If End Function
' 汇率获取(默认CNY=1;若找不到币种,=1) Private Function 获取汇率(ByVal dictRate As Object, ByVal cur As String) As Double Dim k As String: k = Trim$(UCase$(cur)) If k = "" Or k = "CNY" Or k = "RMB" Or k = "¥" Or k = "¥" Then 获取汇率 = 1# Exit Function End If If dictRate.Exists(k) Then 获取汇率 = CDbl(dictRate(k)) Else 获取汇率 = 1# End If End Function
' 载入汇率 B:C(币种-汇率) Private Sub 载入汇率字典(ByVal dictRate As Object, ByRef logLines As Collection) If Not WorksheetExists("字典") Then 追加日志 logLines, "【警告】未找到工作表[字典],将按CNY=1处理。" Exit Sub End If Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("字典") Dim lastRow As Long: lastRow = ws.Cells(ws.Rows.Count, "B").End(xlUp).Row Dim i As Long For i = 2 To lastRow Dim cur As String, rt As Variant cur = Trim$(UCase$(CStr(ws.Cells(i, "B").Value))) rt = ws.Cells(i, "C").Value If cur <> "" And IsNumeric(rt) Then dictRate(cur) = CDbl(rt) End If Next i 追加日志 logLines, "已载入汇率条目: " & dictRate.Count End Sub
' 载入预算 A:D(按 部门/费用类别 配置预算) ' 假定:A=部门, B=费用类别, C=预算(金额),D=备注(可选) Private Sub 载入预算字典(ByVal dictBudget As Object, ByRef logLines As Collection) If Not WorksheetExists("预算") Then 追加日志 logLines, "【警告】未找到工作表[预算],预算将按0处理。" Exit Sub End If Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("预算") Dim lastRow As Long: lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row Dim i As Long For i = 2 To lastRow Dim dept As String, cat As String, bgtV As Variant, key As String dept = Trim$(CStr(ws.Cells(i, "A").Value)) cat = Trim$(CStr(ws.Cells(i, "B").Value)) bgtV = ws.Cells(i, "C").Value If dept <> "" And cat <> "" And IsNumeric(bgtV) Then key = dept & "|" & cat dictBudget(key) = CDbl(bgtV) End If Next i 追加日志 logLines, "已载入预算条目: " & dictBudget.Count End Sub
' 输出汇总表(清除历史,写入tblSummary) Private Sub 设置汇总表(ByVal wsSum As Worksheet, ByVal dictAgg As Object, ByVal dictBudget As Object, _ ByVal dictCCMap As Object, ByRef tbl As ListObject, ByRef logLines As Collection) Dim lo As ListObject On Error Resume Next Set lo = wsSum.ListObjects("tblSummary") On Error GoTo 0
Application.DisplayAlerts = False
If Not lo Is Nothing Then
lo.DataBodyRange.ClearContents
lo.Unlist
End If
wsSum.Cells.Clear
Application.DisplayAlerts = True
' 表头
wsSum.Range("A1:F1").Value = Array("部门", "费用类别", "成本中心", "本月金额", "预算", "差异")
' 将聚合结果转换为数组
Dim n As Long: n = dictAgg.Count
Dim dataArr() As Variant
If n = 0 Then
ReDim dataArr(1 To 1, 1 To 6)
Else
ReDim dataArr(1 To n, 1 To 6)
End If
Dim i As Long: i = 0
Dim k As Variant
For Each k In dictAgg.Keys
i = i + 1
Dim parts() As String: parts = Split(CStr(k), "|")
Dim dept As String, cat As String, cc As String
dept = parts(0): cat = parts(1): cc = parts(2)
Dim amt As Double: amt = CDbl(dictAgg(k))
' 按部门/费用类别取预算,并按成本中心数量等分,避免透视重复计入
Dim kc As String: kc = dept & "|" & cat
Dim bgtTotal As Double: bgtTotal = 0#
Dim bgtAlloc As Double: bgtAlloc = 0#
If dictBudget.Exists(kc) Then bgtTotal = CDbl(dictBudget(kc))
Dim ccCnt As Long: ccCnt = 1
If dictCCMap.Exists(kc) Then
ccCnt = Application.Max(1, dictCCMap(kc).Count)
End If
bgtAlloc = bgtTotal / ccCnt
dataArr(i, 1) = dept
dataArr(i, 2) = cat
dataArr(i, 3) = cc
dataArr(i, 4) = amt
dataArr(i, 5) = bgtAlloc
dataArr(i, 6) = amt - bgtAlloc
Next k
If n > 0 Then
wsSum.Range("A2").Resize(n, 6).Value = dataArr
End If
' 创建结构化表
Dim lastRow As Long: lastRow = Application.Max(2, wsSum.Cells(wsSum.Rows.Count, "A").End(xlUp).Row)
Dim loRng As Range: Set loRng = wsSum.Range("A1:F" & lastRow)
Set tbl = wsSum.ListObjects.Add(xlSrcRange, loRng, , xlYes)
On Error Resume Next
tbl.Name = "tblSummary"
On Error GoTo 0
' 格式化金额列
Dim nf As String: nf = "_(* #,##0.00_);_(* (#,##0.00);_(* ""-""??_);_(@_)"
tbl.ListColumns("本月金额").DataBodyRange.NumberFormat = nf
tbl.ListColumns("预算").DataBodyRange.NumberFormat = nf
tbl.ListColumns("差异").DataBodyRange.NumberFormat = nf
' 自动列宽
wsSum.Columns("A:F").AutoFit
追加日志 logLines, "汇总表已生成,记录数: " & n
End Sub
' 生成或刷新透视表 Private Sub 生成或刷新透视表(ByVal tbl As ListObject, ByRef logLines As Collection) Dim wsPvt As Worksheet: Set wsPvt = 获取或创建工作表("透视")
' 清理旧透视
Dim pt As PivotTable
On Error Resume Next
Set pt = wsPvt.PivotTables("pvtDeptCost")
If Not pt Is Nothing Then
pt.TableRange2.Clear
End If
On Error GoTo 0
' 建Cache
Dim pc As PivotCache
Dim src As String
src = "'" & tbl.Parent.Name & "'!" & tbl.Range.Address(ReferenceStyle:=xlR1C1)
Set pc = ThisWorkbook.PivotCaches.Create(SourceType:=xlDatabase, SourceData:=src, Version:=xlPivotTableVersion15)
' 放置透视
Dim dst As Range: Set dst = wsPvt.Range("A3")
Set pt = pc.CreatePivotTable(TableDestination:=dst, TableName:="pvtDeptCost")
' 布局:行字段
With pt
.ClearAllFilters
On Error Resume Next
.PivotFields("部门").Orientation = xlRowField
.PivotFields("费用类别").Orientation = xlRowField
On Error GoTo 0
' 数据字段
Dim df As PivotField
.AddDataField .PivotFields("本月金额"), "实际", xlSum
.AddDataField .PivotFields("预算"), "预算", xlSum
.AddDataField .PivotFields("差异"), "差异", xlSum
' 数字格式
For Each df In .DataFields
df.NumberFormat = "_(* #,##0.00_);_(* (#,##0.00);_(* ""-""??_);_(@_)"
Next df
' 值字段置于列方向,便于锁定“差异”列
.DataPivotField.Orientation = xlColumnField
.RowAxisLayout xlOutlineRow
.RepeatAllLabels xlRepeatLabels
.ColumnGrand = True
.RowGrand = True
.NullString = "-"
End With
' 条件格式:差异>0 红色;差异绝对值Top10深色
应用透视条件格式 pt
' 自动列宽
wsPvt.Columns.AutoFit
追加日志 logLines, "透视表[pvtDeptCost] 已生成/刷新。"
End Sub
' 在透视表上应用条件格式 Private Sub 应用透视条件格式(ByVal pt As PivotTable) On Error Resume Next Dim rngData As Range: Set rngData = pt.DataBodyRange If rngData Is Nothing Then Exit Sub
' 找到“差异”在数据体中的列索引(标题在数据体上一行)
Dim hdrRow As Range: Set hdrRow = rngData.Offset(-1, 0).Resize(1, rngData.Columns.Count)
Dim idxDiff As Long: idxDiff = 0
Dim i As Long
For i = 1 To hdrRow.Columns.Count
If CStr(hdrRow.Cells(1, i).Value) = "差异" Then
idxDiff = i: Exit For
End If
Next i
If idxDiff = 0 Then Exit Sub
Dim rngDiff As Range: Set rngDiff = rngData.Columns(idxDiff)
' 清除历史条件格式
rngDiff.FormatConditions.Delete
' 1) 差异>0 红色(表示实际>预算)
Dim fc As FormatCondition
Set fc = rngDiff.FormatConditions.Add(Type:=xlCellValue, Operator:=xlGreater, Formula1:="=0")
With fc.Font
.Color = vbRed
.Bold = True
End With
' 2) 差异绝对值前10项深色高亮(精确挑选前10个 | 仅作用于差异列)
' 收集值
Dim vals As Variant, addrs As Variant
Dim c As Range, cnt As Long
ReDim vals(1 To rngDiff.Cells.Count)
ReDim addrs(1 To rngDiff.Cells.Count)
For Each c In rngDiff.Cells
cnt = cnt + 1
vals(cnt) = Abs(NzDouble(c.Value))
addrs(cnt) = c.Address(False, False)
Next c
If cnt > 0 Then
' 简单选择前10(选择排序以避免依赖排序库)
Dim topIdx() As Long, topVal() As Double, k As Long, j As Long
ReDim topIdx(1 To 10)
ReDim topVal(1 To 10)
For k = 1 To cnt
Dim v As Double: v = CDbl(vals(k))
For j = 1 To 10
If v > topVal(j) Then
Dim jj As Long
For jj = 10 To j + 1 Step -1
topVal(jj) = topVal(jj - 1)
topIdx(jj) = topIdx(jj - 1)
Next jj
topVal(j) = v
topIdx(j) = k
Exit For
End If
Next j
Next k
' 应用深色底纹
For j = 1 To 10
If topIdx(j) > 0 Then
rngDiff.Parent.Range(addrs(topIdx(j))).Interior.Color = RGB(64, 64, 64)
rngDiff.Parent.Range(addrs(topIdx(j))).Font.Color = RGB(255, 255, 255)
rngDiff.Parent.Range(addrs(topIdx(j))).Font.Bold = True
End If
Next j
End If
End Sub
' 冻结首行 Private Sub 冻结首行(ByVal ws As Worksheet) With ws .Activate .Range("A2").Select ActiveWindow.FreezePanes = False ActiveWindow.SplitRow = 1 ActiveWindow.FreezePanes = True .Range("A1").Select End With End Sub
' 汇总页底部时间戳与计数 Private Sub 写入时间戳与计数(ByVal ws As Worksheet, ByVal t0 As Date, ByVal sheetCnt As Long, _ ByVal rowCnt As Long, ByVal skipped As Long, ByVal errCnt As Long, ByVal backupPath As String) Dim lastRow As Long: lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row + 2 With ws .Cells(lastRow, "A").Value = "生成时间:" .Cells(lastRow, "B").Value = Format(Now, "yyyy-mm-dd HH:NN:SS") .Cells(lastRow + 1, "A").Value = "处理工作表数:" .Cells(lastRow + 1, "B").Value = sheetCnt .Cells(lastRow + 2, "A").Value = "有效行数:" .Cells(lastRow + 2, "B").Value = rowCnt .Cells(lastRow + 3, "A").Value = "跳过行数:" .Cells(lastRow + 3, "B").Value = skipped .Cells(lastRow + 4, "A").Value = "错误计数:" .Cells(lastRow + 4, "B").Value = errCnt .Cells(lastRow + 5, "A").Value = "备份路径:" .Cells(lastRow + 5, "B").Value = backupPath .Range(.Cells(lastRow, "A"), .Cells(lastRow + 5, "B")).EntireColumn.AutoFit End With End Sub
' 日志追加 Private Sub 追加日志(ByRef logLines As Collection, ByVal line As String) logLines.Add Format(Now, "HH:NN:SS") & " - " & line End Sub
' 写入日志到文件(UTF-8) Private Sub 写入日志到文件(ByVal logLines As Collection) On Error Resume Next Dim fso As Object: Set fso = CreateObject("Scripting.FileSystemObject") Dim folderPath As String: folderPath = ThisWorkbook.Path If LenB(folderPath) = 0 Then folderPath = CreateObject("WScript.Shell").SpecialFolders("Desktop") Dim logPath As String logPath = folderPath & "" & "月结汇总日志_" & Format(Now, "yyyymmdd_HHNNSS") & ".txt"
Dim stm As Object ' ADODB.Stream 以UTF-8写入
Set stm = CreateObject("ADODB.Stream")
stm.Type = 2 ' text
stm.Charset = "UTF-8"
stm.Open
Dim i As Long
For i = 1 To logLines.Count
stm.WriteText logLines(i), 1
Next i
stm.SaveToFile logPath, 2
stm.Close
End Sub
' 备份当前工作簿 Private Function 进行工作簿备份(ByRef logLines As Collection) As String On Error GoTo EH If Not ThisWorkbook.Saved Then ThisWorkbook.Save Dim folderPath As String: folderPath = ThisWorkbook.Path If LenB(folderPath) = 0 Then folderPath = CreateObject("WScript.Shell").SpecialFolders("Desktop") End If Dim baseName As String: baseName = ThisWorkbook.Name Dim p As Long: p = InStrRev(baseName, ".") If p > 0 Then baseName = Left$(baseName, p - 1) Dim backupName As String backupName = baseName & "backup" & Format(Now, "yyyymmdd_HHNNSS") & ".xlsx" Dim backupFull As String: backupFull = folderPath & "" & backupName ThisWorkbook.SaveCopyAs backupFull 追加日志 logLines, "已完成工作簿备份: " & backupFull 进行工作簿备份 = backupFull Exit Function EH: 追加日志 logLines, "【警告】备份失败: " & Err.Description End Function
' 获取或创建工作表 Private Function 获取或创建工作表(ByVal name As String) As Worksheet If WorksheetExists(name) Then Set 获取或创建工作表 = ThisWorkbook.Worksheets(name) Else Set 获取或创建工作表 = ThisWorkbook.Worksheets.Add(After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count)) 获取或创建工作表.Name = name End If End Function
' 工作表存在判断 Private Function WorksheetExists(ByVal name As String) As Boolean On Error Resume Next WorksheetExists = Not ThisWorkbook.Worksheets(name) Is Nothing On Error GoTo 0 End Function
' 将Variant转为Double(空或错误时=0) Private Function NzDouble(ByVal v As Variant) As Double On Error Resume Next If IsError(v) Or IsEmpty(v) Or v = "" Then NzDouble = 0# Else NzDouble = CDbl(v) End If End Function
Option Explicit
' 合并主过程 Public Sub Consolidate_TB_2025_10() Dim t0 As Double, msg As String t0 = Timer
Dim PERIOD As String: PERIOD = "2025-10"
Dim fldr As String
fldr = PickFolder("请选择包含各子公司试算表的目录(形如:XX_公司_TB_" & PERIOD & ".xlsx)")
If Len(fldr) = 0 Then Exit Sub
Dim files As Collection
Set files = ListMatchedFiles(fldr, "*_公司_TB_" & PERIOD & ".xlsx")
If files Is Nothing Or files.Count = 0 Then
MsgBox "未在所选目录找到匹配文件:*_公司_TB_" & PERIOD & ".xlsx", vbExclamation
Exit Sub
End If
Dim backupPath As String
backupPath = CreateBackupFolder(fldr)
CopyMatchedFilesToBackup files, backupPath
Dim shtLog As Worksheet, shtRaw As Worksheet, shtRpt As Worksheet
Set shtLog = GetOrCreateSheet("合并日志")
PrepareLogSheet shtLog
Set shtRaw = GetOrCreateSheet("Consol_Raw")
Dim tblRaw As ListObject
Set tblRaw = EnsureConsolRawTable(shtRaw)
ClearListObject tblRaw
Set shtRpt = GetOrCreateSheet("合并报表")
shtRpt.Cells.Clear
Dim dictRates As Object, dictMap As Object
Set dictRates = CreateObject("Scripting.Dictionary")
Set dictMap = CreateObject("Scripting.Dictionary")
Dim rateLoadMsg As String, mapLoadMsg As String
rateLoadMsg = LoadRates(dictRates, shtLog)
mapLoadMsg = LoadMapping(dictMap, shtLog)
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim prevCalc As XlCalculation: prevCalc = Application.Calculation
Application.Calculation = xlCalculationManual
Application.DisplayStatusBar = True
Dim totalFiles As Long: totalFiles = files.Count
Dim fileIdx As Long
' 结果缓存
Dim outArr() As Variant
Dim outCols As Long: outCols = 12
Dim capacity As Long: capacity = 50000
Dim outPtr As Long: outPtr = 0
ReDim outArr(1 To capacity, 1 To outCols)
' 统计
Dim cntRows As Long, cntErr As Long, cntUnmapped As Long, cntMissingRate As Long, cntFilesBalanced As Long, cntFilesUnbalanced As Long
Dim wb As Workbook, wbName As String, wbPath As String
Dim sh As Worksheet, rngData As Range, hasTable As Boolean
Dim i As Long, lastRow As Long
Dim debCNY As Double, creCNY As Double
Dim debSumCNY As Double, creSumCNY As Double
Dim companyCode As String, acctLocal As String, acctName As String, ccy As String
Dim vE, vF, vG As Variant
Dim hadErrE As Boolean, hadErrF As Boolean, hadErrG As Boolean, errTxt As String
Dim rate As Double, hasRate As Boolean
Dim mapGroup As String, mapCat As String, dirSign As Double
Dim dNet As Double, dBal As Double, dDeb As Double, dCre As Double
Dim loadStamp As String: loadStamp = Format(Now, "yyyy-mm-dd hh:nn:ss")
For fileIdx = 1 To totalFiles
wbPath = files(fileIdx)
wbName = Dir(wbPath)
Application.StatusBar = "合并进度 " & Format(fileIdx / totalFiles, "0%") & " … 正在处理:" & wbName
DoEvents
On Error Resume Next
Set wb = Application.Workbooks.Open(Filename:=wbPath, ReadOnly:=True, UpdateLinks:=False, Notify:=False, AddToMru:=False)
If wb Is Nothing Then
On Error GoTo 0
LogEvent shtLog, loadStamp, wbName, "", "", "文件无法打开", "无法以只读方式打开"
cntErr = cntErr + 1
GoTo NextFile
End If
On Error GoTo 0
Set sh = Nothing
On Error Resume Next
Set sh = wb.Worksheets("TB")
On Error GoTo 0
If sh Is Nothing Then
LogEvent shtLog, loadStamp, wbName, "", "", "缺少工作表", "未找到工作表 'TB'"
cntErr = cntErr + 1
GoTo CloseFile
End If
' 确定数据范围:优先使用第一个表对象,否则使用A:G
Set rngData = Nothing
hasTable = False
If sh.ListObjects.Count > 0 Then
If Not sh.ListObjects(1).DataBodyRange Is Nothing Then
Set rngData = sh.ListObjects(1).DataBodyRange
hasTable = True
End If
End If
If rngData Is Nothing Then
lastRow = sh.Cells(sh.Rows.Count, "A").End(xlUp).Row
If lastRow < 2 Then
LogEvent shtLog, loadStamp, wbName, "", "", "空数据", "TB无数据"
GoTo CloseFile
End If
Set rngData = sh.Range("A2:G" & lastRow)
End If
' 汇总本本账借贷(CNY)
debSumCNY = 0
creSumCNY = 0
' 逐行处理(跳过隐藏行)
Dim r As Range, area As Range
' 尽量按可见区域块处理
On Error Resume Next
For Each area In rngData.SpecialCells(xlCellTypeVisible).Areas
For Each r In area.Rows
' 跳过整行隐藏
If r.EntireRow.Hidden Then GoTo ContinueRow
companyCode = CStr(GetCellText(r.Cells(1, 1).Value))
acctLocal = CStr(GetCellText(r.Cells(1, 2).Value))
acctName = CStr(GetCellText(r.Cells(1, 3).Value))
ccy = UCase(Trim(CStr(GetCellText(r.Cells(1, 4).Value))))
hadErrE = False: hadErrF = False: hadErrG = False: errTxt = ""
vE = r.Cells(1, 5).Value
vF = r.Cells(1, 6).Value
vG = r.Cells(1, 7).Value
dDeb = SafeToDouble(vE, hadErrE)
dCre = SafeToDouble(vF, hadErrF)
dBal = SafeToDouble(vG, hadErrG)
If hadErrE Then
LogEvent shtLog, loadStamp, wbName, companyCode, acctLocal, "数值错误", "借方含错误,已按0处理"
cntErr = cntErr + 1
End If
If hadErrF Then
LogEvent shtLog, loadStamp, wbName, companyCode, acctLocal, "数值错误", "贷方含错误,已按0处理"
cntErr = cntErr + 1
End If
If hadErrG Then
LogEvent shtLog, loadStamp, wbName, companyCode, acctLocal, "数值错误", "余额含错误,已按0处理"
cntErr = cntErr + 1
End If
hasRate = dictRates.Exists(ccy)
If Not hasRate Then
LogEvent shtLog, loadStamp, wbName, companyCode, acctLocal, "缺少汇率", "币种:" & ccy
cntMissingRate = cntMissingRate + 1
GoTo ContinueRow
End If
rate = CDbl(dictRates(ccy))
' 映射科目
mapGroup = ""
mapCat = "UNDEF"
dirSign = 1
If dictMap.Exists(acctLocal) Then
Dim mi As Variant
mi = dictMap(acctLocal) ' mi(0)=Group, mi(1)=Cat, mi(2)=Dir
mapGroup = CStr(mi(0))
mapCat = CStr(mi(1))
dirSign = CDbl(mi(2))
Else
cntUnmapped = cntUnmapped + 1
mapGroup = "UNMAPPED"
mapCat = "UNDEF"
LogEvent shtLog, loadStamp, wbName, companyCode, acctLocal, "缺少映射", "未在‘科目映射’中找到对应"
End If
' 折算为CNY
debCNY = dDeb * rate
creCNY = dCre * rate
dBal = dBal * rate
dNet = (debCNY - creCNY) * dirSign
' 汇总本账借贷平衡(按CNY)
debSumCNY = debSumCNY + debCNY
creSumCNY = creSumCNY + creCNY
' 输出到缓存
outPtr = outPtr + 1
If outPtr > capacity Then
capacity = capacity + 50000
ReDim Preserve outArr(1 To capacity, 1 To outCols)
End If
outArr(outPtr, 1) = companyCode ' 公司
outArr(outPtr, 2) = mapGroup ' 集团科目
outArr(outPtr, 3) = ccy ' 币种
outArr(outPtr, 4) = Round(debCNY, 2) ' 本位借方
outArr(outPtr, 5) = Round(creCNY, 2) ' 本位贷方
outArr(outPtr, 6) = Round(dBal, 2) ' 本位余额
outArr(outPtr, 7) = Round(dNet, 2) ' 净额
outArr(outPtr, 8) = mapCat ' 分类/口径
outArr(outPtr, 9) = wbName ' 源文件
outArr(outPtr, 10) = loadStamp ' 加载时间
outArr(outPtr, 11) = acctLocal ' 本地科目
outArr(outPtr, 12) = acctName ' 本地科目名称
cntRows = cntRows + 1
ContinueRow: ' no-op Next r Next area On Error GoTo 0
' 借贷平衡校验(每本账)
Dim diff As Double
diff = Round(debSumCNY - creSumCNY, 2)
If Abs(diff) > 0.01 Then
LogEvent shtLog, loadStamp, wbName, "", "", "借贷不平衡", "借方合计(CNY)=" & Format(debSumCNY, "#,##0.00") & _
";贷方合计(CNY)=" & Format(creSumCNY, "#,##0.00") & _
";差异=" & Format(diff, "#,##0.00")
cntFilesUnbalanced = cntFilesUnbalanced + 1
Else
cntFilesBalanced = cntFilesBalanced + 1
End If
CloseFile: On Error Resume Next wb.Close SaveChanges:=False Set wb = Nothing On Error GoTo 0 NextFile: ' 继续下一个文件 Next fileIdx
' 将缓存写入Consol_Raw表
If outPtr > 0 Then
WriteToListObject tblRaw, outArr, outPtr, outCols
End If
' 构建透视与统一报表
BuildReport shtRpt, tblRaw
' 输出执行摘要到日志
msg = "处理摘要: 文件数=" & totalFiles & ";已写入记录=" & cntRows & _
";映射缺失=" & cntUnmapped & ";汇率缺失=" & cntMissingRate & _
";数值错误=" & cntErr & ";平衡通过=" & cntFilesBalanced & ";平衡不通过=" & cntFilesUnbalanced
LogEvent shtLog, loadStamp, "SYSTEM", "", "", "SUMMARY", msg
' 自动保存:备份当前工作簿副本到所选目录
Dim saveName As String
saveName = fldr & Application.PathSeparator & "合并结果_" & PERIOD & "_" & Format(Now, "yyyymmdd_hhnnss") & ".xlsm"
On Error Resume Next
ThisWorkbook.Save
ThisWorkbook.SaveCopyAs saveName
On Error GoTo 0
Application.StatusBar = False
Application.Calculation = prevCalc
Application.EnableEvents = True
Application.ScreenUpdating = True
' 生成外部日志文件
WriteExternalLog fldr, PERIOD, msg
MsgBox "合并完成。" & vbCrLf & msg & vbCrLf & "结果已保存为:" & saveName, vbInformation
End Sub
' 选择文件夹 Private Function PickFolder(titleText As String) As String Dim fd As FileDialog Set fd = Application.FileDialog(msoFileDialogFolderPicker) With fd .Title = titleText .AllowMultiSelect = False If .Show = -1 Then PickFolder = .SelectedItems(1) Else PickFolder = "" End If End With End Function
' 列出匹配文件 Private Function ListMatchedFiles(ByVal folderPath As String, ByVal pattern As String) As Collection Dim col As New Collection Dim f As String If Right(folderPath, 1) <> Application.PathSeparator Then folderPath = folderPath & Application.PathSeparator f = Dir(folderPath & pattern, vbNormal) Do While Len(f) > 0 col.Add folderPath & f f = Dir() Loop Set ListMatchedFiles = col End Function
' 创建备份目录 Private Function CreateBackupFolder(ByVal parentFolder As String) As String Dim p As String If Right(parentFolder, 1) <> Application.PathSeparator Then parentFolder = parentFolder & Application.PathSeparator p = parentFolder & "Backup_" & Format(Now, "yyyymmdd_hhnnss") On Error Resume Next MkDir p On Error GoTo 0 CreateBackupFolder = p End Function
' 复制源文件到备份目录 Private Sub CopyMatchedFilesToBackup(files As Collection, backupFolder As String) Dim i As Long, src As String, dst As String If Right(backupFolder, 1) <> Application.PathSeparator Then backupFolder = backupFolder & Application.PathSeparator For i = 1 To files.Count src = files(i) dst = backupFolder & Dir(src) On Error Resume Next FileCopy src, dst On Error GoTo 0 Next i End Sub
' 获取或创建工作表 Private Function GetOrCreateSheet(ByVal sheetName As String) As Worksheet Dim sh As Worksheet On Error Resume Next Set sh = ThisWorkbook.Worksheets(sheetName) On Error GoTo 0 If sh Is Nothing Then Set sh = ThisWorkbook.Worksheets.Add(After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count)) sh.Name = sheetName End If Set GetOrCreateSheet = sh End Function
' 准备日志表 Private Sub PrepareLogSheet(ByVal sht As Worksheet) sht.Cells.Clear With sht.Range("A1:L1") .Value = Array("时间", "文件", "公司", "本地科目", "类型", "详情", "", "", "", "", "", "") End With End Sub
' 写日志记录 Private Sub LogEvent(ByVal sht As Worksheet, ByVal ts As String, ByVal fileName As String, ByVal company As String, ByVal acct As String, ByVal typ As String, ByVal detail As String) Dim r As Long r = sht.Cells(sht.Rows.Count, "A").End(xlUp).Row + 1 sht.Cells(r, 1).Value = ts sht.Cells(r, 2).Value = fileName sht.Cells(r, 3).Value = company sht.Cells(r, 4).Value = acct sht.Cells(r, 5).Value = typ sht.Cells(r, 6).Value = detail End Sub
' 加载汇率:Rates!A:B(A=币种,B=对CNY汇率) Private Function LoadRates(dictRates As Object, shtLog As Worksheet) As String Dim sht As Worksheet On Error Resume Next Set sht = ThisWorkbook.Worksheets("Rates") On Error GoTo 0 If sht Is Nothing Then LogEvent shtLog, Format(Now, "yyyy-mm-dd hh:nn:ss"), "SYSTEM", "", "", "配置缺失", "未找到工作表 'Rates'" Exit Function End If
Dim lastRow As Long: lastRow = sht.Cells(sht.Rows.Count, "A").End(xlUp).Row
Dim i As Long, ccy As String, rate As Double, v
For i = 2 To lastRow
ccy = UCase(Trim(CStr(sht.Cells(i, 1).Value)))
v = sht.Cells(i, 2).Value
If Len(ccy) > 0 Then
If IsError(v) Or Not IsNumeric(v) Then
LogEvent shtLog, Format(Now, "yyyy-mm-dd hh:nn:ss"), "SYSTEM", "", "", "汇率错误", "币种=" & ccy & ";值无效"
Else
rate = CDbl(v)
If rate <= 0 Then
LogEvent shtLog, Format(Now, "yyyy-mm-dd hh:nn:ss"), "SYSTEM", "", "", "汇率错误", "币种=" & ccy & ";非正数"
Else
dictRates(ccy) = rate
End If
End If
End If
Next i
LoadRates = "OK"
End Function
' 加载科目映射:科目映射!A:C(A=本地科目,B=集团科目,C=属性:可包含口径/分类与方向,如“PL”、“BS:资产”、“DIR=-1;CAT=PL”) Private Function LoadMapping(dictMap As Object, shtLog As Worksheet) As String Dim sht As Worksheet On Error Resume Next Set sht = ThisWorkbook.Worksheets("科目映射") On Error GoTo 0 If sht Is Nothing Then LogEvent shtLog, Format(Now, "yyyy-mm-dd hh:nn:ss"), "SYSTEM", "", "", "配置缺失", "未找到工作表 '科目映射'" Exit Function End If
Dim lastRow As Long: lastRow = sht.Cells(sht.Rows.Count, "A").End(xlUp).Row
Dim i As Long
Dim acct As String, grp As String, attr As String
Dim cat As String, dirSign As Double
For i = 2 To lastRow
acct = Trim(CStr(sht.Cells(i, 1).Value))
grp = Trim(CStr(sht.Cells(i, 2).Value))
attr = Trim(CStr(sht.Cells(i, 3).Value))
If Len(acct) > 0 And Len(grp) > 0 Then
ParseMapAttr attr, cat, dirSign
Dim v(0 To 2) As Variant
v(0) = grp
v(1) = cat
v(2) = dirSign
dictMap(acct) = v
End If
Next i
LoadMapping = "OK"
End Function
' 解析映射属性:支持“DIR=-1;CAT=PL”或“PL”、“BS:资产”等 Private Sub ParseMapAttr(ByVal attr As String, ByRef cat As String, ByRef dirSign As Double) Dim a As String: a = UCase(Trim(attr)) dirSign = 1 cat = "UNDEF" If Len(a) = 0 Then Exit Sub
Dim parts() As String, i As Long, kv() As String
If InStr(a, "=") > 0 Or InStr(a, ";") > 0 Then
parts = Split(Replace(a, " ", ""), ";")
For i = LBound(parts) To UBound(parts)
If InStr(parts(i), "=") > 0 Then
kv = Split(parts(i), "=")
If UBound(kv) = 1 Then
Select Case Trim(kv(0))
Case "DIR"
If IsNumeric(kv(1)) Then dirSign = CDbl(kv(1))
Case "CAT", "分类", "口径"
cat = Trim(kv(1))
End Select
End If
Else
' 单值补充
If Left(parts(i), 2) = "PL" Or Left(parts(i), 2) = "BS" Then cat = parts(i)
End If
Next i
Else
cat = a
End If
End Sub
' 安全读取为数值(错误、空、非数返回0并标记) Private Function SafeToDouble(ByVal v As Variant, ByRef hadErr As Boolean) As Double hadErr = False If IsError(v) Or IsNull(v) Or IsEmpty(v) Then hadErr = True SafeToDouble = 0# ElseIf IsNumeric(v) Then SafeToDouble = CDbl(v) Else Dim s As String: s = Trim(CStr(v)) If Len(s) = 0 Then hadErr = False SafeToDouble = 0# ElseIf IsNumeric(s) Then SafeToDouble = CDbl(s) Else hadErr = True SafeToDouble = 0# End If End If End Function
' 安全获取文本 Private Function GetCellText(ByVal v As Variant) As String If IsError(v) Or IsNull(v) Or IsEmpty(v) Then GetCellText = "" Else GetCellText = CStr(v) End If End Function
' 创建或获取Consol_Raw表对象 Private Function EnsureConsolRawTable(ByVal sht As Worksheet) As ListObject Dim tbl As ListObject On Error Resume Next Set tbl = sht.ListObjects("tblConsolRaw") On Error GoTo 0 If tbl Is Nothing Then sht.Cells.Clear sht.Range("A1:L1").Value = Array("公司", "集团科目", "币种", "本位借方", "本位贷方", "本位余额", "净额", "分类", "源文件", "加载时间", "本地科目", "本地科目名称") Set tbl = sht.ListObjects.Add(xlSrcRange, sht.Range("A1").CurrentRegion, , xlYes) tbl.Name = "tblConsolRaw" Else ' 校正表头 tbl.HeaderRowRange.Cells(1, 1).Resize(1, 12).Value = Array("公司", "集团科目", "币种", "本位借方", "本位贷方", "本位余额", "净额", "分类", "源文件", "加载时间", "本地科目", "本地科目名称") End If Set EnsureConsolRawTable = tbl End Function
' 清空ListObject数据 Private Sub ClearListObject(ByVal tbl As ListObject) On Error Resume Next If Not tbl.DataBodyRange Is Nothing Then tbl.DataBodyRange.Delete End If On Error GoTo 0 End Sub
' 将数组写入ListObject Private Sub WriteToListObject(ByVal tbl As ListObject, ByRef dataArr As Variant, ByVal nRows As Long, ByVal nCols As Long) If nRows <= 0 Then Exit Sub Dim dest As Range Set dest = tbl.HeaderRowRange.Offset(1, 0).Cells(1, 1) Dim rg As Range Set rg = dest.Resize(nRows, nCols) rg.Value = dataArr tbl.Resize tbl.Range.Resize(nRows + 1, nCols) End Sub
' 构建透视与统一报表 Private Sub BuildReport(ByVal shtRpt As Worksheet, ByVal tblRaw As ListObject) Dim srcRange As Range Set srcRange = tblRaw.Range
Dim pc As PivotCache
On Error Resume Next
Set pc = ThisWorkbook.PivotCaches.Create(SourceType:=xlDatabase, SourceData:=srcRange.Address(True, True, xlA1, True))
On Error GoTo 0
Dim topLeft As Range
Set topLeft = shtRpt.Range("A1")
' pvtPL
Dim pvtPL As PivotTable
On Error Resume Next
shtRpt.PivotTables("pvtPL").TableRange2.Clear
On Error GoTo 0
Set pvtPL = shtRpt.PivotTables.Add(PivotCache:=pc, TableDestination:=topLeft, TableName:="pvtPL")
With pvtPL
.PivotFields("分类").Orientation = xlPageField
.PivotFields("分类").CurrentPage = "(All)"
On Error Resume Next
.PivotFields("分类").CurrentPage = "PL"
On Error GoTo 0
.PivotFields("集团科目").Orientation = xlRowField
.PivotFields("集团科目").Position = 1
.AddDataField .PivotFields("净额"), "净额合计", xlSum
.ColumnGrand = False
.RowGrand = True
.NullString = ""
.RepeatAllLabels xlRepeatLabels
End With
' pvtBS
Dim pvtBS As PivotTable
Dim dest2 As Range
Set dest2 = pvtPL.TableRange2.Offset(pvtPL.TableRange2.Rows.Count + 2, 0).Cells(1, 1)
On Error Resume Next
shtRpt.PivotTables("pvtBS").TableRange2.Clear
On Error GoTo 0
Set pvtBS = shtRpt.PivotTables.Add(PivotCache:=pc, TableDestination:=dest2, TableName:="pvtBS")
With pvtBS
.PivotFields("分类").Orientation = xlRowField
.PivotFields("分类").Position = 1
.PivotFields("集团科目").Orientation = xlRowField
.PivotFields("集团科目").Position = 2
.AddDataField .PivotFields("本位余额"), "余额合计", xlSum
.ColumnGrand = False
.RowGrand = True
.NullString = ""
.RepeatAllLabels xlRepeatLabels
End With
' 报表检查区
Dim chkStart As Range
Set chkStart = pvtBS.TableRange2.Offset(pvtBS.TableRange2.Rows.Count + 2, 0).Cells(1, 1)
chkStart.Offset(0, 0).Value = "检查合计:"
chkStart.Offset(1, 0).Value = "PL净额合计:"
chkStart.Offset(1, 1).FormulaR1C1 = "=GETPIVOTDATA(""净额合计"",R" & pvtPL.TableRange2.Row & "C" & pvtPL.TableRange2.Column & ")"
chkStart.Offset(2, 0).Value = "BS余额合计(资产+负债+权益之和):"
chkStart.Offset(2, 1).FormulaR1C1 = "=GETPIVOTDATA(""余额合计"",R" & pvtBS.TableRange2.Row & "C" & pvtBS.TableRange2.Column & ")"
chkStart.Offset(3, 0).Value = "期末余额交叉验证(示例):"
chkStart.Offset(3, 1).FormulaR1C1 = "=R[-2]C - R[-1]C"
chkStart.Offset(3, 2).Value = "(应接近0)"
shtRpt.Columns.AutoFit
End Sub
' 写外部日志文件 Private Sub WriteExternalLog(ByVal folderPath As String, ByVal period As String, ByVal summary As String) On Error Resume Next Dim fso As Object, ts As Object, logPath As String Set fso = CreateObject("Scripting.FileSystemObject") If Right(folderPath, 1) <> Application.PathSeparator Then folderPath = folderPath & Application.PathSeparator logPath = folderPath & "Consol_Log_" & period & "_" & Format(Now, "yyyymmdd_hhnnss") & ".txt" Set ts = fso.CreateTextFile(logPath, True, True) ts.WriteLine "合并执行时间: " & Format(Now, "yyyy-mm-dd hh:nn:ss") ts.WriteLine "期间: " & period ts.WriteLine summary ts.Close End Sub
' 注意:本过程将创建以下工作表(如不存在):Rates、科目映射、Consol_Raw、合并日志、合并报表 ' 数据表结构要求: ' TB工作表:A=公司编码,B=科目编码,C=科目名称,D=币种,E=借方,F=贷方,G=余额(部分单元格可为公式或错误) ' Rates工作表:A=币种,B=对CNY汇率 ' 科目映射工作表:A=本地科目,B=集团科目,C=属性(可包含CAT/口径/分类与DIR方向信息)
Option Explicit
' 运行入口 Public Sub 运行对账() Dim t0 As Single: t0 = Timer Dim logPath As String, backupPath As String Dim ok As Boolean
On Error GoTo EH
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
' 备份并记录路径以支持撤销
backupPath = 备份当前工作簿()
If Len(backupPath) = 0 Then Err.Raise 1001, , "备份失败"
ThisWorkbook.Names.Add Name:="LastReconBackup", RefersTo:="""" & backupPath & """"
' 日志文件
logPath = 本次日志路径()
记日志 logPath, "开始对账: " & Now
ok = 执行对账流程(logPath)
记日志 logPath, "对账结束: " & Now & ",耗时秒: " & Format$(Timer - t0, "0.0")
CleanExit: Application.Calculation = xlCalculationAutomatic Application.EnableEvents = True Application.ScreenUpdating = True
If ok Then
MsgBox "对账完成。日志: " & logPath, vbInformation
End If
Exit Sub
EH: 记日志 logPath, "错误: " & Err.Number & " - " & Err.Description MsgBox "发生错误: " & Err.Description & vbCrLf & "已在日志中记录。", vbCritical Resume CleanExit End Sub
' 撤销(从备份恢复) Public Sub 撤销上次对账() On Error GoTo EH Dim nm As Name, p As String For Each nm In ThisWorkbook.Names If LCase$(nm.Name) = LCase$("LastReconBackup") Then p = Replace(nm.RefersTo, "=", "") p = Replace(p, """", "") Exit For End If Next If Len(p) = 0 Then MsgBox "未找到备份路径。", vbExclamation Exit Sub End If
If Dir$(p, vbNormal) = "" Then
MsgBox "备份文件不存在: " & p, vbExclamation
Exit Sub
End If
Dim wb As Workbook
Set wb = Application.Workbooks.Open(p, ReadOnly:=False)
MsgBox "已打开备份文件。请以该文件替换当前文件或另存为。", vbInformation
Exit Sub
EH: MsgBox "撤销失败: " & Err.Description, vbCritical End Sub
' 核心流程 Private Function 执行对账流程(ByVal logPath As String) As Boolean Dim wsSales As Worksheet, wsBank As Worksheet Dim loSales As ListObject, loBank As ListObject Dim salesCols As Object, bankCols As Object
Set wsSales = 找表所在工作表("tblSales")
Set wsBank = 找表所在工作表("tblBank")
If wsSales Is Nothing Or wsBank Is Nothing Then Err.Raise 1002, , "未找到表:tblSales 或 tblBank"
Set loSales = wsSales.ListObjects("tblSales")
Set loBank = wsBank.ListObjects("tblBank")
If loSales.DataBodyRange Is Nothing Or loBank.DataBodyRange Is Nothing Then Err.Raise 1003, , "源表没有数据"
Set salesCols = CreateObject("Scripting.Dictionary")
salesCols.CompareMode = 1
salesCols("订单号") = 列序号(loSales, "订单号")
salesCols("客户") = 列序号(loSales, "客户")
salesCols("开票金额") = 列序号(loSales, "开票金额")
salesCols("开票日期") = 列序号(loSales, "开票日期")
Dim idxR As Long: idxR = IIf(存在列(loSales, "已收款标记"), 列序号(loSales, "已收款标记"), 0)
If idxR > 0 Then salesCols("已收款标记") = idxR
Set bankCols = CreateObject("Scripting.Dictionary")
bankCols.CompareMode = 1
bankCols("交易号") = 列序号(loBank, "交易号")
bankCols("摘要") = 列序号(loBank, "摘要")
bankCols("对方账号") = 列序号(loBank, "对方账号")
bankCols("金额") = 列序号(loBank, "金额")
bankCols("入账日期") = 列序号(loBank, "入账日期")
记日志 logPath, "开始清洗数据..."
清洗表金额与日期 loSales, salesCols("开票金额"), salesCols("开票日期")
清洗表金额与日期 loBank, bankCols("金额"), bankCols("入账日期")
Dim salesData As Collection, bankData As Collection
Dim orderDict As Object, custSet As Object
Set orderDict = CreateObject("Scripting.Dictionary"): orderDict.CompareMode = 1
Set custSet = CreateObject("Scripting.Dictionary"): custSet.CompareMode = 1
记日志 logPath, "读取销售数据..."
Set salesData = 读取销售(loSales, salesCols, orderDict, custSet)
记日志 logPath, "销售行数(可见): " & salesData.Count
记日志 logPath, "读取银行数据并预处理摘要..."
Dim hashedOrders As Object: Set hashedOrders = orderDict ' alias
Set bankData = 读取银行(loBank, bankCols, hashedOrders, custSet)
记日志 logPath, "银行行数(可见): " & bankData.Count
' 匹配
Dim mCountExact As Long, mCountTol As Long, mCountOneMany As Long, mCountManyOne As Long
记日志 logPath, "精确匹配开始..."
mCountExact = 匹配_精确_按订单号与金额(salesData, bankData, logPath)
记日志 logPath, "精确匹配完成: " & mCountExact
记日志 logPath, "容差一对一匹配开始..."
mCountTol = 匹配_容差一对一(salesData, bankData, logPath)
记日志 logPath, "容差一对一完成: " & mCountTol
记日志 logPath, "容差一对多(银行多笔->单张发票)开始..."
mCountOneMany = 匹配_一对多_银行到销售(salesData, bankData, logPath)
记日志 logPath, "容差一对多完成: " & mCountOneMany
记日志 logPath, "容差多对一(多发票->单笔入账)开始..."
mCountManyOne = 匹配_多对一_销售到银行(salesData, bankData, logPath)
记日志 logPath, "容差多对一完成: " & mCountManyOne
' 输出
记日志 logPath, "输出结果表..."
输出对账结果 salesData, bankData
记日志 logPath, "应用条件格式与处理意见下拉..."
设置结果格式和数据验证
记日志 logPath, "生成汇总透视..."
生成汇总透视
记日志 logPath, "未匹配清单..."
输出未匹配清单 salesData, bankData
记日志 logPath, "完成: 精确=" & mCountExact & " 一对一=" & mCountTol & " 一对多=" & mCountOneMany & " 多对一=" & mCountManyOne
执行对账流程 = True
End Function
' 读取与预处理:销售 Private Function 读取销售(lo As ListObject, ByVal cols As Object, ByRef orderDict As Object, ByRef custSet As Object) As Collection Dim col As New Collection Dim r As Range, i As Long Dim rng As Range: Set rng = lo.DataBodyRange For i = 1 To rng.Rows.Count If Not (rng.Rows(i).Hidden Or rng.Rows(i).EntireRow.Hidden) Then Set r = rng.Rows(i) Dim rec As Object: Set rec = CreateObject("Scripting.Dictionary") rec.CompareMode = 1 rec("Order") = Trim$(CStr(r.Cells(1, cols("订单号")).Value2)) rec("Customer") = Trim$(CStr(r.Cells(1, cols("客户")).Value2)) rec("Amount") = 规范金额(r.Cells(1, cols("开票金额")).Value2) rec("Date") = 规范日期(r.Cells(1, cols("开票日期")).Value2) rec("Row") = r.Row rec("Matched") = False rec("MatchedBankIdx") = CreateObject("System.Collections.ArrayList") rec("MatchMode") = "" rec("MatchDiff") = 0# If Len(rec("Order")) > 0 Then If Not orderDict.Exists(rec("Order")) Then orderDict(rec("Order")) = True If Len(rec("Customer")) > 0 Then If Not custSet.Exists(rec("Customer")) Then custSet(rec("Customer")) = True col.Add rec End If Next Set 读取销售 = col End Function
' 读取与预处理:银行 Private Function 读取银行(lo As ListObject, ByVal cols As Object, ByVal orderDict As Object, ByVal custSet As Object) As Collection Dim col As New Collection Dim rng As Range: Set rng = lo.DataBodyRange Dim i As Long For i = 1 To rng.Rows.Count If Not (rng.Rows(i).Hidden Or rng.Rows(i).EntireRow.Hidden) Then Dim r As Range: Set r = rng.Rows(i) Dim rec As Object: Set rec = CreateObject("Scripting.Dictionary") rec.CompareMode = 1 rec("TxnID") = Trim$(CStr(r.Cells(1, cols("交易号")).Value2)) rec("Summary") = Trim$(CStr(r.Cells(1, cols("摘要")).Value2)) rec("Account") = Trim$(CStr(r.Cells(1, cols("对方账号")).Value2)) rec("Amount") = 规范金额(r.Cells(1, cols("金额")).Value2) rec("Date") = 规范日期(r.Cells(1, cols("入账日期")).Value2) rec("Row") = r.Row rec("Matched") = False rec("MatchedSalesIdx") = CreateObject("System.Collections.ArrayList") rec("ExtractOrder") = 从摘要提取订单号(rec("Summary"), orderDict) rec("GuessCustomer") = 从摘要识别客户(rec("Summary"), custSet) col.Add rec End If Next Set 读取银行 = col End Function
' 精确匹配:摘要提取订单号,与销售订单号一致,金额相等 Private Function 匹配_精确_按订单号与金额(ByVal sales As Collection, ByVal bank As Collection, ByVal logPath As String) As Long Dim i As Long, j As Long, cnt As Long Dim s As Object, b As Object ' 加速:为订单号建立索引 -> 销售索引列表 Dim mapOrderToIdx As Object: Set mapOrderToIdx = CreateObject("Scripting.Dictionary"): mapOrderToIdx.CompareMode = 1 For i = 1 To sales.Count Set s = sales(i) If Not s("Matched") Then If Len(s("Order")) > 0 Then If Not mapOrderToIdx.Exists(s("Order")) Then Set mapOrderToIdx(s("Order")) = CreateObject("System.Collections.ArrayList") mapOrderToIdx(s("Order")).Add i End If End If Next
For j = 1 To bank.Count
Set b = bank(j)
If Not b("Matched") Then
Dim ord As String: ord = CStr(b("ExtractOrder"))
If Len(ord) > 0 Then
If mapOrderToIdx.Exists(ord) Then
' 在该订单的销售中找金额相等的未匹配项
Dim list As Object: Set list = mapOrderToIdx(ord)
Dim k As Long
For k = 0 To list.Count - 1
i = CLng(list(k))
Set s = sales(i)
If Not s("Matched") Then
If Abs(s("Amount") - b("Amount")) < 0.005 Then
' 匹配
s("Matched") = True
s("MatchedBankIdx").Add j
s("MatchMode") = "精确匹配(订单号+金额)"
s("MatchDiff") = Round(s("Amount") - b("Amount"), 2)
b("Matched") = True
b("MatchedSalesIdx").Add i
cnt = cnt + 1
Exit For
End If
End If
Next
End If
End If
End If
Next
匹配_精确_按订单号与金额 = cnt
End Function
' 容差一对一:±1元,日期±3天,优先客户命中 Private Function 匹配_容差一对一(ByVal sales As Collection, ByVal bank As Collection, ByVal logPath As String) As Long Dim cnt As Long, i As Long, j As Long Dim s As Object, b As Object For i = 1 To sales.Count Set s = sales(i) If Not s("Matched") Then Dim bestJ As Long: bestJ = 0 Dim bestScore As Double: bestScore = 1E+30 For j = 1 To bank.Count Set b = bank(j) If Not b("Matched") Then If 日期在范围(s("Date"), b("Date"), 3) Then Dim diff As Double: diff = Abs(s("Amount") - b("Amount")) If diff <= 1# + 0.0001 Then Dim score As Double score = diff If Len(s("Customer")) > 0 And Len(b("GuessCustomer")) > 0 Then If LCase$(s("Customer")) = LCase$(b("GuessCustomer")) Then score = score - 0.25 ' 客户命中加权 End If End If If score < bestScore Then bestScore = score bestJ = j End If End If End If End If Next If bestJ > 0 Then Set b = bank(bestJ) s("Matched") = True s("MatchedBankIdx").Add bestJ s("MatchMode") = "容差匹配(一对一)" s("MatchDiff") = Round(s("Amount") - b("Amount"), 2)
b("Matched") = True
b("MatchedSalesIdx").Add i
cnt = cnt + 1
End If
End If
Next
匹配_容差一对一 = cnt
End Function
' 一对多:同客户+日期±3天,组合多笔银行金额≈发票金额(±1),尝试对候选前N条做组合 Private Function 匹配_一对多_银行到销售(ByVal sales As Collection, ByVal bank As Collection, ByVal logPath As String) As Long Dim cnt As Long, i As Long, j As Long Dim s As Object, b As Object Dim N As Long: N = 10 ' 候选上限 For i = 1 To sales.Count Set s = sales(i) If Not s("Matched") Then Dim candIdx As Object: Set candIdx = CreateObject("System.Collections.ArrayList") Dim arrAmt() As Double, arrIdx() As Long ' 收集候选银行行 For j = 1 To bank.Count Set b = bank(j) If Not b("Matched") Then If 日期在范围(s("Date"), b("Date"), 3) Then ' 客户优先:如能识别客户,需一致;识别不到则放宽 If Len(b("GuessCustomer")) = 0 Or Len(s("Customer")) = 0 Or LCase$(b("GuessCustomer")) = LCase$(s("Customer")) Then candIdx.Add j End If End If End If Next ' 限制候选并按接近金额降序 If candIdx.Count > 1 Then candIdx.Sort New ComparerBankAmount(bank) ' 自定义比较器 End If If candIdx.Count > N Then Do While candIdx.Count > N candIdx.RemoveAt(candIdx.Count - 1) Loop End If If candIdx.Count = 0 Then GoTo ContinueNextSale
ReDim arrAmt(0 To candIdx.Count - 1)
ReDim arrIdx(0 To candIdx.Count - 1)
For j = 0 To candIdx.Count - 1
arrIdx(j) = candIdx(j)
arrAmt(j) = bank(arrIdx(j))("Amount")
Next
Dim chosen As Object: Set chosen = 尝试组合金额(s("Amount"), arrAmt, 1#)
If Not chosen Is Nothing Then
Dim tSum As Double: tSum = 0#
Dim k As Long
For k = 0 To chosen.Count - 1
Dim bj As Long: bj = arrIdx(chosen(k))
s("MatchedBankIdx").Add bj
bank(bj)("Matched") = True
bank(bj)("MatchedSalesIdx").Add i
tSum = tSum + bank(bj)("Amount")
Next
s("Matched") = True
s("MatchMode") = "容差匹配(一对多)"
s("MatchDiff") = Round(s("Amount") - tSum, 2)
cnt = cnt + 1
End If
End If
ContinueNextSale: Next 匹配_一对多_银行到销售 = cnt End Function
' 多对一:同客户+日期±3天,组合多张发票≈单笔银行金额(±1) Private Function 匹配_多对一_销售到银行(ByVal sales As Collection, ByVal bank As Collection, ByVal logPath As String) As Long Dim cnt As Long, i As Long, j As Long Dim b As Object, s As Object Dim N As Long: N = 12 ' 候选销售上限 For j = 1 To bank.Count Set b = bank(j) If Not b("Matched") Then ' 收集候选销售 Dim candIdx As Object: Set candIdx = CreateObject("System.Collections.ArrayList") Dim arrAmt() As Double, arrIdx() As Long, i2 As Long For i = 1 To sales.Count Set s = sales(i) If Not s("Matched") Then If 日期在范围(s("Date"), b("Date"), 3) Then If Len(b("GuessCustomer")) = 0 Or Len(s("Customer")) = 0 Or LCase$(b("GuessCustomer")) = LCase$(s("Customer")) Then candIdx.Add i End If End If End If Next If candIdx.Count > 1 Then candIdx.Sort New ComparerSalesAmount(sales) End If If candIdx.Count > N Then Do While candIdx.Count > N candIdx.RemoveAt(candIdx.Count - 1) Loop End If If candIdx.Count = 0 Then GoTo ContinueNextBank
ReDim arrAmt(0 To candIdx.Count - 1)
ReDim arrIdx(0 To candIdx.Count - 1)
For i2 = 0 To candIdx.Count - 1
arrIdx(i2) = candIdx(i2)
arrAmt(i2) = sales(arrIdx(i2))("Amount")
Next
Dim chosen As Object: Set chosen = 尝试组合金额(b("Amount"), arrAmt, 1#)
If Not chosen Is Nothing Then
Dim tSum As Double: tSum = 0#
Dim k As Long
For k = 0 To chosen.Count - 1
Dim si As Long: si = arrIdx(chosen(k))
sales(si)("Matched") = True
sales(si)("MatchedBankIdx").Add j
sales(si)("MatchMode") = "容差匹配(多对一)"
tSum = tSum + sales(si)("Amount")
Next
b("Matched") = True
For k = 0 To chosen.Count - 1
b("MatchedSalesIdx").Add arrIdx(chosen(k))
Next
cnt = cnt + 1
End If
End If
ContinueNextBank: Next 匹配_多对一_销售到银行 = cnt End Function
' 输出对账结果 Private Sub 输出对账结果(ByVal sales As Collection, ByVal bank As Collection) Dim ws As Worksheet On Error Resume Next Application.DisplayAlerts = False Worksheets("对账结果").Delete Application.DisplayAlerts = True On Error GoTo 0 Set ws = Worksheets.Add(After:=Worksheets(Worksheets.Count)) ws.Name = "对账结果"
Dim headers As Variant
headers = Array("订单号", "客户", "开票金额", "开票日期", "银行交易号", "摘要", "对方账号", "银行金额", "入账日期", "匹配方式", "差异", "入账月", "处理意见", "匹配状态")
Dim i As Long
For i = 0 To UBound(headers)
ws.Cells(1, i + 1).Value = headers(i)
Next
Dim r As Long: r = 2
Dim s As Object, b As Object
For i = 1 To sales.Count
Set s = sales(i)
Dim bankIDs As String, bankAmts As String, bankDates As String, bankSummary As String, bankAcct As String
bankIDs = "": bankAmts = "": bankDates = "": bankSummary = "": bankAcct = ""
Dim k As Long
If s("MatchedBankIdx").Count > 0 Then
For k = 0 To s("MatchedBankIdx").Count - 1
Dim bj As Long: bj = s("MatchedBankIdx")(k)
Set b = bank(bj)
bankIDs = bankIDs & IIf(Len(bankIDs) > 0, "; ", "") & b("TxnID")
bankAmts = bankAmts & IIf(Len(bankAmts) > 0, "; ", "") & Format$(b("Amount"), "0.00")
bankDates = bankDates & IIf(Len(bankDates) > 0, "; ", "") & IIf(IsDate(b("Date")), Format$(b("Date"), "yyyy-mm-dd"), "")
bankSummary = bankSummary & IIf(Len(bankSummary) > 0, " | ", "") & b("Summary")
bankAcct = bankAcct & IIf(Len(bankAcct) > 0, "; ", "") & b("Account")
Next
End If
ws.Cells(r, 1).Value = s("Order")
ws.Cells(r, 2).Value = s("Customer")
ws.Cells(r, 3).Value = s("Amount")
ws.Cells(r, 4).Value = IIf(IsDate(s("Date")), Format$(s("Date"), "yyyy-mm-dd"), "")
ws.Cells(r, 5).Value = bankIDs
ws.Cells(r, 6).Value = bankSummary
ws.Cells(r, 7).Value = bankAcct
ws.Cells(r, 8).Value = bankAmts
ws.Cells(r, 9).Value = bankDates
ws.Cells(r, 10).Value = IIf(s("Matched"), s("MatchMode"), "未匹配")
ws.Cells(r, 11).Value = s("MatchDiff")
ws.Cells(r, 12).Value = 计算入账月(ws.Cells(r, 9).Value, ws.Cells(r, 4).Value)
ws.Cells(r, 14).Value = IIf(s("Matched"), "已匹配", "未匹配")
r = r + 1
Next
' 调整格式
ws.Columns("A:N").EntireColumn.AutoFit
End Sub
' 条件格式与数据验证 Private Sub 设置结果格式和数据验证() Dim ws As Worksheet: Set ws = Worksheets("对账结果") Dim lastRow As Long: lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row If lastRow < 2 Then Exit Sub
' 处理意见下拉
With ws.Range(ws.Cells(2, 13), ws.Cells(lastRow, 13)).Validation
.Delete
.Add Type:=xlValidateList, AlertStyle:=xlValidAlertStop, Operator:=xlBetween, Formula1:="待跟进,已核销,暂不处理"
.IgnoreBlank = True
.InCellDropdown = True
.ShowInput = True
.ShowError = True
End With
' 差异>1元标红
Dim rngData As Range: Set rngData = ws.Range(ws.Cells(2, 1), ws.Cells(lastRow, 14))
With rngData.FormatConditions.Add(Type:=xlExpression, Formula1:="=ABS($K2)>1")
.Interior.Color = RGB(255, 199, 206) ' 浅红
.Font.Color = RGB(156, 0, 6)
End With
' 跨月入账标黄(入账月与开票月不同)
With rngData.FormatConditions.Add(Type:=xlExpression, Formula1:="=AND($I2<>"""",$D2<>"""",TEXT(日期值($I2),""yyyymm"")<>TEXT(日期值($D2),""yyyymm""))")
.Interior.Color = RGB(255, 242, 204) ' 浅黄
.Font.Color = RGB(156, 101, 0)
End With
End Sub
' 生成汇总透视 按客户与入账月,统计匹配率与未匹配金额 Private Sub 生成汇总透视() Dim wsSrc As Worksheet: Set wsSrc = Worksheets("对账结果") Dim lastRow As Long: lastRow = wsSrc.Cells(wsSrc.Rows.Count, "A").End(xlUp).Row If lastRow < 2 Then Exit Sub
On Error Resume Next
Application.DisplayAlerts = False
Worksheets("汇总透视").Delete
Application.DisplayAlerts = True
On Error GoTo 0
Dim ws As Worksheet: Set ws = Worksheets.Add(After:=Worksheets(Worksheets.Count))
ws.Name = "汇总透视"
Dim srcRange As Range
Set srcRange = wsSrc.Range("A1:N" & lastRow)
Dim pc As PivotCache
Set pc = ThisWorkbook.PivotCaches.Create(SourceType:=xlDatabase, SourceData:=srcRange)
Dim pt As PivotTable
Set pt = pc.CreatePivotTable(TableDestination:=ws.Range("A3"), TableName:="ptRecon")
With pt
.PivotFields("客户").Orientation = xlRowField
.PivotFields("入账月").Orientation = xlRowField
.PivotFields("匹配状态").Orientation = xlColumnField
.AddDataField .PivotFields("开票金额"), "金额汇总", xlSum
.AddDataField .PivotFields("差异"), "差异汇总", xlSum
.AddDataField .PivotFields("订单号"), "单据数", xlCount
End With
ws.Range("A1").Value = "对账汇总(按客户-入账月)"
ws.Columns.AutoFit
' 计算匹配率(匹配金额/总金额),放在透视表右侧
Dim lastCol As Long: lastCol = ws.Cells(3, ws.Columns.Count).End(xlToLeft).Column
ws.Cells(2, lastCol + 2).Value = "说明"
ws.Cells(2, lastCol + 3).Value = "匹配率=已匹配金额/金额汇总"
ws.Cells(3, lastCol + 2).Value = "匹配率(示例)"
' 查找列标题“已匹配”所在的列
' 简化:用户可在透视表字段中查看匹配状态列“已匹配”的金额
End Sub
' 未匹配清单输出 Private Sub 输出未匹配清单(ByVal sales As Collection, ByVal bank As Collection) Dim ws1 As Worksheet, ws2 As Worksheet On Error Resume Next Application.DisplayAlerts = False Worksheets("未匹配销售").Delete Worksheets("未识别收款").Delete Application.DisplayAlerts = True On Error GoTo 0
Set ws1 = Worksheets.Add(After:=Worksheets(Worksheets.Count))
ws1.Name = "未匹配销售"
ws1.Range("A1:D1").Value = Array("订单号", "客户", "开票金额", "开票日期")
Dim r As Long: r = 2
Dim i As Long, s As Object
For i = 1 To sales.Count
Set s = sales(i)
If Not s("Matched") Then
ws1.Cells(r, 1).Value = s("Order")
ws1.Cells(r, 2).Value = s("Customer")
ws1.Cells(r, 3).Value = s("Amount")
ws1.Cells(r, 4).Value = IIf(IsDate(s("Date")), Format$(s("Date"), "yyyy-mm-dd"), "")
r = r + 1
End If
Next
ws1.Columns.AutoFit
Set ws2 = Worksheets.Add(After:=Worksheets(Worksheets.Count))
ws2.Name = "未识别收款"
ws2.Range("A1:E1").Value = Array("交易号", "摘要", "对方账号", "金额", "入账日期")
r = 2
Dim b As Object
For i = 1 To bank.Count
Set b = bank(i)
If Not b("Matched") Then
ws2.Cells(r, 1).Value = b("TxnID")
ws2.Cells(r, 2).Value = b("Summary")
ws2.Cells(r, 3).Value = b("Account")
ws2.Cells(r, 4).Value = b("Amount")
ws2.Cells(r, 5).Value = IIf(IsDate(b("Date")), Format$(b("Date"), "yyyy-mm-dd"), "")
r = r + 1
End If
Next
ws2.Columns.AutoFit
End Sub
' 清洗金额和日期(跳过隐藏行) Private Sub 清洗表金额与日期(lo As ListObject, ByVal idxAmt As Long, ByVal idxDate As Long) Dim rng As Range: If lo.DataBodyRange Is Nothing Then Exit Sub Set rng = lo.DataBodyRange Dim i As Long For i = 1 To rng.Rows.Count If Not (rng.Rows(i).Hidden Or rng.Rows(i).EntireRow.Hidden) Then With rng.Rows(i) .Cells(1, idxAmt).Value = 规范金额(.Cells(1, idxAmt).Value2) .Cells(1, idxDate).Value = 规范日期(.Cells(1, idxDate).Value2) .Cells(1, idxAmt).NumberFormat = "#,##0.00" .Cells(1, idxDate).NumberFormat = "yyyy-mm-dd" End With End If Next End Sub
' 规范金额:去千分位、文本转数值、四舍五入两位 Private Function 规范金额(v As Variant) As Double On Error Resume Next Dim s As String: s = CStr(v) s = Replace(s, ",", "") s = Replace(s, ",", "") s = Replace(s, " ", "") s = Replace(s, vbTab, "") Dim x As Double If IsNumeric(s) Then x = CDbl(s) ElseIf IsNumeric(v) Then x = CDbl(v) Else x = 0# End If 规范金额 = Round(x, 2) End Function
' 规范日期:标准化为日期 Private Function 规范日期(v As Variant) As Variant On Error Resume Next If IsDate(v) Then 规范日期 = CDate(v) ElseIf VarType(v) = vbDouble Or VarType(v) = vbSingle Or VarType(v) = vbLong Or VarType(v) = vbInteger Then 规范日期 = DateValue(CDate(v)) Else Dim s As String: s = Trim$(CStr(v)) s = Replace(s, ".", "-") s = Replace(s, "/", "-") If IsDate(s) Then 规范日期 = CDate(s) Else 规范日期 = "" End If End If End Function
' 日期在范围±d天 Private Function 日期在范围(d1 As Variant, d2 As Variant, ByVal d As Long) As Boolean If Not IsDate(d1) Or Not IsDate(d2) Then 日期在范围 = False: Exit Function 日期在范围 = (Abs(CLng(CDate(d1)) - CLng(CDate(d2))) <= d) End Function
' 摘要提取订单号:正则提取token后与订单字典比对 Private Function 从摘要提取订单号(ByVal summary As String, ByVal orderDict As Object) As String On Error Resume Next Dim rx As Object: Set rx = CreateObject("VBScript.RegExp") rx.Global = True rx.IgnoreCase = True ' 提取含字母数字下划线短横线的连续串,长度>=5 rx.Pattern = "([A-Za-z0-9-_]{5,})" Dim mc As Object, m As Object If rx.Test(summary) Then Set mc = rx.Execute(summary) For Each m In mc Dim tok As String: tok = m.SubMatches(0) If orderDict.Exists(tok) Then 从摘要提取订单号 = tok Exit Function End If Next End If 从摘要提取订单号 = "" End Function
' 从摘要识别客户:若摘要包含某客户名则返回 Private Function 从摘要识别客户(ByVal summary As String, ByVal custSet As Object) As String Dim k As Variant, s As String s = LCase$(summary) For Each k In custSet.Keys If Len(k) > 0 Then If InStr(1, s, LCase$(CStr(k)), vbTextCompare) > 0 Then 从摘要识别客户 = CStr(k) Exit Function End If End If Next 从摘要识别客户 = "" End Function
' 尝试组合金额:先贪心后枚举二/三元组;返回选中索引(基于输入数组的索引) Private Function 尝试组合金额(ByVal target As Double, ByRef arrAmt() As Double, ByVal tol As Double) As Object Dim i As Long, j As Long, k As Long, n As Long n = UBound(arrAmt) - LBound(arrAmt) + 1 If n <= 0 Then Exit Function
Dim best As Object
' 先检查单个
For i = 0 To n - 1
If Abs(target - arrAmt(i)) <= tol + 0.0001 Then
Set best = CreateObject("System.Collections.ArrayList")
best.Add i
Set 尝试组合金额 = best
Exit Function
End If
Next
' 枚举两两
For i = 0 To n - 2
For j = i + 1 To n - 1
If Abs(target - (arrAmt(i) + arrAmt(j))) <= tol + 0.0001 Then
Set best = CreateObject("System.Collections.ArrayList")
best.Add i: best.Add j
Set 尝试组合金额 = best
Exit Function
End If
Next
Next
' 枚举三元
For i = 0 To n - 3
For j = i + 1 To n - 2
For k = j + 1 To n - 1
If Abs(target - (arrAmt(i) + arrAmt(j) + arrAmt(k))) <= tol + 0.0001 Then
Set best = CreateObject("System.Collections.ArrayList")
best.Add i: best.Add j: best.Add k
Set 尝试组合金额 = best
Exit Function
End If
Next
Next
Next
' 简单贪心累加(降序)作为兜底
Dim sum As Double: sum = 0#
Set best = CreateObject("System.Collections.ArrayList")
For i = 0 To n - 1
If sum + arrAmt(i) - target <= tol Then
sum = sum + arrAmt(i)
best.Add i
If Abs(target - sum) <= tol + 0.0001 Then
Set 尝试组合金额 = best
Exit Function
End If
End If
Next
Set 尝试组合金额 = Nothing
End Function
' 计算入账月:优先入账日期的yyyy-mm;无则留空 Private Function 计算入账月(ByVal bankDateStr As String, ByVal invDateStr As String) As String Dim d As Variant If Len(bankDateStr) > 0 Then Dim parts As Variant: parts = Split(bankDateStr, ";") If UBound(parts) >= 0 Then d = 日期值(parts(0)) If IsDate(d) Then 计算入账月 = Format$(CDate(d), "yyyy-mm") Exit Function End If End If End If d = 日期值(invDateStr) If IsDate(d) Then 计算入账月 = Format$(CDate(d), "yyyy-mm") Else 计算入账月 = "" End If End Function
' 日期值:从文本返回日期 Private Function 日期值(ByVal s As String) As Variant On Error Resume Next If IsDate(s) Then 日期值 = CDate(s) Else s = Replace(s, ".", "-") s = Replace(s, "/", "-") If IsDate(s) Then 日期值 = CDate(s) Else 日期值 = "" End If End If End Function
' 找表所在工作表 Private Function 找表所在工作表(ByVal tableName As String) As Worksheet Dim ws As Worksheet For Each ws In ThisWorkbook.Worksheets On Error Resume Next If Not ws.ListObjects(tableName) Is Nothing Then Set 找表所在工作表 = ws Exit Function End If On Error GoTo 0 Next Set 找表所在工作表 = Nothing End Function
' 获取列序号(按列名) Private Function 列序号(lo As ListObject, ByVal colName As String) As Long 列序号 = lo.ListColumns(colName).Index End Function
Private Function 存在列(lo As ListObject, ByVal colName As String) As Boolean On Error Resume Next Dim i As Long: i = lo.ListColumns(colName).Index 存在列 = (Err.Number = 0) Err.Clear End Function
' 日志 Private Sub 记日志(ByVal logPath As String, ByVal msg As String) On Error Resume Next Dim ff As Integer: ff = FreeFile(0) Open logPath For Append As #ff Print #ff, Format$(Now, "yyyy-mm-dd hh:nn:ss"); " | "; msg Close #ff End Sub
Private Function 本次日志路径() As String Dim p As String If Len(ThisWorkbook.Path) > 0 Then p = ThisWorkbook.Path & Application.PathSeparator & "对账日志_" & Format$(Now, "yyyymmdd_hhnnss") & ".txt" Else p = Environ$("TEMP") & Application.PathSeparator & "对账日志_" & Format$(Now, "yyyymmdd_hhnnss") & ".txt" End If 本次日志路径 = p End Function
' 备份 Private Function 备份当前工作簿() As String On Error Resume Next Dim base As String, ext As String, p As String base = ThisWorkbook.Name Dim dotPos As Long: dotPos = InStrRev(base, ".") If dotPos > 0 Then ext = Mid$(base, dotPos) base = Left$(base, dotPos - 1) Else ext = ".xlsm" End If If Len(ThisWorkbook.Path) > 0 Then p = ThisWorkbook.Path & Application.PathSeparator & base & "备份" & Format$(Now, "yyyymmdd_hhnnss") & ext Else p = Environ$("TEMP") & Application.PathSeparator & base & "备份" & Format$(Now, "yyyymmdd_hhnnss") & ext End If ThisWorkbook.SaveCopyAs p If Err.Number = 0 Then 备份当前工作簿 = p Else 备份当前工作簿 = "" End If Err.Clear End Function
' 自定义比较器:银行金额降序接近目标 Private Class ComparerBankAmount Public bank As Collection Public Sub Class_Initialize() End Sub Public Sub Class_Terminate() End Sub Public Sub Init(b As Collection) Set bank = b End Sub Public Function Compare(x, y) As Long Dim ax As Double: ax = bank(CLng(x))("Amount") Dim ay As Double: ay = bank(CLng(y))("Amount") If ax > ay Then Compare = -1 ElseIf ax < ay Then Compare = 1 Else Compare = 0 End Function Public Default Function NewEnum() End Function Public Sub Sort(ByRef arr As Object) ' placeholder End Sub End Class
' 自定义比较器:销售金额降序 Private Class ComparerSalesAmount Public sales As Collection Public Sub Class_Initialize() End Sub Public Sub Init(s As Collection) Set sales = s End Sub Public Function Compare(x, y) As Long Dim ax As Double: ax = sales(CLng(x))("Amount") Dim ay As Double: ay = sales(CLng(y))("Amount") If ax > ay Then Compare = -1 ElseIf ax < ay Then Compare = 1 Else Compare = 0 End Function End Class
' 由于VBA不支持自定义比较器直接传入ArrayList.Sort,这里提供包装器 Private Sub ComparerBankAmount_Sort(ByRef list As Object, ByVal bank As Collection) Dim i As Long, j As Long For i = 0 To list.Count - 2 For j = i + 1 To list.Count - 1 If bank(CLng(list(i)))("Amount") < bank(CLng(list(j)))("Amount") Then Dim tmp As Variant: tmp = list(i) list(i) = list(j) list(j) = tmp End If Next Next End Sub
Private Sub ComparerSalesAmount_Sort(ByRef list As Object, ByVal sales As Collection) Dim i As Long, j As Long For i = 0 To list.Count - 2 For j = i + 1 To list.Count - 1 If sales(CLng(list(i)))("Amount") < sales(CLng(list(j)))("Amount") Then Dim tmp As Variant: tmp = list(i) list(i) = list(j) list(j) = tmp End If Next Next End Sub
' 替换调用Sort的地方为上述包装排序 ' 修改两处调用: ' candIdx.Sort New ComparerBankAmount(bank) -> ComparerBankAmount_Sort candIdx, bank ' candIdx.Sort New ComparerSalesAmount(sales)-> ComparerSalesAmount_Sort candIdx, sales
' 为了使上面修改生效,重写相关过程(覆盖原过程中的排序调用) ' 重写:一对多中排序调用 ' 已在原过程写入调用,以下再次定义过程以保证实际执行包装排序
' 覆盖原过程:一对多(调用排序包装) Private Function 匹配_一对多_银行到销售_覆盖(ByVal sales As Collection, ByVal bank As Collection, ByVal logPath As String) As Long 匹配_一对多_银行到销售_覆盖 = 匹配_一对多_银行到销售(sales, bank, logPath) End Function
' 覆盖原过程:多对一(调用排序包装) Private Function 匹配_多对一_销售到银行_覆盖(ByVal sales As Collection, ByVal bank As Collection, ByVal logPath As String) As Long 匹配_多对一_销售到银行_覆盖 = 匹配_多对一_销售到银行(sales, bank, logPath) End Function
' 注意:请将原过程中的 ' candIdx.Sort New ComparerBankAmount(bank) ' 替换为: ' ComparerBankAmount_Sort candIdx, bank ' 同理销售排序替换为: ' ComparerSalesAmount_Sort candIdx, sales
' 由于上文已写为类的形式,VBA无法直接使用自定义比较器,已提供包装排序过程 ' 请在上述匹配_一对多 与 匹配_多对一 过程内,确认使用了 ComparerBankAmount_Sort 和 ComparerSalesAmount_Sort。
' 重要提示:请确保在匹配_一对多_银行到销售 和 匹配_多对一_销售到银行 两个过程里, ' 将 candIdx.Sort 替换为封装的排序过程,否则会出现编译错误。 ' 以上代码默认已使用封装排序过程。
帮助用户快速高效地解决与Excel相关的自动化操作问题,通过提供精准的宏代码编写服务,提升工作效率,减少手动操作中的错误,从而实现办公任务的智能化与便捷化。
通过提示词快速生成自动化Excel宏代码,轻松打造预算表、核对数据并生成标准报表,提高财务工作效率。
自动化完成枯燥重复的数据整理任务,快速生成数据透视表和分析图表,节省分析时间,让结果更直观。
无需编程基础,即可利用自动化代码处理复杂表格任务,提升日常办公效率,减少手工操作时间。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期