大数跨境

ERP 库存为什么总对不上?把采购、销售和退货放进同一本账

ERP 库存为什么总对不上?把采购、销售和退货放进同一本账 企业软件源码
2026-11-07
16
导读:ERP 库存为什么总对不上?把采购、销售和退货放进同一本账仓库一收到 5 台显示器,之后出了两次库,明细里最后一笔却还是 5 台。

ERP 库存为什么总对不上?把采购、销售和退货放进同一本账

仓库一收到 5 台显示器,之后出了两次库,明细里最后一笔却还是 5 台。光看采购和销售单,很容易算成 3 台。打开出入库明细才会发现,中间还有一笔出库撤销和一笔其它入库。

对库存时,先定商品和仓库,再把期间内每一笔带正负号的变动排齐。 单据回答这批货为什么移动,流水回答这次变了多少,库存余额回答这个仓现在还有多少。

本文用 RuoYi Office 的历史演示单据和实际流水读一遍账,再拆开 SpringBoot 的审核、条件更新和流水写入代码。后面的退货数字是按规则推演的教学算例。

同一台显示器,在仓库一是怎样从 5 变回 5 的

下面是系统「出入库明细」的完整画面。先只认同一商品「戴尔 U2723QE 显示器」和同一「仓库一」,跳过同张销售单里的笔记本、网线等其它商品。按画面中该商品的流水时间,从早到晚读。

历史演示画面。表格默认把较新的记录放在前面,下面只摘录画面可见的五行。

流水时间
变动来源
出入库数量
该行库存量
15:30:02
采购入库,入库单尾号 000001
+5
5
15:30:15
销售出库,出库单尾号 000001
−1
4
16:55:19
销售出库撤销,同一出库单
+1
5
16:58:16
其它入库,另一张单
+1
6
17:15:49
销售出库,出库单尾号 000002
−1
5

这组记录里的正数有三种来源。第一条是采购到货,第三条是撤销原出库,第 4 条是其它入库。数量的正负决定加减,业务类型决定该回查哪张单。 尤其是撤销出库和销售退货,都会让库存增加,但业务含义不同。

表里最后一行的 5 是这段历史流水的该行库存量。要看现在还有多少,应到「产品库存」按同一商品与仓库查看当前余额。查某段时间时,还要先确认期初,不能只对着明细第一页求和。

采购入库和销售出库都按单据行记账

在采购入库详情里,单号 CGRK20260908000001 关联采购订单,产品清单里的仓库为「仓库一」,数量为 5。回到上一张明细图,按完整入库单号定位,可以看到这笔采购入库的 +5。

采购订单记录计划采购量,入库单描述这一批实际入哪个仓。当前代码在采购入库单进入审核通过状态时,才遍历入库明细,取每行的商品、仓库、数量和单据来源。采购入库审核的增量是正数,反审核成功后是负数。源码在同一个分支里选择流水类型和数量符号,随后把入库行原有的 ID 传给库存服务。

Integer bizType = approve ? ErpStockRecordBizTypeEnum.PURCHASE_IN.getType()        : ErpStockRecordBizTypeEnum.PURCHASE_IN_CANCEL.getType();purchaseInItems.forEach(purchaseInItem -> {    BigDecimal count = approve ? purchaseInItem.getCount() : purchaseInItem.getCount().negate();    stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(            purchaseInItem.getProductId(), purchaseInItem.getWarehouseId(), count,            bizType, purchaseInItem.getInId(), purchaseInItem.getId(), purchaseIn.getNo()));});

如果一张采购订单分两批到货,对账要分别找到两张入库单及其流水。订单号可以说明两批货来自同一笔采购,流水里保留的业务单号则是实际发生库存变更的入库单号。用采购订单号直接筛库存流水,可能漏掉它下面的执行单据。

销售出库详情的单号 XSCK20260908000002 也能在第一张图找到。详情合计是 3,下面有三条各 1 的商品行。流水中同一个单号对应三行 −1,显示器只是其中一行。看到单号相同而记录多条时,先核对商品和明细行,避免把一张多商品单误认为重复记账。

