SpringBoot3+Vue3 库存盘点:账面和实盘对不上,差异单怎么走审批再调账
🌐 文档地址 :https://ruoyioffice.com
📦 源码1·GitHub :https://github.com/yuqing2026/ruoyi-office
📦 源码2·GitCode :https://gitcode.com/zhouzhongyan/ruoyi-office
📦 源码3·Gitee :https://gitee.com/yqzy1688/ruoyi-office
💬 微信:17156169080(备注「RuoYi Office」)
月底仓库抽盘,账面 12 台,货架 10 台。有人直接在库存数字上减 2,出库单和盘点对不上,审计问「谁改的」没人接。RuoYi Office 的物料库存不这么干。账面从库存带出,实盘手填,盈亏写在盘点明细上;这张盘点单就是差异单。点审核,SpringBoot3 才写盘盈入库或盘亏出库流水。库存表只认流水,不认手改。

▲ 上:禁止直接改 erp_stock。下:账面只读、实盘可写、盈亏=实盘−账面;status 到已审核才出现业务类型 40 / 42 的出入库明细
引言:库存盘点难在「谁有权改库存」
物料库存是数量账。盘点要回答的不是「卡片在哪个房间」,是这个 SKU 在这个仓,系统数和货架数差几件,差的部分何时入账。
| 错法 | 看起来省事 | 三个月后 |
|---|---|---|
| 仓管改库存字段 | 立刻盘平 | 没有单据,对不上进销存 |
| 未审核就改库存 | 少点一次 | 填错实盘已经出库 |
| 盘平行也写流水 | 逻辑简单 | 出入库明细被 0 行刷屏 |
| 反审核只改状态 | 少写代码 | 库存留下一次盘盈,再也冲不回 |
固定资产盘点是另一条链:一物一卡,差异要拆更正 / 盘亏报废 / 盘盈建卡,走 Flowable。那篇已经写过。物料库存不必再拆第二张 BPM 单 ------差异就在盘点明细的 count 上,审核按钮就是调账。
一、产品能力与特点
库存盘点单是「一次抽盘的账面、实盘和盈亏清单」。谁用:仓管、物控在 ERP → 库存管理 → 库存盘点 新建或打开详情。解决的事:货架数和系统数对不上时,留下可审核、可冲回的单据。
| 能力 | 面向角色 | 和「只做一张审批单」差在哪 |
|---|---|---|
| 账面自动带出 | 仓管 | 选仓库+产品后读 erp_stock,账面列只读 |
| 实盘手填 | 仓管 | 盈亏实时算,为 0 表示盘平 |
| 审核才调账 | 库管主管 | 不是 Flowable 节点,是单据状态 10→20 |
| 反审核冲回 | 库管主管 | 写作废流水,不是删历史行 |
和固定资产差异处理单对照一句就够:卡片台账改的是位置和状态,要单独审批回写;SKU 数量账改的是件数,盘点单审核即可。不要把两套话术抄在同一张菜单里。
二、业务流程怎么串
主路径:新建盘点 → 填实盘看出盈亏 → 审核写流水 → 出入库明细能追到单号。
2.1 新建盘点单,账面从库存来
打开 /erp/stock/check,点新建。表头填盘点时间、备注;明细选仓库和产品。账面库存 stockCount 由当前库存接口带出,不要让仓管手改账面来「做平」。
实际库存 actualCount 才是货架数。盈亏 count = actualCount - stockCount。例如账面 12、实盘 10,盈亏 −2,审核后走盘亏出库。

▲ 盘点单 QCPD20260915000001。主仓库、罗技键盘:账面 0、实盘 3、盈亏 +3。未审核时库存一张未动
状态:盘点单已保存,erp_audit_status=10(未审核),erp_stock 未变。
2.2 主管点审核,这张单就是差异单
列表或详情上的「审批」把状态改成 20。没有第二张「差异处理单」可点------物料差异已经写在明细 count 上。count=0 的行直接跳过,避免盘平行制造空流水。
count>0 写成盘盈入库(业务类型 40),count<0 写成盘亏出库(42)。数量带符号写入 erp_stock_record,库存余额按流水累加。

