SpringBoot3+Vue3 固定资产折旧计提:平均年限法、月度批次与净值回写
2026/8/27 8:38:24 网站建设 项目流程

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」)

一句话:折旧计提要验收的不是「能不能除出月折旧」,而是试算、审批、过账三跳分开。RuoYi Office 生成批次只写计提行,卡片净值不动;Flowable 通过后才回写累计折旧和净值。同一期间算错了,走冲销单把累计减回去,而不是改历史行。

▲ 左列 StraightLineCalculator:残值、应计总额、月折旧、末月夹紧;右列 generate 试算 → 审批期间卡片不动 → onProcessApproved 过账。底部是进池条件与冲销独立 bizType


引言:财务最怕的是点一次按钮就把净值改了

资产全生命周期里,入库、领用、盘点都有单据。折旧却常被做成「月底跑个 SQL 直接 update 卡片」。现场炸掉的不是公式,是这三件事缠在一起:

现场原话直接改卡片的后果
「这个月不该提,提了怎么退」没有冲销单,只能手工改累计,对不上凭证
「审批还在走,净值已经少了一截」试算和过账同一跳,驳回了数回不去
「同一台电脑这个月提了两次」没有「公司 + 期间」唯一在途单,对账只能翻日志

资产模块总览、领用调拨报废,已有专文。本文只写折旧计算器 + 月度批次 + 审批回写:平均年限法怎么夹紧残值、generate 跳过哪些卡片、过账写哪些字段、冲销如何把累计减回去。

技术栈锚点(2026):Spring Boot 3.5 + Java 17,PC 为 Vue3 折旧计提单详情,审批走 Flowable,金额一律BigDecimal两位HALF_UP


一、计提单是账,卡片是余额

结论:资产卡片上的累计折旧和净值,是过账结果,不是试算结果。

一次月度折旧拆成三张东西:

对象何时写入能否当余额用
计提批次头点「生成」否,只汇总期间、金额、过账状态
计提明细行点「生成」否,是建议分录
资产卡片财务字段审批通过postAccrual,下游盘点、处置都读它

同一公司同一期间,只允许一张可编辑的在途计提单。已存在且不可编辑(已在审、已过账)再生成,直接拒绝。草稿则删行重算,不算另开一张单。

冲销是第二种bizType,不是在原单上把金额改成负数。原单过账后才能冲;冲销单自己再走一遍审批,通过后把卡片累计减回去,原单过账状态改为已冲销。两边单据号互相写,对账不用猜。


二、平均年限法:月折旧,末月夹紧

结论:月折旧 = (原值 − 原值×残值率) ÷ 使用年限(月);本期再和剩余应计额取小。

DepreciationCalculator只暴露一个supports(method)和一个calculate(...)。当前实现是平均年限(直线法),方法值1。换工作量法或双倍余额递减,加一个 Calculator Bean 即可,批次服务只resolveCalculator

publicBigDecimalcalculate(BigDecimaloriginalValue,BigDecimalresidualRate,IntegerusefulLife,BigDecimalaccumulatedBefore){if(originalValue==null||usefulLife==null||usefulLife<=0){returnBigDecimal.ZERO;}BigDecimalrate=residualRate!=null?residualRate:BigDecimal.ZERO;BigDecimalresidualValue=originalValue.multiply(rate).divide(BigDecimal.valueOf(100),2,RoundingMode.HALF_UP);BigDecimaldepreciable=originalValue.subtract(residualValue).max(BigDecimal.ZERO);BigDecimalmonthly=depreciable.divide(BigDecimal.valueOf(usefulLife),2,RoundingMode.HALF_UP);BigDecimalaccumulated=accumulatedBefore!=null?accumulatedBefore:BigDecimal.ZERO;BigDecimalremaining=depreciable.subtract(accumulated).max(BigDecimal.ZERO);returnmonthly.min(remaining).max(BigDecimal.ZERO);}

几个容易踩的口径:

  • 残值率是百分数。卡片上写5,表示 5%,不是 0.05。计算器里要先/100
  • 使用年限是月。36 表示三年,不要再乘 12。
  • 末月夹紧monthly.min(remaining)保证累计不会超过「原值 − 残值」,净值不会打穿残值。
  • 算出来 ≤ 0 不进明细。已提满、年限非法、原值为空,计入skipCount,不生成 0 元行干扰对账。

用卡片上的真实数验一笔。演示资产MOCK-CS-006华为 MateBook D16:原值 6499,残值率 5%,预计残值 325,年限 36 月,累计折旧 1300,净值 5199。

应计总额 = 6499 − 325 = 6174;月折旧 = 6174 ÷ 36 ≈ 171.50;剩余应计 = 6174 − 1300 = 4874。本期取min(171.50, 4874),还没到末月,不会夹紧。净值 6499 − 1300 = 5199,和卡片财务信息一致。

