简介:这是一套基于Excel VBA开发的轻量级进销存管理系统,面向中小企业管理者、财务人员及VBA初学者,解决日常进货、销售、库存等核心业务的手工记录低效与数据易错问题。资源包共3个文件(85KB),含主程序文件进销存管理.xlsm(启用宏的Excel工作簿,承载全部VBA逻辑与交互界面)、customUI.xml(自定义功能区配置,优化操作入口)及.rels关系文件(保障宏功能正常加载),结构精简、开箱即用。已有3035人学习下载,体现了其在低成本信息化实践中的广泛认可。用户可直接运行xlsm文件,体验进货单录入、销售出库、库存实时更新、多条件查询及阈值预警等完整闭环功能;代码模块清晰分层,涵盖事件驱动响应、Range/Worksheet对象操作、自定义对话框设计等典型VBA实战技术,是理解业务系统与Excel自动化结合的优质学习范例。
1. 为什么还在用 Excel 做进销存?不是图省事,而是它真能扛住小团队三年不换系统
你可能刚被老板甩来一个需求:“下周起所有出入库单子走线上,别再手写贴纸条了。”——结果发现隔壁组用的“进销存系统”是 Excel 文件,双击打开,点几下按钮,库存自动扣减、成本自动结转、月报一键生成。没有服务器、不装客户端、不配IT人员,连U盘拷走就能在客户现场演示。这不是玄学,是 Excel VBA 在中小商贸、五金配件、文具批发这类场景里真实存在的生存逻辑:单机可部署、逻辑全透明、改起来比改Word样式还快。它不解决千万级SKU并发下单,但能稳稳托住年流水300万以内、员工5~15人的业务流。本篇不讲“Excel多慢多low”,只拆解一个真实可运行的VBA进销存骨架:从商品主档维护、采购入库、销售出库到库存台账与毛利统计,全部用原生Excel+VBA实现,零外部依赖。你会看到代码怎么把“点击按钮→弹窗输入→校验规则→写入指定工作表→刷新汇总”的链路闭环,也会踩到那些只有亲手敲过2000行VBA才懂的坑——比如日期格式错位导致库存清零、合并单元格让Range.Find失效、用户窗体控件事件丢失……这些不是文档里的警告,是某开发者在凌晨两点对着空白报表按F8单步调试出来的血泪经验。
2. 搭建核心数据结构:用工作表模拟数据库表,VBA只读写不建表
Excel做进销存的第一道硬门槛,不是写代码,而是设计“表结构”。VBA不提供CREATE TABLE语法,所有数据都压在工作表里,但必须像数据库一样有主键、外键、约束和索引意识。我们用4张工作表构成最小可行骨架:商品主档(商品编码、名称、单位、进价、售价、启用状态)、采购单(单号、日期、供应商、明细行)、销售单(单号、日期、客户、明细行)、库存台账(日期、单号、商品编码、数量、类型【入库/出库】、结存)。注意:所有明细行必须扁平化存储——采购单明细不能塞在采购单工作表的合并单元格里,而要单独放在采购单明细工作表,用采购单号作为外键关联。这是VBA能稳定读写的唯一方式。下面这段代码初始化商品主档表头并设置基础校验:
Sub InitProductSheet() Dim ws As Worksheet Set ws = ThisWorkbook.Worksheets("商品主档") ' 清空旧数据(保留表头) If ws.UsedRange.Rows.Count > 1 Then ws.Range("A2:" & ws.Cells(ws.Rows.Count, "A").End(xlUp).Address).ClearContents End If ' 写入表头(固定列顺序,VBA后续全靠列号定位) ws.Range("A1:G1").Value = Array("商品编码", "商品名称", "单位", "最新进价", "最新售价", "启用状态", "备注") ' 设置数据验证:启用状态只能是"是"或"否" With ws.Range("F2:F1000").Validation .Delete .Add Type:=xlValidateList, AlertStyle:=xlValidAlertStop, _ Formula1:="是,否" .IgnoreBlank = True .InCellDropdown = True End With ' 设置商品编码列格式为文本(防0开头编码被转成数字) ws.Columns("A").NumberFormat = "@" End Sub提示:VBA中
Columns("A").NumberFormat = "@"这行是后悔药。曾有项目因商品编码“00123”被Excel自动转成123,导致采购单匹配不到商品,整月库存账对不上。文本格式必须在写入任何数据前设置,且对整列生效——局部设置会失效。
2.1 商品编码生成规则:不用GUID,用可读+可扩展的自增码
商品编码不是随便填的字符串,它是整个系统检索、汇总、打印单据的锚点。我们采用类别码-年份后两位-三位序号格式,例如HW-24-001(HW=五金,24=2024年,001=当年第一款)。VBA自动生成逻辑如下:
Function GenerateProductCode(category As String) As String Dim ws As Worksheet, lastRow As Long, maxSeq As Long, yearPart As String Set ws = ThisWorkbook.Worksheets("商品主档") yearPart = Right(Year(Date), 2) ' 取当前年份后两位 ' 查找该类别+该年份的最大序号 lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row maxSeq = 0 Dim i As Long For i = 2 To lastRow If ws.Cells(i, 1).Value Like category & "-" & yearPart & "-###" Then Dim seqStr As String seqStr = Mid(ws.Cells(i, 1).Value, Len(category) + 4, 3) If IsNumeric(seqStr) Then If CLng(seqStr) > maxSeq Then maxSeq = CLng(seqStr) End If End If Next i GenerateProductCode = category & "-" & yearPart & "-" & Format(maxSeq + 1, "000") End Function逻辑说明:
Like操作符用于模糊匹配,category & "-" & yearPart & "-###"表示“类别-年份-三位数字”的模式;Mid(..., Len(category)+4, 3)精准截取序号段,避免HW-24-1001被误判为100;Format(maxSeq + 1, "000")确保补零,输出001而非1。
参数说明:category必须是2~3位大写字母(如"HW"、"WJ"),硬编码在用户窗体中下拉选择,杜绝手工输入错误。
2.2 采购单明细自动关联商品信息:用VLOOKUP替代循环查找
当用户在采购单明细行输入商品编码,右侧商品名称、单位、进价应自动带出。新手常写For循环遍历商品主档,但1000行商品时卡顿明显。正确做法是:用VBA调用Excel内置函数VLOOKUP,速度提升10倍以上:
Sub FillProductInfoByCode(targetCell As Range) Dim wsProd As Worksheet, prodCode As String Set wsProd = ThisWorkbook.Worksheets("商品主档") prodCode = Trim(targetCell.Value) If prodCode = "" Then Exit Sub ' 用Application.VLookup替代循环,返回数组[名称,单位,进价] Dim result As Variant result = Application.VLookup(prodCode, wsProd.Range("A1:G" & wsProd.Cells(wsProd.Rows.Count, "A").End(xlUp).Row), Array(2, 3, 4), False) If IsError(result(1)) Then MsgBox "未找到商品编码:" & prodCode & ",请检查是否已录入主档", vbExclamation targetCell.ClearContents Exit Sub End If ' 向右填充:B列名称、C列单位、D列进价 targetCell.Offset(0, 1).Value = result(1) ' 名称 targetCell.Offset(0, 2).Value = result(2) ' 单位 targetCell.Offset(0, 3).Value = result(3) ' 进价 End Sub关键点:
Array(2,3,4)让VLOOKUP一次返回三列值,避免三次调用;IsError(result(1))判断首列是否查到,比IsError(result)更准(后者在result为单值时行为异常);- 此函数绑定在采购单明细表的
Worksheet_Change事件中,仅监控商品编码列(A列),避免全表监听拖慢响应。
3. 单据流转引擎:采购入库与销售出库的事务性处理
进销存的核心不是存数据,而是保证“一笔采购入库,库存+1;一笔销售出库,库存-1”这个因果链不中断。VBA没有数据库事务,我们必须用代码模拟ACID:原子性(All or Nothing)、一致性(库存不为负)、隔离性(多人同时操作不冲突)、持久性(断电不丢数据)。这里以采购入库为例,拆解四步闭环:
3.1 采购单审核:校验必填项、库存预警、金额平衡
用户点击“审核采购单”按钮时,VBA需完成三重校验:
- 字段完整性:单号、日期、供应商、至少一行明细;
- 业务合理性:所有商品启用状态为“是”,数量>0,单价>0;
- 财务平衡性:采购总金额 = Σ(数量×单价),与单据底部合计栏比对。
Sub AuditPurchaseOrder(orderNo As String) Dim wsMain As Worksheet, wsDetail As Worksheet Set wsMain = ThisWorkbook.Worksheets("采购单") Set wsDetail = ThisWorkbook.Worksheets("采购单明细") ' 步骤1:定位该单据在采购单主表的位置 Dim mainRow As Long mainRow = Application.Match(orderNo, wsMain.Columns(1), 0) If IsError(mainRow) Then MsgBox "采购单号不存在:" & orderNo, vbCritical Exit Sub End If ' 步骤2:检查明细是否存在(用COUNTIF查单号出现次数) Dim detailCount As Long detailCount = Application.CountIf(wsDetail.Columns(1), orderNo) If detailCount = 0 Then MsgBox "单据" & orderNo & "无明细行,无法审核", vbExclamation Exit Sub End If ' 步骤3:逐行校验明细(关键!) Dim i As Long, prodCode As String, qty As Double, price As Double Dim totalAmount As Double: totalAmount = 0 For i = 2 To wsDetail.Cells(wsDetail.Rows.Count, "A").End(xlUp).Row If wsDetail.Cells(i, 1).Value = orderNo Then prodCode = Trim(wsDetail.Cells(i, 2).Value) qty = wsDetail.Cells(i, 3).Value price = wsDetail.Cells(i, 4).Value ' 校验商品存在且启用 If Application.CountIf(ThisWorkbook.Worksheets("商品主档").Columns(1), prodCode) = 0 Then MsgBox "明细第" & i & "行:商品编码" & prodCode & "未在主档中", vbCritical Exit Sub End If If ThisWorkbook.Worksheets("商品主档").Range("F" & Application.Match(prodCode, ThisWorkbook.Worksheets("商品主档").Columns(1), 0)).Value <> "是" Then MsgBox "明细第" & i & "行:商品" & prodCode & "已停用", vbExclamation Exit Sub End If ' 校验数量价格 If qty <= 0 Or price < 0 Then MsgBox "明细第" & i & "行:数量必须>0,单价不能为负", vbExclamation Exit Sub End If totalAmount = totalAmount + qty * price End If Next i ' 步骤4:比对主表合计金额 Dim expectedTotal As Double expectedTotal = wsMain.Cells(mainRow, 5).Value ' 假设E列为合计金额 If Abs(totalAmount - expectedTotal) > 0.01 Then ' 浮点误差容限 MsgBox "金额校验失败:明细计算总额" & Format(totalAmount, "0.00") & " ≠ 主表填写总额" & Format(expectedTotal, "0.00"), vbCritical Exit Sub End If ' 校验通过,更新状态为"已审核" wsMain.Cells(mainRow, 6).Value = "已审核" ' F列为状态列 MsgBox "采购单" & orderNo & "审核成功!", vbInformation End Sub3.2 入库执行:原子性写入库存台账与更新商品进价
审核通过后,点击“执行入库”才真正修改库存。此操作必须一步到位:
- 向
库存台账追加N行记录(每行对应一个商品); - 更新
商品主档中对应商品的“最新进价”(取本次采购单价); - 刷新所有汇总看板(库存余额、周转率等)。
Sub ExecutePurchaseInbound(orderNo As String) Dim wsDetail As Worksheet, wsStock As Worksheet, wsProd As Worksheet Set wsDetail = ThisWorkbook.Worksheets("采购单明细") Set wsStock = ThisWorkbook.Worksheets("库存台账") Set wsProd = ThisWorkbook.Worksheets("商品主档") ' 找到库存台账最后一行,准备追加 Dim nextStockRow As Long nextStockRow = wsStock.Cells(wsStock.Rows.Count, "A").End(xlUp).Row + 1 ' 遍历该单据所有明细 Dim i As Long, prodCode As String, qty As Double, price As Double For i = 2 To wsDetail.Cells(wsDetail.Rows.Count, "A").End(xlUp).Row If wsDetail.Cells(i, 1).Value = orderNo Then prodCode = Trim(wsDetail.Cells(i, 2).Value) qty = wsDetail.Cells(i, 3).Value price = wsDetail.Cells(i, 4).Value ' 写入库存台账:日期、单号、商品编码、数量、类型、结存(此处先写0,后续统算) wsStock.Cells(nextStockRow, 1).Value = Date wsStock.Cells(nextStockRow, 2).Value = orderNo wsStock.Cells(nextStockRow, 3).Value = prodCode wsStock.Cells(nextStockRow, 4).Value = qty wsStock.Cells(nextStockRow, 5).Value = "入库" wsStock.Cells(nextStockRow, 6).Value = 0 ' 结存暂置0 ' 更新商品主档最新进价(仅当本次单价更高时更新,符合会计谨慎性) Dim prodRow As Long prodRow = Application.Match(prodCode, wsProd.Columns(1), 0) If price > wsProd.Cells(prodRow, 4).Value Then wsProd.Cells(prodRow, 4).Value = price End If nextStockRow = nextStockRow + 1 End If Next i ' 关键:调用库存结存重算函数(见4.1节) RecalculateStockBalances MsgBox "采购单" & orderNo & "入库完成,共更新" & (nextStockRow - 1 - wsStock.Cells(wsStock.Rows.Count, "A").End(xlUp).Row) & "个商品库存", vbInformation End Sub注意:
RecalculateStockBalances是独立函数,不在本节展开,但必须在此处调用——否则库存台账里全是0,用户看到的是“假入库”。
4. 库存结存黑匣子:用VBA重算每一行的实时结存,拒绝SUMIFS翻车
Excel自带的SUMIFS函数在进销存场景是定时炸弹:当库存台账有10万行,每次打开都要重算所有SUMIFS,卡顿到怀疑人生;更致命的是,它无法处理“同一商品多笔出入库交叉时的时序依赖”——比如上午入库100个,下午出库80个,晚上又入库50个,SUMIFS只认静态区间,算不出每行的动态结存。VBA必须接管这个黑匣子,用单次遍历+哈希缓存实现O(n)复杂度重算:
4.1 动态结存重算函数:按商品分组,时序累加
Sub RecalculateStockBalances() Dim ws As Worksheet, lastRow As Long, i As Long Set ws = ThisWorkbook.Worksheets("库存台账") lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row If lastRow < 2 Then Exit Sub ' 清空旧结存列(F列) ws.Range("F2:F" & lastRow).ClearContents ' 用字典缓存每个商品的当前结存(避免重复查表) Dim stockDict As Object Set stockDict = CreateObject("Scripting.Dictionary") ' 第一次遍历:收集所有商品初始结存(从商品主档读取期初数,此处简化为0) ' 实际项目中,此处应读取"期初库存"工作表或商品主档的"期初数量"列 Dim prodCode As String For i = 2 To lastRow prodCode = Trim(ws.Cells(i, 3).Value) If Not stockDict.Exists(prodCode) Then stockDict(prodCode) = 0 ' 默认期初为0 End If Next i ' 第二次遍历:按时间顺序(A列日期)逐行更新结存 ' 先排序:按日期升序,同日期按行号升序(保证时序) ws.Sort.SortFields.Clear ws.Sort.SortFields.Add Key:=ws.Range("A2:A" & lastRow), SortOn:=xlSortOnValues, Order:=xlAscending ws.Sort.SortFields.Add Key:=ws.Range("A2:A" & lastRow), SortOn:=xlSortOnValues, Order:=xlAscending ws.Sort.SetRange ws.Range("A1:F" & lastRow) ws.Sort.Header = xlYes ws.Sort.Apply ' 重新获取排序后的lastRow(可能变动) lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row ' 逐行计算 For i = 2 To lastRow prodCode = Trim(ws.Cells(i, 3).Value) Dim qty As Double: qty = ws.Cells(i, 4).Value Dim typeStr As String: typeStr = Trim(ws.Cells(i, 5).Value) If typeStr = "入库" Then stockDict(prodCode) = stockDict(prodCode) + qty ElseIf typeStr = "出库" Then stockDict(prodCode) = stockDict(prodCode) - qty ' 强制校验:结存不能为负(业务规则) If stockDict(prodCode) < 0 Then MsgBox "库存预警:商品" & prodCode & "结存将为负值(" & stockDict(prodCode) & "),请检查单据", vbExclamation ' 此处可选择停止计算、标记错误行、或强制归零 stockDict(prodCode) = 0 End If End If ws.Cells(i, 6).Value = stockDict(prodCode) ' 写入F列结存 Next i End Sub逻辑说明:
- 用
Scripting.Dictionary缓存商品结存,查询O(1),比Application.VLookup快一个数量级; - 排序是必须步骤:库存结存本质是时间序列累加,不按日期排就乱套;
MsgBox预警是业务安全阀,实际项目中可改为写入日志工作表或触发邮件通知。
4.2 销售出库的特殊处理:成本结转与毛利计算
销售出库不仅要扣减库存,还要结转销售成本(先进先出FIFO),并计算毛利。VBA无法像ERP那样维护批次,我们采用加权平均法简化:
- 当前商品加权平均成本 = (期初金额 + 本期入库金额)/(期初数量 + 本期入库数量);
- 本次销售成本 = 销售数量 × 加权平均成本;
- 毛利 = 销售收入 - 销售成本。
Sub ExecuteSalesOutbound(orderNo As String) Dim wsDetail As Worksheet, wsStock As Worksheet, wsProd As Worksheet Set wsDetail = ThisWorkbook.Worksheets("销售单明细") Set wsStock = ThisWorkbook.Worksheets("库存台账") Set wsProd = ThisWorkbook.Worksheets("商品主档") ' 步骤1:计算每个商品的加权平均成本(需遍历历史台账) Dim costDict As Object, qtyDict As Object, amountDict As Object Set costDict = CreateObject("Scripting.Dictionary") Set qtyDict = CreateObject("Scripting.Dictionary") Set amountDict = CreateObject("Scripting.Dictionary") ' 初始化:从商品主档读取期初(此处简化为期初数量=0,期初金额=0) Dim prodCode As String, i As Long For i = 2 To wsProd.Cells(wsProd.Rows.Count, "A").End(xlUp).Row prodCode = Trim(wsProd.Cells(i, 1).Value) If Not qtyDict.Exists(prodCode) Then qtyDict(prodCode) = 0 amountDict(prodCode) = 0 End If Next i ' 遍历库存台账,累计入库数量与金额(只统计"入库"类型) Dim lastStockRow As Long lastStockRow = wsStock.Cells(wsStock.Rows.Count, "A").End(xlUp).Row For i = 2 To lastStockRow If wsStock.Cells(i, 5).Value = "入库" Then prodCode = Trim(wsStock.Cells(i, 3).Value) Dim qty As Double: qty = wsStock.Cells(i, 4).Value Dim price As Double: price = GetLatestPurchasePrice(prodCode) ' 自定义函数,查最近一笔采购单价 If Not qtyDict.Exists(prodCode) Then qtyDict(prodCode) = 0 If Not amountDict.Exists(prodCode) Then amountDict(prodCode) = 0 qtyDict(prodCode) = qtyDict(prodCode) + qty amountDict(prodCode) = amountDict(prodCode) + qty * price End If Next i ' 计算加权平均成本并缓存 For Each prodCode In qtyDict.Keys If qtyDict(prodCode) > 0 Then costDict(prodCode) = amountDict(prodCode) / qtyDict(prodCode) Else costDict(prodCode) = 0 End If Next prodCode ' 步骤2:执行出库(写台账)并计算毛利 Dim nextStockRow As Long nextStockRow = wsStock.Cells(wsStock.Rows.Count, "A").End(xlUp).Row + 1 Dim totalGrossProfit As Double: totalGrossProfit = 0 For i = 2 To wsDetail.Cells(wsDetail.Rows.Count, "A").End(xlUp).Row If wsDetail.Cells(i, 1).Value = orderNo Then prodCode = Trim(wsDetail.Cells(i, 2).Value) Dim salesQty As Double: salesQty = wsDetail.Cells(i, 3).Value Dim salesPrice As Double: salesPrice = wsDetail.Cells(i, 4).Value Dim costPrice As Double: costPrice = costDict(prodCode) ' 写出库台账 wsStock.Cells(nextStockRow, 1).Value = Date wsStock.Cells(nextStockRow, 2).Value = orderNo wsStock.Cells(nextStockRow, 3).Value = prodCode wsStock.Cells(nextStockRow, 4).Value = salesQty wsStock.Cells(nextStockRow, 5).Value = "出库" wsStock.Cells(nextStockRow, 6).Value = 0 ' 结存待重算 ' 计算并记录毛利(写入G列) Dim grossProfit As Double grossProfit = salesQty * (salesPrice - costPrice) wsStock.Cells(nextStockRow, 7).Value = grossProfit ' G列为毛利 totalGrossProfit = totalGrossProfit + grossProfit nextStockRow = nextStockRow + 1 End If Next i ' 步骤3:重算结存(调用4.1节函数) RecalculateStockBalances MsgBox "销售单" & orderNo & "出库完成,总毛利:" & Format(totalGrossProfit, "0.00") & "元", vbInformation End Sub5. 避坑指南:VBA进销存开发中90%的人踩过的5个深坑
VBA进销存看似简单,但每个坑都足以让系统上线后天天救火。以下是某开发者在三个不同行业项目中踩出的血泪经验,按发生频率排序:
5.1 现象:库存台账结存列全是0,手动点“计算”也没用
原因:RecalculateStockBalances函数中,ws.Sort.Apply执行后,工作表处于“排序中”状态,后续ws.Cells(i,6).Value=写入操作被Excel拦截,但VBA不报错。
解决:在排序代码后立即添加ws.Calculate强制重算,或改用Range.Sort方法(不触发全表重算):
' 替换原排序代码为: Dim sortRange As Range Set sortRange = ws.Range("A1:F" & lastRow) sortRange.Sort Key1:=sortRange.Columns(1), Order1:=xlAscending, Header:=xlYes5.2 现象:用户窗体里下拉框选完商品,点确定后商品编码消失
原因:窗体控件ComboBox的ListIndex属性在用户点击后未及时读取,或TextBox的Exit事件中清空了值。
解决:所有窗体交互必须用AfterUpdate事件(非Change),并在按钮点击时用Me.ComboBox1.Value读值:
Private Sub cmdOK_Click() Dim selectedCode As String selectedCode = Me.cboProduct.Value ' 用.Value,不用.Text或.ListIndex If selectedCode = "" Then Exit Sub ' 后续逻辑... End Sub5.3 现象:采购单审核时提示“未找到商品”,但商品主档明明有
原因:商品编码列设置了“文本”格式,但用户粘贴时Excel自动转成“常规”格式,导致VLOOKUP查不到(文本"001"≠数字1)。
解决:在商品主档初始化时,不仅设NumberFormat = "@",还要用TextToColumns强制转文本:
ws.Columns("A").TextToColumns Destination:=ws.Columns("A"), DataType:=xlDelimited, TextQualifier:=xlDoubleQuote, ConsecutiveDelimiter:=False, Tab:=True, Semicolon:=False, Comma:=False, Space:=False, Other:=False5.4 现象:多人同时操作同一Excel文件,库存数据错乱
原因:Excel共享工作簿功能已废弃,VBA无锁机制,A用户读库存100,B用户也读100,A扣80剩20,B扣50剩50,最终库存变成50而非20。
解决:放弃多人实时编辑,改用“单机版+定期合并”模式:每天下班前导出当日单据XML,晨会时由主管合并到主文件;或用OneDrive/SharePoint的“文件锁定”特性(需Office 365订阅)。
5.5 现象:打印采购单时,明细行跨页断裂,抬头重复打印
原因:Excel默认打印设置不识别“明细表”结构,标题行未设置“顶端标题行”。
解决:在打印前用VBA设置:
With ActiveSheet.PageSetup .PrintTitleRows = "$1:$1" ' 仅第一行作标题 .PrintArea = "A1:G" & lastDetailRow ' 明确打印区域 .Zoom = False .FitToPagesWide = 1 .FitToPagesTall = False End With ActiveSheet.PrintOut6. 进阶技巧:用VBA自动生成月度毛利分析看板,告别手工粘贴
系统跑三个月后,老板要的不再是“库存还有多少”,而是“哪个品类毛利最高”“哪些客户贡献最大”。此时手工用数据透视表太慢,VBA可以一键生成动态看板。核心思路:用字典聚合数据,用ChartObjects插入图表,用Shapes添加交互按钮。以下代码生成“Top10商品毛利榜”:
Sub GenerateMonthlyProfitDashboard() Dim wsStock As Worksheet, wsReport As Worksheet Set wsStock = ThisWorkbook.Worksheets("库存台账") Set wsReport = ThisWorkbook.Worksheets("毛利分析") ' 清空报告页 wsReport.Cells.Clear ' 步骤1:按商品聚合毛利(G列),只取当月数据 Dim profitDict As Object, dateStart As Date, dateEnd As Date Set profitDict = CreateObject("Scripting.Dictionary") dateStart = DateSerial(Year(Date), Month(Date), 1) dateEnd = DateSerial(Year(Date), Month(Date) + 1, 0) Dim i As Long, lastRow As Long, prodCode As String, profit As Double lastRow = wsStock.Cells(wsStock.Rows.Count, "A").End(xlUp).Row For i = 2 To lastRow If IsDate(wsStock.Cells(i, 1).Value) Then If wsStock.Cells(i, 1).Value >= dateStart And wsStock.Cells(i, 1).Value <= dateEnd Then If wsStock.Cells(i, 5).Value = "出库" And Not IsEmpty(wsStock.Cells(i, 7).Value) Then prodCode = Trim(wsStock.Cells(i, 3).Value) profit = wsStock.Cells(i, 7).Value If Not profitDict.Exists(prodCode) Then profitDict(prodCode) = 0 End If profitDict(prodCode) = profitDict(prodCode) + profit End If End If End If Next i ' 步骤2:按毛利降序排列,取Top10 Dim keys() As Variant, values() As Variant keys = profitDict.Keys values = profitDict.Items ' 简单冒泡排序(数据量小,够用) Dim j As Long, tempKey As Variant, tempVal As Double For i = LBound(keys) To UBound(keys) - 1 For j = i + 1 To UBound(keys) If values(i) < values(j) Then tempKey = keys(i): keys(i) = keys(j): keys(j) = tempKey tempVal = values(i): values(i) = values(j): values(j) = tempVal End If Next j Next i ' 步骤3:写入报告页(A1:B11) wsReport.Range("A1:B1").Value = Array("商品编码", "毛利(元)") Dim topN As Long: topN = Application.Min(10, profitDict.Count) For i = 0 To topN - 1 wsReport.Cells(i + 2, 1).Value = keys(i) wsReport.Cells(i + 2, 2).Value = values(i) Next i ' 步骤4:插入柱状图 Dim cht As ChartObject Set cht = wsReport.ChartObjects.Add(Left:=100, Width:=500, Top:=200, Height:=300) With cht.Chart .SetSourceData Source:=wsReport.Range("A1:B" & topN + 1) .ChartType = xlColumnClustered .HasTitle = True .ChartTitle.Text = "本月Top" & topN & "商品毛利榜" .Axes(xlCategory, xlPrimary).HasTitle = True .Axes(xlCategory, xlPrimary).AxisTitle.Text = "商品编码" .Axes(xlValue, xlPrimary).HasTitle = True .Axes(xlValue, xlPrimary).AxisTitle.Text = "毛利(元)" End With ' 步骤5:添加刷新按钮 With wsReport.Shapes.AddFormControl(xlButtonControl, 100, 520, 100, 30) .Name = "btnRefreshProfit" .OnAction = "GenerateMonthlyProfitDashboard" .TextFrame.Characters.Text = "刷新本月毛利" End With End Sub关键细节:
DateSerial(Year(Date), Month(Date), 1)精确生成当月1日,比"1/" & Month(Date) & "/" & Year(Date)更可靠;- 图表
SetSourceData必须用Range对象,不能用字符串地址(如"A1:B11"会报错); - 按钮
.OnAction指向宏名,必须是公有Sub且无参数,否则点击无效。
我一般会在系统交付时,把这份看板代码封装进“管理报表”菜单,再教客户主管按Ctrl+Shift+P一键生成。他们不再需要打开VBA编辑器,也不用担心改坏代码——因为所有逻辑都在后台,前台只留按钮和图表。这种“藏技术于无形”的交付,才是VBA进销存真正的价值:它不炫技,但让业务员每天少花20分钟整理数据,三年下来就是400小时,够他们多跑30个新客户。希望帮到你。
本文还有配套的精品资源,点击获取