SpringBoot3+Vue3 库存盘点:账面和实盘对不上,差异单怎么走审批再调账

SpringBoot3+Vue3 库存盘点:账面和实盘对不上,差异单怎么走审批再调账

🌐 文档地址https://ruoyioffice.com

📦 源码1·GitHubhttps://github.com/yuqing2026/ruoyi-office

📦 源码2·GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office

📦 源码3·Giteehttps://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)

  1. 打开 ERP → 库存管理 → 库存盘点/erp/stock/check)。
  2. 新建盘点单,选仓库和产品,确认账面库存是带出来的。
  3. 把实际库存改成和账面不同,看盈亏数量是否等于实盘减账面。
  4. 保存后点审批,再到 出入库明细 按单号找 40 或 42。
  5. 点反审批,确认出现 41 或 43,库存回到审核前。
  6. 不要去产品库存页手工改余额来「盘平」。
  7. 固定资产盘点请走资产模块的差异处理单,不要在本页找 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 支持一下!

相关推荐
行者-全栈开发2 小时前
Spring Boot 接入 MaxKey 单点登录:6 个内部系统,一次登录全通行
spring boot·iam·oauth2·统一认证·oidc·sso 单点登录·maxkey
user_admin_god3 小时前
第 09 篇:实践一 —— 文本摘要(Map-Reduce 分块)
java·人工智能·spring boot·语言模型
vx_BS813303 小时前
【项目编号:project49759】Spring Boot 宠物综合服务平台:商城、预约、咨询、活动与健康档案的一体化实现
java·spring boot·eclipse·tomcat·intellij-idea
user_admin_god3 小时前
第 05 篇:结构化输出 —— 让模型稳定返回 JSON
java·人工智能·spring boot·语言模型
驭渊的小故事4 小时前
Spring Boot 注解详解01
java·前端·spring boot
前端初见4 小时前
Java+AI零基础入门到大牛
java·spring boot·spring·java-ee·intellij-idea
RuoyiOffice4 小时前
SpringBoot3+PaddleOCR 发票识别:不接云端大模型,本地飞桨怎么把专票读进进项单
spring boot·vue3·飞桨·paddleocr·spring boot 3·发票ocr·进项发票
ly76894 小时前
Spring Boot 3.2 虚拟线程在 Web 层与 JDBC 池的实践边界
前端·spring boot·后端·tomcat·jdbc·虚拟线程
QQ_21696290965 小时前
基于SpringBoot+Vue的小生活平台的设计与实现
java·数据库·vue.js·spring boot·spring·微信小程序·生活