▲ MOCK-CS-006 华为 MateBook D16,财务信息页签。原值 6499、累计折旧 1300、净值 5199、残值率 5%、预计残值 325、平均年限法、使用年限 36 月、折旧起算 2024-12-01。净值 = 原值 − 累计,计算器吃的就是这些字段

折旧起算日决定这张卡何时进池:未报废、起算日 ≤ 期间末日、年限 > 0。维修中仍计提——演示这台电脑状态是维修中,财务字段照样在。报废才排除。


三、PC 端:看明细行,不看空列表

结论:计提单详情必须同时看见期间合计和每张卡的期初 / 本期 / 期末。

3.1 计提单:13 行、合计 2479.38、过账后被冲销

演示单AM708-2026080200003是 2026-07 期间的折旧计提,13 条明细,合计 2479.38,审批通过,过账状态已冲销——它已经被后面那张冲销单处理过。

▲ 单据 AM708-2026080200003。表头期间 2026-07、数量 13、合计 2479.38、单据类型折旧计提、过账状态已冲销。明细能看到原值、计提前净值、本期折旧、期末净值;第一行 ZC20260728-0001 原值 800.00,本期 12.67,期末净值 787.33

明细列是对账口:期初累计、计提前净值、本期折旧、期末累计、期末净值。生成时一行都不能少,审批过程只读。财务要问「这 12.67 怎么来的」,回到卡片原值 / 残值率 / 年限,用上一节公式能手算出来。

3.2 冲销单:原单号、同样金额、过账状态已过账

AM708-2026080200004的单据类型是折旧冲销,原计提单号指向AM708-2026080200003,期间仍是 2026-07,合计仍是 2479.38,过账状态已过账。备注写明「冲销原单」。

▲ 单据 AM708-2026080200004。单据类型折旧冲销、原计提单号 AM708-2026080200003、期间 2026-07、合计 2479.38、已过账。下方「来自原计提单」明细与原单同一批资产、同一套金额,冲销过账时按本期折旧把卡片累计减回去

两张单不要合成一张「红字行」。计提过账改余额,冲销过账再改回来,凭证和卡片都留痕迹。原单不允许再生成计提,也不允许对冲销单再冲销。


四、生成:试算进行,跳过要计数

结论:generate的职责是算出建议分录,不是改卡片。

进池条件在 Mapper:asset_status ≠ 已报废,折旧起算日非空且 ≤ 期间末日,使用年限 > 0,可按公司过滤。

试算循环里还要再跳三类:本期这张卡已有计提行、找不到对应 Calculator、calculate得到空或 ≤ 0。跳过的张数进skipCount,进明细的张数进totalCount,金额加总进totalAmount。一批全跳过则拒绝生成空单。

TrialResulttrial=trialCalculate(reqVO.getCompanyId(),reqVO.getDepreciationPeriod(),batchId);if(CollUtil.isEmpty(trial.lines)){throwexception(ASSET_DEPRECIATION_BATCH_EMPTY);}recordMapper.insertBatch(trial.lines);AssetDepreciationBatchDOtotals=newAssetDepreciationBatchDO();totals.setId(batchId);totals.setTotalCount(trial.lines.size());totals.setTotalAmount(trial.totalAmount);totals.setSkipCount(trial.skipCount);batchMapper.updateById(totals);

明细行把「计提前」和「计提后」都存下来:accumulatedBefore/accumulatedAfter/cardNetValueBefore/netValue/depreciationAmount。过账时不再重算一遍,避免审批期间有人改了卡片年限,过账数和审批看到的对不上。


五、过账:审批通过才碰卡片

结论:onProcessApproved是唯一允许改卡片累计折旧和净值的入口。