历史画面按单号回查,出库详情里的业务时间和流水列表的记录时间来自不同字段。按单号、商品和仓库核对更直接。

移动端也能从单据上核对审核状态。下面是另一张历史演示出库单,页面显示「已审核」和对应的反审批入口。它用于说明操作状态,和上面的显示器流水属于不同单据。

审核时,ErpSaleOutServiceImpl.updateSaleOutStatus 先比对当前状态,再执行带原状态条件的更新。状态相同会报错,更新影响行数为 0 也会报错。销售出库的库存分支如下,审核给出负数,撤销给出正数。

Integer bizType = approve ? ErpStockRecordBizTypeEnum.SALE_OUT.getType()        : ErpStockRecordBizTypeEnum.SALE_OUT_CANCEL.getType();saleOutItems.forEach(saleOutItem -> {    BigDecimal count = approve ? saleOutItem.getCount().negate() : saleOutItem.getCount();    stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(            saleOutItem.getProductId(), saleOutItem.getWarehouseId(), count,            bizType, saleOutItem.getOutId(), saleOutItem.getId(), saleOut.getNo()));});

后端收到的是逐行的增量和来源。两个同仓的商品行各算各的库存,一个商品分到不同仓库也要各自核对。销售出库已收款、采购入库已付款时,对应服务会拦截反审核,业务人员需先按实际收付款情况处理后续单据。

销售退货是货回来,采购退货是货出去

在 ERP 菜单里,销售退货和采购退货分别放在销售、采购下。创建两种退货单时,服务会分别校验对应的已审核订单。库存从哪一个仓增减,仍取退货明细上的仓库。审核销售退货时,传入正数;审核采购退货时,传入负数。两者各有独立的流水类型和反审核类型。

单据
审核时的库存增量
反审核时的增量
流水类型编号
采购入库
+数量
−数量
70 / 71
销售出库
−数量
+数量
50 / 51
销售退货
+数量
−数量
60 / 61
采购退货
−数量
+数量
80 / 81

以下是销售退货分支。单号、业务主键和行主键来自退货单本身,后续可以把这条入库流水定位到具体退货明细。

Integer bizType = approve ? ErpStockRecordBizTypeEnum.SALE_RETURN.getType()        : ErpStockRecordBizTypeEnum.SALE_RETURN_CANCEL.getType();saleReturnItems.forEach(saleReturnItem -> {    BigDecimal count = approve ? saleReturnItem.getCount() : saleReturnItem.getCount().negate();    stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(            saleReturnItem.getProductId(), saleReturnItem.getWarehouseId(), count,            bizType, saleReturnItem.getReturnId(), saleReturnItem.getId(), saleReturn.getNo()));});

采购退货对应的是 PURCHASE_RETURN / PURCHASE_RETURN_CANCEL,数量表达式正好相反。

BigDecimal count = approve ? purchaseReturnItem.getCount().negate() : purchaseReturnItem.getCount();

用同一商品、同一仓库做一笔教学推演。期初为 0,采购入库 10 件、销售出库 3 件、客户退回 1 件,每笔审核后分别是 10 → 7 → 8。如果再把 2 件退给供应商,采购退货审核后是 6。这时若撤销采购退货,成功时增加 2 件,余额回到 8,并另写一条对应的撤销流水。两种退货已有退款金额时,服务会拦截各自的反审核。

教学算例。数字用来核对库存方向,实际流水见开篇的系统画面。

后端怎样让余额和流水一起写入

余额表 erp_stock 按商品 ID 和仓库 ID 查一条记录,count 是当前库存。流水表 erp_stock_record 上的 count 则是本次增量,totalCount 是写入该行时计算的库存值。它还保存 bizType、bizId、bizItemId、bizNo,分别对应业务类型、单据主键、明细行主键和业务单号。

createStockRecord 当前实现先改余额,再组装并插入流水。摘录如下。

