简介:这份进销存VB.NET源码面向中小企业部署与VB.NET桌面开发学习者,提供采购、销售、库存等核心业务流程的完整实现,可直接运行参考,也可作为二次开发基础。压缩包共433个文件、约14.2MB,以174个vb源文件与135个resx资源文件为主体,另有35个rpt水晶报表模板、22个mod模块文件及xsd数据集定义,并附带xpMenu、fxyPrintModel等dll组件与可执行程序,覆盖界面、业务逻辑与数据交互各层。已有633人学习下载。源码中XP风格菜单、水晶报表实例与自定义套打实例尤为实用,可据此生成销售、采购、库存报表并定制收据发票打印模板,在线升级与启动提示也提供了完整工程思路,适合对照学习WinForm项目结构与报表集成方式。
1. 进销存 VB.NET 源码:一套能跑通采购到报表的桌面系统拆解
手上这套进销存 VB.NET 源码,第一次打开时我并没有急着点“运行”,而是先看目录结构——采购、销售、库存、往来账、报表五个模块分得清清楚楚,水晶报表的 rpt 文件和自定义套打的绘制代码单独放在 Report 目录下。这恰恰是很多同类源码包最容易糊弄的地方:业务逻辑能跑,报表一打就歪。这套东西的价值不在于它有多“企业级”,而在于它把进销存最核心的三条链路——单据流转、库存扣减、套打输出——用 VB.NET 桌面技术栈完整串了一遍,适合需要快速搭一套内部台账、或者想研究水晶报表套打怎么落地的从业者。下面按我实际拆包的顺序讲,从环境到单据,再到报表和踩坑。
2. 环境与数据库落地:从源码包到能登录的第一屏
2.1 技术栈判断与运行环境准备
拿到源码先别装,先确认它吃什么环境。这套是 VB.NET WinForms 项目,解决方案文件是 .sln,项目文件是 .vbproj,数据库走的是 SQL Server(从连接字符串和建库脚本能看出来)。常见做法是装 Visual Studio 2019 或 2022,勾选“.NET 桌面开发”工作负载,.NET Framework 版本按项目属性里的目标框架对齐——老项目多半是 4.5 到 4.8 之间,别硬升到 .NET 6 以上,WinForms 设计器和 Crystal Reports 的 COM 引用会直接翻车。
数据库这边,先找包里的 .sql 脚本,一般在 DB 或 Script 目录。用 SSMS 连本地实例,新建一个库,把脚本按顺序跑完。注意脚本里如果有USE [xxx]语句,库名要和你建的保持一致,否则后面连接字符串对不上,登录界面点下去就是一句“未将对象引用设置到对象的实例”,这种报错最没营养,纯属环境没对齐。
连接字符串通常藏在 App.config 或某个模块的常量里,形如:
<connectionStrings> <add name="ConnStr" connectionString="Data Source=.;Initial Catalog=JXC;User ID=sa;Password=你的密码" providerName="System.Data.SqlClient" /> </connectionStrings>Data Source=.表示本机默认实例,如果是命名实例就写.\SQLEXPRESS;Initial Catalog必须和建库名一致。改完记得重新生成,别只改文件不编译。
2.2 首次登录与基础数据初始化
环境通了之后,第一件事不是急着录单,而是把基础数据铺好。进销存的地基是四张表:商品档案、客户/供应商档案、仓库档案、计量单位。源码里一般有对应的维护窗体,菜单路径类似“基础资料 → 商品管理”。这里有个顺序问题:先建仓库和单位,再建商品,因为商品表通常外键引用单位,仓库又要被单据引用。顺序反了,保存时报外键约束错误,新手容易以为是代码 bug。
初始化时建议只录两三条测试数据,别一上来就导几百条。用最小数据集跑通一张采购入库单,确认库存数变了、往来账有记录,再批量导入。批量导入常见做法是用 SQL Server 的导入向导或者写个临时脚本,但要注意商品编码的唯一约束,重复编码会整批失败。
提示:建库脚本跑完后,先备份一份空库。后面测试单据把数据搞乱了,直接还原比一条条删快得多。
3. 单据流转与库存扣减:采购、销售、库存三张表的联动逻辑
3.1 单据主从表结构与事务边界
进销存的骨架就是单据。这套源码里,采购入库、销售出库、库存调拨基本都是“主表 + 明细表”结构:主表存单号、日期、往来单位、合计金额,明细表存商品、数量、单价、金额,靠主表主键关联。看代码时重点找保存按钮背后的那段逻辑,通常是一个事务包住三步:写主表拿回自增 ID、循环写明细、更新库存。
Using tran As SqlTransaction = conn.BeginTransaction() Try ' 1. 插入主表,SCOPE_IDENTITY 拿回单号 Dim sqlMain As String = "INSERT INTO PurchaseMain(BillNo, BillDate, SupplierID, TotalAmt) " & "VALUES(@no, @date, @sup, @amt); SELECT SCOPE_IDENTITY();" Dim cmdMain As New SqlCommand(sqlMain, conn, tran) cmdMain.Parameters.AddWithValue("@no", txtBillNo.Text) cmdMain.Parameters.AddWithValue("@date", dtpBillDate.Value) cmdMain.Parameters.AddWithValue("@sup", cboSupplier.SelectedValue) cmdMain.Parameters.AddWithValue("@amt", decTotal) Dim mainID As Integer = Convert.ToInt32(cmdMain.ExecuteScalar()) ' 2. 循环写明细,同时累加库存 For Each row As DataGridViewRow In dgvDetail.Rows Dim sqlDetail As String = "INSERT INTO PurchaseDetail(MainID, ProductID, Qty, Price) " & "VALUES(@mid, @pid, @qty, @price)" Dim cmdDetail As New SqlCommand(sqlDetail, conn, tran) cmdDetail.Parameters.AddWithValue("@mid", mainID) cmdDetail.Parameters.AddWithValue("@pid", row.Cells("ProductID").Value) cmdDetail.Parameters.AddWithValue("@qty", row.Cells("Qty").Value) cmdDetail.Parameters.AddWithValue("@price", row.Cells("Price").Value) cmdDetail.ExecuteNonQuery() ' 3. 库存表按商品+仓库累加 Dim sqlStock As String = "UPDATE Stock SET Qty = Qty + @qty " & "WHERE ProductID = @pid AND WarehouseID = @wid" Dim cmdStock As New SqlCommand(sqlStock, conn, tran) cmdStock.Parameters.AddWithValue("@qty", row.Cells("Qty").Value) cmdStock.Parameters.AddWithValue("@pid", row.Cells("ProductID").Value) cmdStock.Parameters.AddWithValue("@wid", cboWarehouse.SelectedValue) cmdStock.ExecuteNonQuery() Next tran.Commit() Catch ex As Exception tran.Rollback() MessageBox.Show("保存失败:" & ex.Message) End Try End Using这段逻辑的关键在事务边界:主表、明细、库存必须同生共死。我见过改过的版本把库存更新放到事务外面,结果明细写成功、库存没扣,账实不符查半天。SCOPE_IDENTITY()比@@IDENTITY安全,后者在有触发器时会拿到触发器里插入的 ID,这是血泪经验。参数化写法别偷懒改成字符串拼接,单号里带个单引号就能让整条语句崩掉。
3.2 库存扣减的时机与负库存控制
销售出库和采购入库方向相反,但坑更多。核心问题是:库存什么时候扣?常见两种做法,一是单据保存即扣,二是审核后才扣。这套源码默认是保存即扣,简单直接,但业务上意味着“草稿单也占库存”。如果你们的流程需要审核环节,得在保存时只写单据、审核时再动库存,代码要拆成两段,别在保存里顺手扣了。
负库存控制是另一个分水岭。源码里一般有个开关或者直接判断Qty - @qty < 0就拦。实际用起来,建议在出库保存前查一次当前库存:
SELECT ISNULL(Qty, 0) FROM Stock WHERE ProductID = @pid AND WarehouseID = @wid拿到的值小于出库数量就提示“库存不足”,别等 UPDATE 完再判断。注意并发场景:两个人同时出同一件商品,查的时候都够,扣的时候就不够了。桌面单机版问题不大,多人共用一套库时,稳妥做法是在 UPDATE 里加条件WHERE Qty >= @qty,受影响行数为 0 就回滚,这比先查后扣可靠。
3.3 往来账与应收应付的生成
采购入库对应应付增加,销售出库对应应收增加,这套源码在单据保存后通常会往往来账表插一条记录,字段包括单位、方向(应收/应付)、金额、关联单号。看这块代码时留意一点:退货单的方向要反过来,别照抄正常单的逻辑。我拆的时候发现有个版本退货也走“应收增加”,结果对账时客户欠款越退越多,典型的符号错误。
对账查询一般按单位 + 日期区间汇总,SQL 里SUM(CASE WHEN Direction = 1 THEN Amt ELSE -Amt END)这种写法很常见,方向字段用 1/-1 比用字符串“收”“付”好维护。金额字段建议用 decimal 而不是 float,财务数据用浮点迟早出小数误差。
4. 水晶报表与自定义套打:从模板绑定到针式打印机对齐
4.1 水晶报表的数据源绑定方式
这套源码的报表部分用的是 Crystal Reports,rpt 文件在 Report 目录。水晶报表在 VB.NET 里绑定数据有两条路:推模式(代码里构造 DataSet 赋给ReportDocument.SetDataSource)和拉模式(rpt 里写死连接)。老项目多用推模式,因为连接字符串不暴露在模板里。典型代码:
Dim ds As New DataSet() Dim da As New SqlDataAdapter("SELECT * FROM V_PurchasePrint WHERE BillNo = @no", conn) da.SelectCommand.Parameters.AddWithValue("@no", txtBillNo.Text) da.Fill(ds, "PurchasePrint") Dim rpt As New ReportDocument() rpt.Load(Application.StartupPath & "\Report\PurchaseBill.rpt") rpt.SetDataSource(ds.Tables("PurchasePrint")) CrystalReportViewer1.ReportSource = rpt CrystalReportViewer1.Refresh()V_PurchasePrint一般是个视图,把主表、明细、单位名称、商品名称拼好,报表端只管排版。这样做的好处是 rpt 里不用写多表关联,改字段只动视图。注意rpt.Load的路径用Application.StartupPath拼相对路径,别写死盘符,换台机器就找不到文件。水晶报表的运行时要装对应的 CRRuntime,版本和开发时引用的程序集对齐,32 位 64 位别搞混,否则报“无法加载报表”。
4.2 自定义套打的坐标控制与打印偏移
套打是这套源码里比较实在的部分。所谓套打,就是预先印好表格的纸张,程序只打数据,位置必须对得死死的。源码里自定义套打一般不走水晶报表的固定布局,而是用PrintDocument自己画,或者在水晶报表里用绝对定位的文本框。自己画的版本更灵活,核心是PrintPage事件里按坐标DrawString:
Private Sub pd_PrintPage(sender As Object, e As PrintPageEventArgs) Handles pd.PrintPage Dim g As Graphics = e.Graphics Dim font As New Font("宋体", 10) Dim y As Single = 50 ' 表头固定位置 g.DrawString(txtCustomer.Text, font, Brushes.Black, 120, 40) g.DrawString(dtpDate.Value.ToString("yyyy-MM-dd"), font, Brushes.Black, 520, 40) ' 明细逐行,行高按纸张实测调整 For Each row As DataGridViewRow In dgvDetail.Rows g.DrawString(row.Cells("ProductName").Value.ToString(), font, Brushes.Black, 60, y) g.DrawString(row.Cells("Qty").Value.ToString(), font, Brushes.Black, 320, y) g.DrawString(row.Cells("Price").Value.ToString("F2"), font, Brushes.Black, 420, y) y += 22 Next End Sub坐标单位是百分之一英寸,y += 22就是行距约 5.6 毫米,这个值必须拿尺子量实际纸张的表格行高来定,不能拍脑袋。针式打印机还有进纸偏移,常见做法是在打印设置里加一个全局偏移量,X、Y 各留一个可调参数,现场微调。我一般会把偏移量存到配置文件,而不是写死在代码里,换打印机不用重新编译。
注意:套打调试别用 A4 激光打印机试,行高和边距跟针式机完全两回事。拿实际用的打印机、实际的空白单据纸调,调一次记一次参数。
4.3 报表参数与打印份数控制
实际业务里,一张单据经常要打多联(客户联、存根联),套打代码里通常用一个循环控制份数,每份之间可能需要微调偏移,因为多联纸的复写层有厚度差。源码里如果有PrintCount参数,看它是在PrintPage里循环还是在外层重复调用Print()。前者要注意每页只触发一次PrintPage,循环画多份时HasMorePages的处理别写错,否则要么只打一份,要么无限打。
报表参数传递也是易错点。水晶报表的参数字段如果设了“离散值”,代码里ParameterFields的赋值顺序和名称必须和 rpt 里完全一致,大小写敏感。传错参数不报错,只是报表空白,这种黑匣子式的失败最耗时间,建议在赋值后打个日志把参数名和值输出来核对。
5. 避坑清单:这套源码实际跑起来最容易翻车的五处
5.1 连接字符串明文与部署路径问题
现象:本机跑得好好的,拷到另一台机器登录就报连接失败。原因多半是 App.config 里的连接字符串写死了本机实例名或 sa 密码,目标机器实例不同。解决是把Data Source改成目标机实际实例,或者用 Windows 身份验证Integrated Security=True避开密码。部署时确认 exe 同目录下有正确的 config 文件,别只拷了 exe。
5.2 水晶报表运行时版本不匹配
现象:设计器里预览正常,运行时报“无法加载文件或程序集 CrystalDecisions...”或直接闪退。原因是开发机装了完整版水晶报表,目标机只装了运行时,且位数或版本对不上。解决是确认项目引用的 CrystalDecisions 程序集版本,在目标机装同版本 CRRuntime,32 位程序装 32 位运行时。发布时把这些 dll 一起带上,别指望目标机自带。
5.3 库存更新漏掉仓库维度
现象:同一商品在两个仓库都有货,出库后总数对了,但分仓库的账不对。原因是库存 UPDATE 只按商品 ID 更新,没带仓库条件,把两个仓库的记录一起改了。解决是库存表必须有商品 + 仓库的联合唯一约束,所有增减都带WarehouseID条件,调拨单要一减一加两条语句。
5.4 套打坐标在不同打印机上漂移
现象:办公室调试好的单据,拿到仓库的针式打印机上整体偏左或偏下几毫米。原因是不同打印机的可打印区域和进纸基准不同。解决是把 X、Y 偏移做成配置项,现场用一张测试单微调后保存;同时打印前用e.PageSettings.HardMarginX/Y参考硬件边距,别假设从 0 开始。
5.5 单据号并发生成重复
现象:多人同时开单,偶尔出现两张单号一样的单据,主键冲突或数据串行。原因是单号用“查最大号 + 1”生成,两个请求同时查到同一个最大值。解决是单号生成放进事务并用UPDLOCK锁住序列表,或者直接用数据库序列/自增,别在应用层算。桌面单机版可以忽略,共用库必须处理。
6. 二次开发与验证:把源码改成自己业务的三个实操技巧
拿到这套源码,多数人不是原样用,而是要改成自己的字段和流程。第一个技巧是加字段先加视图。业务表加列之后,别急着改所有窗体,先把打印和查询用的视图补上新列,报表和列表页往往只认视图,改一处就够。第二个技巧是单据状态用枚举而不是魔法数字,源码里如果到处是If Status = 1,先抽一个枚举模块,后面加“已审核”“已作废”不用全局搜数字。
验证改动是否安全,我习惯跑一个最小回归:新建一张采购单 → 查库存增加 → 查应付增加 → 打印套打一张 → 做一张退货单 → 确认库存和应付都回退。这五步覆盖了主链路,任何一步数字不对,问题就锁定在那段事务里。下面这个查询可以快速核对某商品的账实是否一致:
SELECT p.ProductName, ISNULL(SUM(CASE WHEN d.Direction = 1 THEN d.Qty ELSE -d.Qty END), 0) AS 流水结存, ISNULL(s.Qty, 0) AS 库存表结存 FROM Product p LEFT JOIN StockFlow d ON p.ProductID = d.ProductID LEFT JOIN Stock s ON p.ProductID = s.ProductID GROUP BY p.ProductName, s.Qty HAVING ISNULL(SUM(CASE WHEN d.Direction = 1 THEN d.Qty ELSE -d.Qty END), 0) <> ISNULL(s.Qty, 0)这条 SQL 把流水账汇总和库存表对比,HAVING只输出不一致的商品。每次改完库存相关逻辑,我都会跑一遍,空结果才敢往下走。从那以后我每次动单据保存或库存扣减的代码,都强制先跑这个对账查询再提交,比事后翻日志省太多时间。希望帮到你。
本文还有配套的精品资源,点击获取