▲ 同一张 QCPD20260915000001。盈亏仍是 +3。要改实盘必须先反审核,反审核会按相反数量冲流水
状态:status=20,有盈亏的行已有库存流水,biz_no 等于盘点单号。
2.3 出入库明细核对调账
打开 ERP → 库存管理 → 出入库明细 (/erp/stock/record)。按盘点单号或业务类型 40 / 42 过滤。这里应能看到产品、仓库、数量和来源单号。对不上就反审核,不要再去库存表改余额。

▲ 出入库明细第一行:罗技键盘、主仓库、盘盈入库、单号 QCPD20260915000001、数量 3。这就是审核写下的流水
状态:库存余额 = 历史流水之和。盘点单是原因,流水是结果。
三、设计怎么落地
3.1 设计思路
| 决策点 | 方案 | 理由 |
|---|---|---|
| 差异写在哪 | 盘点明细 count |
SKU 数量差一张单能说清,不必再生成 BPM 单 |
| 何时改库存 | 审核 / 反审核 | 未审先改,填错实盘无法收回 |
| 盘平行 | count==0 跳过 |
流水只记录真正盈亏 |
| 冲回怎么做 | 数量取反 + 41 / 43 | 保留审计,不删 40 / 42 |
| 和资产盘点 | 分模块、分菜单 | 卡片回写要 Flowable,数量账不要假装有会签 |
3.2 表结构
#mermaid-svg-RoZrvBokWpWfdAjU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RoZrvBokWpWfdAjU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RoZrvBokWpWfdAjU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RoZrvBokWpWfdAjU .error-icon{fill:#552222;}#mermaid-svg-RoZrvBokWpWfdAjU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RoZrvBokWpWfdAjU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RoZrvBokWpWfdAjU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RoZrvBokWpWfdAjU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RoZrvBokWpWfdAjU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RoZrvBokWpWfdAjU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RoZrvBokWpWfdAjU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RoZrvBokWpWfdAjU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RoZrvBokWpWfdAjU .marker.cross{stroke:#333333;}#mermaid-svg-RoZrvBokWpWfdAjU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RoZrvBokWpWfdAjU p{margin:0;}#mermaid-svg-RoZrvBokWpWfdAjU .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-RoZrvBokWpWfdAjU .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-RoZrvBokWpWfdAjU .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-RoZrvBokWpWfdAjU .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-RoZrvBokWpWfdAjU .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-RoZrvBokWpWfdAjU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RoZrvBokWpWfdAjU .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-RoZrvBokWpWfdAjU .node rect,#mermaid-svg-RoZrvBokWpWfdAjU .node circle,#mermaid-svg-RoZrvBokWpWfdAjU .node ellipse,#mermaid-svg-RoZrvBokWpWfdAjU .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RoZrvBokWpWfdAjU .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-RoZrvBokWpWfdAjU .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-RoZrvBokWpWfdAjU .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RoZrvBokWpWfdAjU .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-RoZrvBokWpWfdAjU .edgeLabel .label text{fill:#333;}#mermaid-svg-RoZrvBokWpWfdAjU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} items
posts
erp_stock_check
string
no
int
status
decimal
total_count
erp_stock_check_item
long
warehouse_id
long
product_id
decimal
stock_count
decimal
actual_count
decimal
count
erp_stock_record
int
biz_type
string
biz_no
decimal
count
| 表 | 关键列 | 作用 |
|---|---|---|
erp_stock_check |
no / status / total_count |
盘点单头;10 未审核、20 已审核 |
erp_stock_check_item |
stock_count / actual_count / count |
账面、实盘、盈亏 |
erp_stock_record |
biz_type / biz_no / count |
唯一调账出口;库存余额由它累加 |
erp_stock 是余额快照,不要在盘点过程中直接 UPDATE 它。
3.3 时序图
erp_stock StockRecordService StockCheckService Vue3 盘点弹窗 erp_stock StockRecordService StockCheckService Vue3 盘点弹窗 #mermaid-svg-nmAncvGEoWnTESbG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nmAncvGEoWnTESbG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nmAncvGEoWnTESbG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nmAncvGEoWnTESbG .error-icon{fill:#552222;}#mermaid-svg-nmAncvGEoWnTESbG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nmAncvGEoWnTESbG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nmAncvGEoWnTESbG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nmAncvGEoWnTESbG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nmAncvGEoWnTESbG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nmAncvGEoWnTESbG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nmAncvGEoWnTESbG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nmAncvGEoWnTESbG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nmAncvGEoWnTESbG .marker.cross{stroke:#333333;}#mermaid-svg-nmAncvGEoWnTESbG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nmAncvGEoWnTESbG p{margin:0;}#mermaid-svg-nmAncvGEoWnTESbG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nmAncvGEoWnTESbG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-nmAncvGEoWnTESbG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nmAncvGEoWnTESbG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-nmAncvGEoWnTESbG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-nmAncvGEoWnTESbG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-nmAncvGEoWnTESbG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-nmAncvGEoWnTESbG .sequenceNumber{fill:white;}#mermaid-svg-nmAncvGEoWnTESbG #sequencenumber{fill:#333;}#mermaid-svg-nmAncvGEoWnTESbG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-nmAncvGEoWnTESbG .messageText{fill:#333;stroke:none;}#mermaid-svg-nmAncvGEoWnTESbG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nmAncvGEoWnTESbG .labelText,#mermaid-svg-nmAncvGEoWnTESbG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-nmAncvGEoWnTESbG .loopText,#mermaid-svg-nmAncvGEoWnTESbG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-nmAncvGEoWnTESbG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nmAncvGEoWnTESbG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-nmAncvGEoWnTESbG .noteText,#mermaid-svg-nmAncvGEoWnTESbG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-nmAncvGEoWnTESbG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nmAncvGEoWnTESbG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nmAncvGEoWnTESbG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nmAncvGEoWnTESbG .actorPopupMenu{position:absolute;}#mermaid-svg-nmAncvGEoWnTESbG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-nmAncvGEoWnTESbG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nmAncvGEoWnTESbG .actor-man circle,#mermaid-svg-nmAncvGEoWnTESbG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-nmAncvGEoWnTESbG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} status 仍为 10,库存不动 altcount\>0count\<0 保存明细(账面/实盘/盈亏)updateStatus(id, 20)count==0 则跳过bizType=40 盘盈入库bizType=42 盘亏出库按 count 调整余额出入库明细按单号查询
反审核把 status 拉回 10,并对每一行把当时的 count 取反,业务类型改成 41 或 43。
3.4 核心代码
账面只读、实盘可写。 对应 2.1。盈亏列没有编辑槽,避免人改差异伪装盘平。
typescript
{
field: 'stockCount',
title: '账面库存',
minWidth: 80,
formatter: 'formatAmount3',
},
{
field: 'actualCount',
title: '实际库存',
minWidth: 120,
fixed: 'right',
slots: { default: 'actualCount' },
formatter: 'formatAmount3',
},
{
field: 'count',
title: '盈亏数量',
minWidth: 120,
fixed: 'right',
formatter: 'formatAmount3',
},
审核才出入库,盘平行不写流水。 对应 2.2。approve 用明细 count 原值;反审核取反,并切换作废类型。
java
stockCheckItems.forEach(stockCheckItem -> {
if (stockCheckItem.getCount().compareTo(BigDecimal.ZERO) == 0) {
return;
}
BigDecimal count = approve
? stockCheckItem.getCount()
: stockCheckItem.getCount().negate();
Integer bizType;
if (approve) {
bizType = count.compareTo(BigDecimal.ZERO) > 0
? ErpStockRecordBizTypeEnum.CHECK_MORE_IN.getType()
: ErpStockRecordBizTypeEnum.CHECK_LESS_OUT.getType();
} else {
bizType = count.compareTo(BigDecimal.ZERO) > 0
? ErpStockRecordBizTypeEnum.CHECK_MORE_IN_CANCEL.getType()
: ErpStockRecordBizTypeEnum.CHECK_LESS_OUT_CANCEL.getType();
}
stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
stockCheckItem.getProductId(), stockCheckItem.getWarehouseId(), count,
bizType, stockCheckItem.getCheckId(), stockCheckItem.getId(), stockCheck.getNo()));
});
前端审核只改状态,不自己算库存。 对应 2.2 的按钮。数量账在服务端入账,避免弹窗和列表各算一次。
typescript
async function handleUpdateStatus(
row: ErpStockCheckApi.StockCheck,
status: number,
) {
const hideLoading = message.loading({
content: `确定${status === 20 ? '审批' : '反审批'}该盘点单吗?`,
duration: 0,
});
try {
await updateStockCheckStatus(row.id!, status);
message.success(`${status === 20 ? '审批' : '反审批'}成功`);
handleRefresh();
} finally {
hideLoading();
}
}
业务类型字典要写进实施备忘:40 盘盈入库、41 盘盈入库(作废)、42 盘亏出库、43 盘亏出库(作废)。报表按这四类过滤,不要和采购入库 混在一句「都是入库」。
四、边界与对照
| 设计要点 | 落地方式 | 对业务的价值 |
|---|---|---|
| 差异在明细 | count=实盘−账面 |
一张单说清对不上多少 |
| 未审不动库存 | 只改 status=10 | 填错实盘没有副作用 |
| 审核才调账 | updateStockCheckStatus |
主管动作可审计 |
| 盘平不写流水 | count==0 return |
明细干净 |
| 反审核冲回 | 数量取反 + 41/43 | 能改,不是删历史 |
| 账面只读 | 列无编辑槽 | 不能改账面伪装盘平 |
| 和资产盘点分轨 | 资产走 asset_stocktake_adjust |
卡片回写不套数量账 |
| 余额只认流水 | erp_stock_record |
禁止盘点过程 UPDATE 余额 |
传统「盘点结束直接改库存数字」和本方案的差别:前者改完无单,后者每件盈亏都能从出入库明细跳回盘点单号。
五、快速体验
在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)
- 打开 ERP → 库存管理 → 库存盘点 (
/erp/stock/check)。 - 新建盘点单,选仓库和产品,确认账面库存是带出来的。
- 把实际库存改成和账面不同,看盈亏数量是否等于实盘减账面。
- 保存后点审批,再到 出入库明细 按单号找 40 或 42。
- 点反审批,确认出现 41 或 43,库存回到审核前。
- 不要去产品库存页手工改余额来「盘平」。
- 固定资产盘点请走资产模块的差异处理单,不要在本页找 Flowable。
源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office
相关阅读:资产盘点差异处理(更正 / 盘亏核销 / 盘盈建卡);和本文一个讲卡片,一个讲件数。
常见问题(FAQ)
ERP 库存盘点为什么没有单独的差异审批单?
物料差异是件数,写在盘点明细的盈亏列就够了。再拆一张 Flowable 单,仓管要点两次、实施要配两套菜单,对数量账没有额外信息。需要单独审批回写的是固定资产卡片,走资产差异处理。
账面和实盘对不上,什么时候库存才会变?
保存盘点单不会变。只有把状态从「未审核」改成「已审核」,并且该行 count≠0 时,才写盘盈或盘亏流水。未审核的差异只是草稿。
反审核会把流水删掉吗?
不会。系统再写一条数量相反、类型为作废的流水。审计要看的是「曾经盘盈过、后来冲回」,不是库存数字自己长回去。
盘平的行会不会也出库一次?
不会。count 与 0 比较相等就 return,不调用 createStockRecord。出入库明细里不应出现数量为 0 的盘点行。
RuoYi Office 的库存盘点可以体验吗?
可以。后端 Spring Boot 3.5 + 前端 Vue3,演示站按上面路径新建一张带盈亏的盘点单并审批即可。完整能力与私有化部署咨询商业版。
结语
物料盘点要保住的是可冲回的流水,不是货架上那个口头数字。账面带来、实盘手填、审核入账、反审核冲回。差异单就是这张盘点单本身,不要为了「看起来更正式」再叠一张空审批。
卡片盘点继续用资产模块的三岔回写。两套对象不要合成一个「万能盘点」。
你们仓库现在是改库存数字盘平,还是已经留下盘点单号?欢迎对一下反审核有没有冲得回来。
💡 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)
📦 源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office
💬 技术咨询 :添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!