@Transactional(rollbackFor = Exception.class)public void createStockRecord(ErpStockRecordCreateReqBO createReqBO) {    BigDecimal totalCount = stockService.updateStockCountIncrement(            createReqBO.getProductId(), createReqBO.getWarehouseId(), createReqBO.getCount());    ErpStockRecordDO stockRecord = BeanUtils.toBean(createReqBO, ErpStockRecordDO.class)            .setTotalCount(totalCount);    stockRecordMapper.insert(stockRecord);}

四类单据的状态方法也标注了事务。按当前调用路径,单据状态更新和库存写入由事务包裹,明细扣减失败会抛异常。联调多行单据时,可以让第一行库存充足、第二行不足,操作失败后再查单据状态、两行余额和流水,确认整单回滚。

按源码绘制的写入链路。流水里的单据主键和明细行主键用于追溯,单号用于界面查询。

负库存控制有两层。服务先用读到的余额给出库存不足提示。真正扣减时,在同一条数据库更新上加库存下限条件,防止两笔请求都读到旧余额后各扣一次。下面是 Mapper 的扣减分支节选。

if (!negativeEnable) {    updateWrapper.ge(ErpStockDO::getCount, count.abs());}updateWrapper.setSql("count = count - " + count.abs());

当前服务把负库存开关设为 false。假设同一仓剩 5 件,两张单分别要出 4 件,单就已有余额行的扣减条件看,先完成的更新扣到 1 后,另一条更新就不满足 count >= 4。这和前面的“按原状态更新”各解决一件事。前者守的是余额下限,后者防止同一状态迁移重复触发库存变更。

还有一个对账细节。当前 updateStockCountIncrement 先读取余额,数据库做增量更新,随后用此前读到的 stock.getCount().add(count) 生成流水的 totalCount。两个事务如果都先读到 10、分别增加 1,成功执行的增量最终可能让余额到 12,而各自计算的流水库存值都可能是 11。这是从代码推演的并发情形,需结合实际数据库隔离级别实测。排查这类差异时,应同时核对当前余额与带符号的流水增量,并把单行 totalCount 当作写入时计算值来检查。

对不上时,先确认是不是漏算了别的业务

出入库明细可按商品、仓库、类型、业务单号和创建时间过滤。按单号查原单时,注意当前 Mapper 对单号做模糊匹配,返回后仍需核对完整单号。按期间对账时,先放开类型过滤,把同仓的撤销、其它入库、调拨或盘点差异一起纳入。只筛采购和销售,会把开篇的其它入库漏掉。

下面是一张独立的盘点示例。它显示「主仓库」某商品账面为 0、实盘为 3、盈亏数量为 3。看到盘点差异后,要继续按盘点单号回查出入库明细,确认是否实际生成了变动。盘点页面上的实盘数和系统当前余额有各自的含义。

如果账面数字仍然接不上,可以按下面的顺序缩小范围。

现场看到的问题
优先核对
单据有数量,流水却查不到
查看执行单的审核状态,用入库单、出库单或退货单的完整单号查明细
单号查到了,变动数量却不符
逐条核对商品、仓库和单据行,确认有没有把多行合计当作一行
同单号有两条方向相反的记录
看业务类型是否分别为正常变动和作废,按两笔增量合算
只差在某一行的库存量
核对查询时间范围、当前余额和后续增量。有并发写入时单独复查该行的计算值

比如,开篇看到 XSCK20260908000001 同时有 −1 和 +1,就应先确认后一行的类型是销售出库作废。把它当成客户退货,会查错单据,也会把业务责任归到错误的环节。

实际排查可以按四个字段收口。用商品、仓库查余额和期间流水,用业务类型、完整单号回到原单,再核对单据行和增量的正负。先把每一笔解释清楚,才知道是漏了单据、仓库选错,还是要追查写入链路。

【声明】内容源于网络
0
0
企业软件源码
RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
内容 186
粉丝 0
企业软件源码 RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
总阅读5.2k
粉丝0
内容186