ERP看板别把订单额当销售额:出入库、退货与收付款如何分开统计?
一张订单已经审核,货还没发出去,能不能计入首页“销售”?货已经出库,但客户没有结清,销售数字要不要跟着变?上个月发出的货,这个月退回来,又该扣在哪一期?
这些问题都发生在同一条业务链上,却不能交给同一个金额字段回答。订单记录承诺,出入库记录履约,收付款记录结算。管理者需要把它们放在一起看,系统则需要把来源分开存、把计算分开做。
下面结合RuoYi Office的真实界面和统计实现,沿一批已有演示数据核对。看板使用PC页面,单据使用独立移动端H5详情;两类界面都只读查看,没有创建、审批或修改业务记录。H5截图不代表原生APP真机验证。
▲ 订单、出入库与退货、收付款属于三类事实。主图中的历史界面用于表达产品链路,下面的数字核对以本次开发演示样本为准。
先看清首页上同时出现了什么
经营看板把销售、采购、库存和待办放在一个入口里。金额卡帮助判断期间业务规模,待出库、待入库帮助定位履约积压,待收款、待付款帮助找到尚未结清的单据。
▲ 本次开发演示数据:今年销售1,197,559.31,待出库2,待收款7。三个数字的单位和范围不同;整屏小字可点开放大。
“今年销售”在当前实现中来自销售出库减销售退货;“待出库2”是符合条件的订单笔数;“待收款7”是符合条件的出库单笔数。它们不是把同一批订单换三个名字展示。
尤其要注意,首页切换今日、本月、本年时,前端只重新加载概览金额。工作台里的待收款、待付款是当前未结清状态,不跟随期间切换;概览中的待出入库计数本身也没有加期间条件。
这样的组合适合同时回答“这一期发生了多少”和“现在还有什么没处理”,但产品说明必须把两种范围讲清。不能因为卡片相邻,就把所有数字都理解成“本年新增”。
从订单到出库:承诺兑现了,结算可能还没完成
先看同一张单据的两种身份。订单ERP-HOME-SO-202609-01在已有样本中已审核,总数量58,出库数量也是58,含税金额232,149.46。
▲ 独立H5页面,430×932完整视口。订单数量与出库进度用来判断履约,金额字段用来表达订单规模,不能据此认定款项已经到账。
对应出库单ERP-HOME-SX-202609-01的orderId指向这张订单,出库数量58,应收金额232,149.46,已收金额0。它的出库时间是9月3日15:30,订单时间则是当天10:00。
▲ 同一开发样本的出库单已审核,已收仍为0,未收232,149.46。完整履约和完成结算是两个判断;本次没有执行收款。
同一张单内部也有金额口径:本例产品金额为205,442.00,税额26,707.46,两者合计才是232,149.46,优惠和其它费用均为0。拿产品金额与含税应收比较,即便日期、客户、单号完全相同,仍会出现差额。对账前应先确认比较的是产品金额、含税金额还是优惠后的最终金额,不能只看字段名称中都有一个“金额”。
这张单已经完成了订单对应的出库,却仍满足待收款条件。反过来,订单上即使有订金,也不能把订金字段直接当作出库净额的一部分;预收、履约、核销需要各自明确关联。
数据模型也保留了这种分工:订单与出库通过order_id关联,退货也关联订单;收款单下面还有收款项,收款项通过业务类型与业务ID指向销售出库或销售退货。
▲ 虚线表示由biz_type与biz_id决定的业务关联,不是一个同时指向两张表的物理外键。收款分配关系用于解释源码模型,不代表本次有完整收退款样本。
为什么收款要多一层明细?因为一次收款可能分配给多张出库单,一张出库单也可能分多次结清。总账式地给客户记一个金额,无法准确回答某张业务单还欠多少。
两笔未出库,怎样解释68,682.53的差额
把视角从一张单拉回本次开发演示数据。固定租户、未删除数据和2026年起的业务时间,读取到:
| 核对项 | 金额或数量 |
|---|---|
| 已审核销售订单 | 16笔,1,266,241.84 |
| 销售出库 | 14笔,1,197,559.31 |
| 尚未出库的已审核订单 | 2笔,68,682.53 |
两张待出库订单金额分别是40,675.48和28,007.05,出库数量均为0。于是这份样本中有一条可核对的等式:
1,266,241.84 − 1,197,559.31 = 40,675.48 + 28,007.05 = 68,682.53。
这说明差额来自尚未兑现的订单,而不是首页少算了两张单。本次样本的出库均为已审核,且没有销售退货,因此看板净额恰好等于出库合计。另有两笔未审核订单,不包含在上表的“已审核订单”里。
这条等式只解释当前样本,不能升级成所有企业的固定公式。部分出库、订单变更、跨期履约、折扣及退货,都可能让“订单额减出库额”不再直接等于剩余订单的整单金额。
实现统计时应先明确要核对的集合,再找差额原因。直接要求订单列表合计必须等于首页销售,往往会把两个本来合理的数字一起改错。
退货扣在哪一期,要看退货事实的日期
当前销售统计分别汇总出库与退货,然后相减。下面摘录实际MyBatis查询的金额与日期部分;为突出主线,省略两处分支中原有的可信租户上下文条件,实际使用时必须保留租户限制。
<selectid="getSalePrice"resultType="java.math.BigDecimal">SELECT (SELECT COALESCE(SUM(total_price), 0) FROM erp_sale_out WHERE out_time >= #{beginTime}<iftest="endTime != null">AND out_time<#{endTime}</if>AND deleted = 0) - (SELECT COALESCE(SUM(total_price), 0) FROM erp_sale_return WHERE return_time >= #{beginTime}<iftest="endTime != null">AND return_time<#{endTime}</if>AND deleted = 0)</select>出库使用out_time,退货使用return_time。按这个定义,上月出库、本月退货,会影响本月净额,并不会因为关联的订单在上个月,就自动回改上个月的统计。
如果某个月退货金额大于当月出库,按发生期计算的净额可以为负。这不是一定要被前端修成0的异常值,它可能反映前期业务在本期集中退回。若管理上需要同时观察正向规模与退货压力,可以分别展示出库额、退货额和净额,不能让一个被截断的数字掩盖负向业务。
两个集合分别聚合,还有一个实际好处:避免把订单、出库明细、收款项一起JOIN后再SUM主单金额。一张出库有三行产品、两条收款分配时,未经去重的关联可能把金额重复累计。指标需要哪个事实,就在正确粒度上先计算哪个事实。
采购方向与此对称:按in_time汇总采购入库,再按return_time扣除采购退货。采购订单额不直接当作采购入库净额,付款金额也不混入该公式。
▲ 金额走出库与退货的期间汇总,待办走当前状态判断。它们可以由首页统一返回,但仍保留各自的来源与范围。
这里还有一个现状边界:当前销售、采购净额SQL未显式增加审核状态过滤,不能将其介绍为“仅已审核口径”。本次出库样本恰好都已审核,不能证明未审核数据一定不会计入。若业务要求审核后才进入经营指标,应同步调整统计与核对规则,而不能只修改卡片名称。
另外,“出库减退货”是这里的业务净额,不能直接等同于会计收入确认或利润。
待收款是找单据,实际收款是看分配
首页判断待出库与待收款,使用的是两个不同字段关系。下面是实际实现的核心条件:
privatelongcountPendingOut(){returnnvl(saleOrderMapper.selectCount(newLambdaQueryWrapperX<ErpSaleOrderDO>().eq(ErpSaleOrderDO::getStatus,ErpAuditStatus.APPROVE.getStatus()).apply("out_count < total_count")));}privatelongcountPendingReceipt(){returnnvl(saleOutMapper.selectCount(newLambdaQueryWrapperX<ErpSaleOutDO>().eq(ErpSaleOutDO::getStatus,ErpAuditStatus.APPROVE.getStatus()).apply("receipt_price < total_price")));}第一段数的是尚未出完的已审核订单,第二段数的是尚未结清的已审核出库单。一张欠100的单和一张欠10万的单,在“待收款笔数”里都只算一笔。
本次开发样本符合待收款条件的出库单是7笔,对应total_price - receipt_price合计1,077,643.71。这是业务单余额核对结果,不是首页已经展示的待收款金额,更不是银行账户余额;当前开发样本未找到相应收款单及收款项,不能据此验证完整资金流水。
收款服务则需要识别分配对象。实际校验中,销售出库取正向应收,销售退货取负向金额,并检查客户一致:
returnconvertList(list,o->BeanUtils.toBean(o,ErpFinanceReceiptItemDO.class,item->{if(ObjectUtil.equal(item.getBizType(),ErpBizTypeEnum.SALE_OUT.getType())){ErpSaleOutDOout=saleOutService.validateSaleOut(item.getBizId());Assert.equals(out.getCustomerId(),customerId,"客户必须相同");item.setTotalPrice(out.getTotalPrice()).setBizNo(out.getNo());}elseif(ObjectUtil.equal(item.getBizType(),ErpBizTypeEnum.SALE_RETURN.getType())){ErpSaleReturnDOret=saleReturnService.validateSaleReturn(item.getBizId());Assert.equals(ret.getCustomerId(),customerId,"客户必须相同");item.setTotalPrice(ret.getTotalPrice().negate()).setBizNo(ret.getNo());}else{thrownewIllegalArgumentException("业务类型不正确:"+item.getBizType());}}));业务类型与业务ID共同决定查哪张单,单号是可读标识;后端从业务单重新取应收值,并验证客户一致,避免仅凭前端传来的金额完成分配。源码中收款单的合计来自分配项之和,实付字段还会扣除优惠金额,因此“核销了多少应收”和“本次实际收了多少”也需要区分,不能让优惠悄悄消失在总数里。
不能把退货本身录成负数后,又不检查约定地再次取负,否则方向会反转。金额符号、优惠金额与实际收付金额的关系,需要作为结算规则统一定义。
当前实现还会在创建收款单及明细后更新业务单的累计收款字段,而收款单初始状态是未审核;累计查询也没有关联收款主单审核状态。因此,业务单上的“已收金额”不能自动解释成已审核、已银行对账的现金到账额。若要做资金日报,应另按收付款发生时间、有效状态和账户核对,不能直接把这个累计字段求和代替期间现金流。
采购与付款,也要分别看履约和结算
采购侧使用同样的分工:入库表达货物已接收,付款表达对供应商的结算。当前看板的待付款条件,是已审核采购入库单的payment_price < total_price,数的是单据笔数;不能拿它与本期采购净额直接相减。
付款服务按采购入库或采购退货识别分配对象,校验供应商一致,采购退货金额取负向参与分配。创建付款单后也会更新采购业务单累计,因而“已付”字段同样不自动等于已审核、已银行对账的期间付款。
要做现金收支分析,应从收付款单据的发生时间和有效状态出发,再追溯其分配项;要看采购履约规模,则从入库与退货出发。统一页面可以共用组件和筛选表达,不能共用一个含义不明的金额总数。本次采购入库合计999,033.00与看板一致,付款回写规则依据源码核对,未另行执行付款实验。
日期相同,也不一定在比较同一个期间
首页期间解析同时返回本期与对比期,关键分支如下,省略时间起点的取得:
returnswitch(period){case"today"->newLocalDateTime[]{today,null,LocalDateTimeUtils.getYesterday(),today};case"year"->newLocalDateTime[]{year,null,year.minusYears(1),year};default->newLocalDateTime[]{month,null,LocalDateTimeUtils.beginOfMonth(month.minusMonths(1)),month};};本期结束传null时,SQL不增加上界,严格说是“从本期起点开始”,不是由数据库条件保证“截至此刻”。若存在未来日期单据,也需要明确是否计入。对比期则使用结束边界;本月尚未结束时直接对比完整上月,也不等于相同已过天数的比较。
业务若需要截至某个时点,就应显式传入截止时间;需要同进度同比或环比,则计算同进度的对比区间。固定期间可采用左闭右开的[开始, 下一期间开始),避免对月末最后一秒及小数精度作隐含假设。
让看板可解释,关键在于把每个数字的来源、日期、状态、方向和单位一起交代清楚。订单增长、出库增长、未结清单据增加,分别提示接单、履约和结算上的不同变化。把这些事实正确连接起来,企业管理一体化平台才能同时支持业务办理与经营判断。