以下情境为教学假设,人物与数字不代表经核实的真实案例。
3.1 节把应收余额换算成了现金占用天数。LedgerLite 的 12,000 美元应收是 18.2 天收入,Nord Studio 的 11,264 美元是 26.6 天收入。两个数字都只回答“占用了多少”,没有回答“这些钱有多老”。同样是一万美元应收,全部在 30 天内和一半超过 90 天,现金占用一样,风险完全不一样。
账龄表就是把余额按年龄拆开。它不改变应收总额,只改变你看待总额的方式。这一节先建立四个账龄档和每档的准备比例,再用两个案例算出分档准备金额,最后解释为什么“余额乘一个统一比例”会系统性地低估风险。
余额只有一个数,账龄是一张结构图
应收账龄表通常按 30 天一段切分:0–30 天、31–60 天、61–90 天、90 天以上。每一档记录的是“这个年龄区间里还有多少应收”,而不是“这个区间里发生了多少收入”。所有档位相加必须等于应收总额,否则表就漏了钱。
| 档位 | 含义 | 常见状态 | 本节准备率(教学假设) |
|---|---|---|---|
| 0–30 天 | 未到期或刚到期 | 正常账期内 | 1% |
| 31–60 天 | 逾期 1 个月左右 | 可能只是付款流程慢 | 5% |
| 61–90 天 | 逾期 2 个月 | 需要主动催收 | 20% |
| 90 天以上 | 逾期 3 个月以上 | 争议、失联或财务困难 | 50% |
四个准备率是本节的教学假设,不是会计准则,也不是任何行业的统计坏账率。它们的用途是建立一条可复算的曲线:账龄越老,每一美元的准备越高。真实比例要按客户结构、历史回收率和合同条款调整;调整时,整张表一起重算。
账龄表和应收余额的关系,可以写成三个公式:
逾期金额 = 应收余额 × 逾期比例
某档金额 = 逾期金额 × 该档在逾期中的占比
坏账准备 = Σ(某档金额 × 该档准备率)
关键在第二行。只有应收余额和逾期比例,还算不出准备;必须知道逾期金额在 31–60、61–90、90+ 三档之间怎么分布。这个分布就是账龄表真正多出来的信息。
两张账龄表:同样的余额,不同的年龄
LedgerLite 的应收是 12,000 美元,逾期 30 天以上占 6%,即 720 美元。Nord Studio 的应收是 11,264 美元,逾期占 8%,即 901.12 美元。两家都没有给出逾期金额在三档之间的拆分,本节采用一个统一的教学假设:逾期金额按 60%、25%、15% 分到 31–60、61–90、90+。依据是多数逾期会在两个月内收回,90 天以上只是尾部;这个拆分不是案例事实,换一个拆分,准备金额会变。
LedgerLite 的账龄表如下。
| 档位 | 金额 | 占应收 | 准备率 | 准备金额 |
|---|---|---|---|---|
| 0–30 天 | $11,280.00 | 94.0% | 1% | $112.80 |
| 31–60 天 | $432.00 | 3.6% | 5% | $21.60 |
| 61–90 天 | $180.00 | 1.5% | 20% | $36.00 |
| 90 天以上 | $108.00 | 0.9% | 50% | $54.00 |
| 合计 | $12,000.00 | 100% | — | $224.40 |
Nord Studio 的账龄表如下。
| 档位 | 金额 | 占应收 | 准备率 | 准备金额 |
|---|---|---|---|---|
| 0–30 天 | $10,362.88 | 92.0% | 1% | $103.63 |
| 31–60 天 | $540.67 | 4.8% | 5% | $27.03 |
| 61–90 天 | $225.28 | 2.0% | 20% | $45.06 |
| 90 天以上 | $135.17 | 1.2% | 50% | $67.59 |
| 合计 | $11,264.00 | 100% | — | $243.31 |
两家的应收只差 736 美元,准备金额却差 18.91 美元,准备率分别是 1.87% 和 2.16%。差额不是来自余额,而是来自年龄结构:Nord 的逾期比例更高,90+ 档的金额更大。
更有意思的是 90+ 档的“以小博大”。LedgerLite 的 90+ 只占应收的 0.9%,却贡献了 54.00 ÷ 224.40 = 24.1% 的准备;Nord 的 90+ 占 1.2%,贡献了 67.59 ÷ 243.31 = 27.8% 的准备。余额上看起来是零头,准备上却是四分之一。这就是账龄表要解决的问题:余额告诉你规模,账龄告诉你哪一部分在吃掉利润。
统一比例为什么会低估风险
如果只看案例事实里的“坏账准备 1%”和“2%”,会得到两个很整齐的数字:LedgerLite 12,000 × 1% = 120 美元,Nord 11,264 × 2% = 225.28 美元。分档计算的结果是 224.40 美元和 243.31 美元,分别高出 104.40 美元和 18.03 美元。
| 口径 | LedgerLite | Nord Studio | 差额来源 |
|---|---|---|---|
| 统一比例 | $120.00(1%) | $225.28(2%) | 把所有应收视为同质 |
| 账龄分档 | $224.40(1.87%) | $243.31(2.16%) | 90+ 档按 50% 计提 |
| 差额 | +$104.40 | +$18.03 | 逾期尾部 |
统一比例不是错,它是一种简化。问题在于它隐含了一个假设:所有应收的损失概率相同。账龄表显示这个假设不成立。LedgerLite 的 90+ 只有 108 美元,按 50% 计提是 54 美元;如果按 1% 统一计提,这 108 美元只准备 1.08 美元,相差近 50 倍。余额越小,统一比例越容易把尾部风险抹平。
反过来,统一比例也可能高估。如果一家公司的应收全部在 0–30 天,90+ 为零,那么 1% 的统一比例和分档结果完全一致;如果 0–30 档的实际损失率低于 1%,统一比例还会略微高估。账龄表的价值不是永远算出更大的数字,而是让准备金额跟着年龄结构走,而不是跟着一个拍脑袋的平均数走。
还有一层容易被忽略:准备金额是流动的。这个月为 90+ 计提的 54 美元,如果下个月客户付清,准备要反向冲回,利润表上会出现一笔非现金收益;反过来,一笔 0–30 的应收如果突然进入争议,下个月可能直接跳到 61–90,准备率从 1% 变成 20%。账龄表的每一格都在移动,准备不是一次算完的静态数字,而是跟着格子的移动每月重算。只看年度合计,会错过这种移动。
因此,账龄表的月度对比比单月快照更有信息量。单月表告诉你“现在有多老”,连续三个月的表告诉你“正在变老还是正在变好”;后者才是准备是否足够的依据。
这里要区分三个概念。逾期比例是“有多少钱晚于约定日期”,案例里的 6% 和 8% 属于这一类;准备率是“每一档预计收不回来多少”,1%/5%/20%/50% 属于这一类;核销是“确认某笔钱再也收不回来,把它从应收里拿掉”。逾期不等于坏账,准备也不等于核销。核销的会计处理与追回后的反转,见 4.1 坏账准备:计提、核销与转回 ;本节只处理准备的计算。
读账龄表的四个比率
一张账龄表不需要很多指标,四个比率就能看出结构。
| 比率 | 公式 | LedgerLite | Nord Studio | 读法 |
|---|---|---|---|---|
| 当前占比 | 0–30 ÷ 应收 | 94.0% | 92.0% | 越高越健康 |
| 逾期占比 | 31+ ÷ 应收 | 6.0% | 8.0% | 案例事实 |
| 90+ 占比 | 90+ ÷ 应收 | 0.9% | 1.2% | 尾部有多重 |
| 准备率 | 准备 ÷ 应收 | 1.87% | 2.16% | 年龄结构的综合价格 |
第一个比率看整体健康度。LedgerLite 的 94% 在 30 天内,Nord 的 92%,两家都不算差。第二个比率是案例已经给出的逾期率,LedgerLite 6%,Nord 8%。第三个比率把尾部单独拎出来:0.9% 和 1.2% 看起来都很小,但乘上 50% 的准备率之后,就是 24% 和 28% 的准备贡献。
第四个比率是准备率,它把前三个比率合成一个数。LedgerLite 1.87%,Nord 2.16%。如果只看逾期率,Nord 比 LedgerLite 高 2 个百分点;如果只看准备率,Nord 比 LedgerLite 高 0.29 个百分点。差额被当前档的 1% 稀释了:Nord 的应收里有 92% 是当前的,这部分只按 1% 计提,把整体准备率拉低。
这也解释了为什么账龄表要按月更新。客户今天还在 0–30 档,下个月可能进入 31–60 档;一笔 90+ 的应收收回后,准备要反转,整体准备率会下降。账龄表是快照,不是永久标签。按月滚动,才能看到“逾期在往哪个方向走”,而不是只知道“这个月有多少逾期”。
把账龄表写成代码
下面的 aging.ts 把账龄分档和准备计算固化成一条链路:输入应收余额、逾期比例和逾期拆分,输出四档金额、每档准备、总准备、准备率,以及准备对逾期的覆盖率。金额用整数美分,比例用基点,任何一档的金额加不回去都会被拒绝。
// file: src/contract/aging.ts
// tier: L1
// verify: node --test test/contract/aging.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type AgingBucket = "0-30" | "31-60" | "61-90" | "90+";
export const BUCKET_ORDER: AgingBucket[] = ["0-30", "31-60", "61-90", "90+"];
/** Teaching assumption, not an accounting standard or observed default rate. */
export const PROVISION_RATES: Record<AgingBucket, Bps> = {
"0-30": 100,
"31-60": 500,
"61-90": 2000,
"90+": 5000,
};
export type AgingRow = {
bucket: AgingBucket;
amount: Cents;
rateBps: Bps;
provision: Cents;
};
export type AgingReport = {
receivables: Cents;
current: Cents;
overdue: Cents;
rows: AgingRow[];
provision: Cents;
provisionRateBps: Bps;
coverageOfOverdueBps: Bps;
};
export type AgingInput = {
receivables: Cents;
overdueBps: Bps;
/** Split of overdue across 31-60, 61-90 and 90+, in bps. */
overdueSplitBps?: [Bps, Bps, Bps];
};
export const DEFAULT_OVERDUE_SPLIT: [Bps, Bps, Bps] = [6000, 2500, 1500];
/**
* An aging table is the age structure of the balance, not the balance itself.
* The overdue amount is split across three buckets, and the 90+ bucket takes
* the rounding remainder so the rows always add back to the total.
*/
export function buildAging(input: AgingInput): Result<AgingReport> {
if (!Number.isInteger(input.receivables) || input.receivables < 0) {
return fail("receivables must be a non-negative integer");
}
if (
!Number.isInteger(input.overdueBps) ||
input.overdueBps < 0 ||
input.overdueBps > 10000
) {
return fail("overdue share must be an integer between zero and 10000 bps");
}
const split = input.overdueSplitBps ?? DEFAULT_OVERDUE_SPLIT;
if (
split.length !== 3 ||
split.some((part) => !Number.isInteger(part) || part < 0) ||
split[0] + split[1] + split[2] !== 10000
) {
return fail("overdue split must be three non-negative bps summing to 10000");
}
const overdue = applyRate(input.receivables, input.overdueBps);
const current = input.receivables - overdue;
const first = applyRate(overdue, split[0]);
const second = applyRate(overdue, split[1]);
const third = overdue - first - second;
const amounts: Record<AgingBucket, Cents> = {
"0-30": current,
"31-60": first,
"61-90": second,
"90+": third,
};
const rows: AgingRow[] = BUCKET_ORDER.map((bucket) => ({
bucket,
amount: amounts[bucket],
rateBps: PROVISION_RATES[bucket],
provision: applyRate(amounts[bucket], PROVISION_RATES[bucket]),
}));
const provision = rows.reduce((total, row) => total + row.provision, 0);
return ok({
receivables: input.receivables,
current,
overdue,
rows,
provision,
provisionRateBps: divide(provision, input.receivables),
coverageOfOverdueBps: divide(provision, overdue),
});
}
把 LedgerLite 的 1,200,000 美分和 600 基点代入,得到逾期 72,000 美分,四档金额 1,128,000、43,200、18,000、10,800 美分,四档准备 11,280、2,160、3,600、5,400 美分,总准备 22,440 美分,准备率 187 基点(1.87%)。Nord 的 1,126,400 美分和 800 基点得到逾期 90,112 美分,四档金额 1,036,288、54,067、22,528、13,517 美分,四档准备 10,363、2,703、4,506、6,759 美分,总准备 24,331 美分,准备率 216 基点(2.16%)。这些数字与上表逐行对应,只是单位从美元换成了美分。
代码里有三个刻意的设计。第一,90+ 档取舍入余数,而不是独立四舍五入;否则四档金额相加可能差一两美分,账龄表就对不上余额。第二,准备率放在一个常量里,改政策只改一处,不会出现四档比例互相矛盾。第三,同时返回准备率和逾期覆盖率:前者回答“整体准备够不够”,后者回答“逾期部分被覆盖了多少”,两个问题不能用一个数字回答。
账龄表最终会进入资产负债表的应收净额:应收总额减去坏账准备,才是报表上的净应收。这个减法在 3.3 资产负债表 里出现过,本节不重复。下一节把账龄表向前推一步:账龄是怎么形成的,开票、对账和催收的每个节点如何改变现金到账时间。
小结
- 账龄表是应收余额的年龄结构,不是余额本身;四档相加必须等于应收总额。
- 本节采用 0–30、31–60、61–90、90+ 四档,准备率 1%/5%/20%/50% 是教学假设,不是统计坏账率。
- LedgerLite 的 12,000 美元应收中,逾期 720 美元,分档准备合计 224.40 美元,准备率 1.87%。
- Nord Studio 的 11,264 美元应收中,逾期 901.12 美元,分档准备合计 243.31 美元,准备率 2.16%。
- 统一比例口径给出 120 美元和 225.28 美元,低于分档结果;差额来自 90+ 档的 50% 准备。
- 90+ 档只占应收的 0.9% 和 1.2%,却贡献了 24.1% 和 27.8% 的准备;尾部风险不能按余额占比看。
- 四个比率足够读表:当前占比、逾期占比、90+ 占比、准备率;账龄表要按月滚动,不能当永久标签。
阅读导航:上一节:3.1 账期的真实成本:Net 15、30、60 差多少 · 下一节:3.3 开票、对账与催收节奏 。
常见错误与边界
- 把账龄表当成应收余额表。 余额表只有一行合计;账龄表要回答“这些钱分别有多老”,四档相加必须回到余额。
- 用一个统一比例计提所有应收。 LedgerLite 的 1% 统一比例给出 120 美元,分档给出 224.40 美元;差额 104.40 美元就是 90+ 尾部被抹平的部分。
- 把逾期率当坏账率。 6% 和 8% 是晚于约定日期的应收占比,不是最终损失比例;损失要看分档准备率与回收情况。
- 按发票日期而不是到期日分档。 账龄应按“到期后多少天”计算;按开票日会把长账期合同误判为老应收。
- 忘记预付款和贷项。 客户预付、贷项通知单和部分退款都会改变余额的年龄结构;直接拿开票总额分档会高估。
- 只更新余额,不更新账龄。 每月余额相同,可能是一批老账收回、一批新账变老;账龄结构已经完全不同。
- 把准备当成现金支出。 坏账准备是会计估计,减少账面利润和应收净额,不减少银行余额;现金影响发生在核销或收回时。
- 把 90+ 当成必然全损。 50% 是教学假设,不是“90 天以上一定收不回”;有争议但仍在沟通的应收,回收率可能高于这个比例。
- 忽略客户集中度。 一笔 90+ 的大客户应收,可能比十笔小客户更值得单独评估;集中度见 7.2 客户集中度 。
- 账龄表只做一次。 账龄是流动的;一个月不更新,表上的“当前”可能已经是上个月的“逾期”。
本节的边界是“给定逾期比例与逾期拆分”的静态分档。它假设每档内部的损失概率相同,不含客户信用差异、争议状态和回收努力。它足够回答“这笔余额该准备多少”,但不足以回答“这笔钱什么时候能收回来”。后者要把账龄表还原成一张回款时间分布,也就是下一节的开票、对账与催收节奏。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。