publicvoidonProcessApproved(StringbusinessKey){AssetDepreciationBatchDOhead=validateExists(Long.parseLong(businessKey));if(Objects.equals(head.getPostStatus(),POST_POSTED)){return;// 已过账,审批回调重入直接返回}if(Objects.equals(head.getBizType(),BIZ_REVERSE)){postReverse(head);}else{postAccrual(head);}}

计提过账逐行:

  1. 卡片accumulatedDepreciation = line.accumulatedAfter
  2. 卡片netValue = line.netValue
  3. 写一条资产日志,内容带期间、本期、累计前→后、净值
  4. 批次postStatus = 已过账

冲销过账反向:用原计提行的depreciationAmount去减卡片累计,净值按原值 − 新累计回写;原单标成已冲销并互写单号;冲销单自己标已过账。财务凭证如果接了 SPI,冲销时带上原凭证号,失败只打 warn,不回滚卡片——卡片对账优先于外部凭证。

过账状态三态:0未过账、1已过账、2已冲销。审批状态和过账状态分开,驳回不会误把已过账改回去;已过账的回调重入直接 return,避免重复减净值。


六、设计决策与边界

决策点方案理由
试算与过账分离generate 只写行审批中净值不能先变
期间唯一在途公司 + 期间一把锁防止同月提两次
草稿可重算删行再 trial改残值率后不用作废单号
计算器可插拔supports(method)先落地平均年限,不堵后续方法
年限用月usefulLife直接做除数避免「年×12」各写各的
残值率百分数除以 100和卡片展示5%一致
末月夹紧min(月折旧, 剩余应计)不打穿残值
冲销独立单据bizType+ 再审批红字可审计,不是改历史
过账幂等已过账则 return流程回调重入安全
跳过计数skipCount与明细分开空行不进账,漏提能查出原因

边界:

  • 维修中、闲置仍计提,只有报废排除。若现场「停用停提」,要改进池条件,不要在计算器里偷偷返回 0。
  • 本期已有行就 skip,依赖「一卡一期一行」。手工插行或跨公司重复卡号会让 skip 失真。
  • 财务推送是过账之后的可选动作,失败不影响卡片。对账先看卡片和计提行,再看凭证号。

七、数据结构要点

批次头:bill_codebiz_type(计提/冲销)、company_iddepreciation_periodyyyy-MM)、total_count/total_amount/skip_countprocess_statuspost_statussource_batch_id/reverse_batch_id(双向追溯)、财务推送三件套。

明细行:batch_idasset_idasset_codedepreciation_perioddepreciation_method、原值、期初累计、计提前净值、本期、期末累计、期末净值、line_status(有效/已冲销)。

卡片财务:原值、累计折旧、净值、残值率、预计残值、折旧方法、使用年限(月)、开始使用日、折旧起算日。计算器读原值 / 残值率 / 年限 / 已累计;过账写累计和净值。

金额字段全部两位小数。期间用YearMonth解析,期间末日拿来跟起算日比较,不要拿期间首日,否则月初购入当月可能进不了池。


八、技术亮点总结

设计要点实现方式价值
账条与余额分离行先写、卡片后写审批驳回不影响净值
平均年限按月应计总额 ÷ 月数和财务口径一致
末月夹紧min(月折旧, 剩余)不打穿残值
进池过滤未报废 + 起算日 + 年限报废资产不再提
跳过计数skipCount漏提可解释
期间唯一公司+期间在途锁同月不双提
过账幂等POST_POSTED 短路回调重入安全
冲销独立单bizType=2 再审批红字可审计
单号互写source / reverse两张单能对上
日志带数本期/累计前→后/净值卡片时间线能翻

九、快速体验

在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)

  1. 打开资产 → 资产台账,点进一台在用或维修中的设备,切到「财务信息」,记下原值、残值率、年限、累计、净值。
  2. 手算月折旧:(原值 − 原值×残值率/100) ÷ 年限,和下一张计提明细对一下本期金额。
  3. 打开折旧计提单AM708-2026080200003,看 2026-07、13 行、合计 2479.38、过账状态已冲销。
  4. 再打开AM708-2026080200004,确认单据类型是冲销、原单号指向 0003、已过账。
  5. 回到卡片,核对累计和净值是否已被冲销回写(演示环境以当前数为准)。
  6. 本地可再生成一个未过账草稿:改残值率后重新生成,确认卡片净值在审批通过前不变。

源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office


常见问题(FAQ)

生成折旧批次会立刻改卡片净值吗?

不会。generate只插入计提行并回写合计。卡片的累计折旧和净值只在审批通过的postAccrual里更新。

残值率填 5 是 5% 还是 0.05?

是 5%。计算器先除以 100 再乘原值。卡片财务信息展示为5%,预计残值应等于原值 × 5%。

为什么最后几个月的本期折旧比前面小?

末月夹紧。本期 = min(标准月折旧, 应计总额 − 已累计)。提满到残值之后再生成,calculate得到 0,这张卡会进 skipCount,不再出现 0 元明细。

已经过账的月份想作废,改原单行可以吗?

不可以。对已过账计提单发起冲销,生成独立冲销单再审批。通过后按原行本期金额减累计。冲销单不能再冲,原单过账状态变为已冲销。

维修中的资产还提不提?

提。进池只排除已报废,并要求折旧起算日已到、年限大于 0。停用停提要改过滤条件,不要指望计算器对维修状态返回 0。


结语

固定资产折旧的工程难点不在四则运算,而在让试算可驳回、让过账可对账、让冲销可审计。平均年限法把残值率和月数说清楚,末月夹紧保证净值不打穿残值;批次按公司+期间生成建议分录;Flowable 通过后才写卡片;算错了走另一张冲销单,而不是在历史行上改数字。

入库、领用、盘点解决资产在不在、谁在用。折旧解决这台资产这个月该少多少净值。两件事单据分开,财务才肯认系统里的数。

你们现在的折旧,是月底一条 SQL 直接 update,还是每张卡都能从计提行手算回卡片?欢迎在评论里对一下残值率和年限口径。


💡想要体验 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 支持一下!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询