以下情境为教学假设,人物与数字不代表经核实的真实案例。
第 3 章把三张历史报表接成了一个闭合的年度模型,第 4 章解释了收入为什么不能按收款确认。这两章回答的都是「过去一年发生了什么」。而一个人经营的公司最先遇到的危机不是年度利润为负,是某个月的某一天账户余额不够付工资、税款或云账单。年度报表看不见这一天,因为它把十二个月折成了一行。
13 周现金流表就是把这一行重新展开。它不预测未来一年,只把未来一个季度拆成 13 个格子,每个格子问两个问题:这一周会进多少钱,这一周必须出多少钱。答案越具体,越能提前发现缺口。本节先建立周表的口径,再用三个案例说明三种收入形态在周表里的不同形状。
为什么是 13 周而不是 12 个月
13 周约等于一个季度,但比「三个月」更适合做现金管理。原因是周与支付周期对齐:工资按周或双周发,平台按周或按月结算,信用卡和云服务按月扣款,税款按季度预缴。用月做格子,会把月末的几笔大额支出压成一个数字;用周做格子,才能看到哪一周同时撞上工资、税款和平台结算延迟。
| 视角 | 时间粒度 | 回答的问题 | 典型盲区 |
|---|---|---|---|
| 年度预算 | 12 个月 | 这一年能不能盈利 | 月度内的支付撞车 |
| 三张报表 | 季度或年度 | 利润有没有变成现金 | 具体哪一天缺钱 |
| 13 周现金流表 | 1 周 | 未来 13 周哪一周会低于水位 | 13 周之后的趋势 |
13 周还有一层行为上的意义:它足够短,短到每一格都敢写具体数字;又足够长,长到能覆盖一次完整的回款周期。LedgerLite 的账期是 Net 15,Tidecast 的平台月结 30 天,Nord Studio 的咨询是 Net 30。13 周能装下「开票、到期、到账」的完整链条,也能装下一次季度税款的到期日。
年度预算的方法论见 5.1 自上而下与自下而上的预算 ;本节不重复预算编制,只处理预算落地到周之后的现金节奏。两者的关系是:年度预算决定「花多少」,13 周表决定「哪一周花得出来」。
表的五个字段与四条口径
一张周表最少需要五个字段。字段少,才能每周更新;字段多,反而没人维护。
| 字段 | 含义 | 来源 | 常见错误 |
|---|---|---|---|
| 周次 | 第 1 到第 13 周 | 固定 | 用日期代替周次,跨月就乱 |
| 期初现金 | 上周期末结转 | 银行余额 | 把应收账款当现金 |
| 流入 | 本周确定到账的钱 | 合同、平台账单、发票 | 把「应该会付」写成「会付」 |
| 流出 | 本周必须付的钱 | 工资、税、供应商账期 | 漏掉季度性税款 |
| 期末现金 | 期初 + 流入 − 流出 | 计算 | 不做滚动,只做一次 |
四条口径决定了这张表能不能用。
第一,只放已签约或已发生的收付。潜在客户、口头承诺、还没开票的项目都不进表,它们属于销售预测,不属于现金预测。第二,按现金实际到账或付出的那一周记录,不按收入确认的那一周。第三,一次性项目单独成行,不和经常性收支混在一起,否则下一周无法判断基线。第四,期初现金必须来自银行对账单,不能来自账面现金,因为账面现金可能包含在途资金。
这四条听起来像会计常识,但它们决定了周表是「管理工具」还是「安慰剂」。一张把所有乐观回款都算进去的表,永远显示绿色,也就永远没用。
LedgerLite:订阅现金流的周表
LedgerLite 的现金结构最简单:260 家客户、ARPA 77 美元、MRR 20,020、ARR 240,240。其中 70% 是月付,30% 是年付并享受 15% 折扣。把年收入除以 52,每周的基础回款是 4,620 美元;再按付款方式拆开,月付部分每周 3,234,年付部分按季度续约、每次 18,018。
成本侧同样按年除以 52:变动成本 38,000 除以 52 是 731,运营费用 72,000 除以 52 是 1,385,创始人薪酬 60,000 除以 52 是 1,154,合计每周 3,270。所得税 11,941 不按周摊,而是作为一次性支出落在预缴周。
| 周 | 期初 | 流入 | 流出 | 净额 | 期末 |
|---|---|---|---|---|---|
| 1 | $95,000 | 3,234 | 3,270 | −$36 | $94,964 |
| 2 | 94,964 | 3,234 | 3,270 | −$36 | $94,928 |
| 3 | 94,928 | 3,234 | 3,270 | −$36 | $94,892 |
| 4 | 94,892 | 3,234 | 3,270 | −$36 | $94,856 |
| 5 | 94,856 | 3,234 | 3,270 | −$36 | $94,820 |
| 6 | 94,820 | 21,252 | 3,270 | $17,982 | $112,802 |
| 7 | 112,802 | 3,234 | 3,270 | −$36 | $112,766 |
| 8 | 112,766 | 3,234 | 3,270 | −$36 | $112,730 |
| 9 | 112,730 | 3,234 | 15,211 | −$11,977 | $100,753 |
| 10 | 100,753 | 3,234 | 3,270 | −$36 | $100,717 |
| 11 | 100,717 | 3,234 | 3,270 | −$36 | $100,681 |
| 12 | 100,681 | 3,234 | 3,270 | −$36 | $100,645 |
| 13 | 100,645 | 3,234 | 3,270 | −$36 | $100,609 |
这张表最值得看的是三个数。第一,常规周的净额是 −36,也就是说,只靠月付客户,LedgerLite 每周略微失血,全年靠年付续约的脉冲补回来。第二,第 6 周流入 21,252,是 3,234 的月付加 18,018 的年付续约,这一周把现金推到 112,802。第三,第 9 周流出 15,211,是常规 3,270 加税款 11,941,这一周把现金拉回 100,753。
最低点出现在第 5 周,94,820,正好在年付续约到账之前。这不是巧合:年付客户的续约日决定了订阅公司的现金节奏,续约周之前的几周永远是最紧的。如果年付比例从 30% 升到 40%,月付部分从 14,014 降到 12,012,常规周流入从 3,234 降到 2,772,周净额从 −36 变成 −498,最低点会降到 92,510;第 6 周的脉冲则从 18,018 升到 24,024。年付比例越高,续约前的坑越深,续约那周的峰越高。
1.5% 月流失在 13 周里几乎看不见
LedgerLite 的月流失率是 1.5%,收入口径,年化约 16.6%。把这个流失率折成周,每周约 0.35%。如果三个月不做任何新签,月付回款从 3,234 开始逐周衰减:
| 口径 | 13 周累计回款 | 与持平口径的差 |
|---|---|---|
| 假设零流失 | $42,042 | — |
| 按 1.5% 月流失逐周衰减 | $41,174 | −$868 |
| 第三个月的单月收入 | $19,133 | 比期初少 $887 |
13 周只少 868 美元,占累计回款的 2.1%。这正是周表的局限:它对「一次性撞车」极其敏感,对「缓慢侵蚀」几乎无感。1.5% 的月流失在一年里会吃掉 16.6% 的收入,但在一个季度里只留下 2% 的痕迹。所以周表必须和年度趋势一起看:周表负责发现「哪一周会断」,趋势负责发现「一年后还剩多少」。
Tidecast 与 Nord:脉冲与账期
同样是 13 周,Tidecast 和 Nord Studio 的表形状完全不同。Tidecast 是买断制,收入集中在发行月和 Q4;Nord Studio 是服务加产品,收入被账期切成两段。
Tidecast 的 13 周窗口从 11 月中旬到次年 2 月中旬,正好覆盖 Q4 高峰和 1 月的低谷。单份净得按标价 19.99、退款 4%、Steam 分成 30%、官网支付费 5% 计算,加权后每份约 14.39 美元。12 月销量 2,400 份,折每周 554 份;1 月 480 份,折每周 111 份。平台月结 30 天,所以 12 月的销售要到 1 月才变成现金。案例没有给出 Tidecast 的资产负债表,本节把可支配现金 92,090 当作期初水位,只用于演示 13 周的节奏。
| 周 | 期初 | 流入 | 流出 | 净额 | 期末 |
|---|---|---|---|---|---|
| 1 | $92,090 | 2,073 | 327 | $1,746 | $93,836 |
| 2 | 93,836 | 2,073 | 327 | $1,746 | $95,582 |
| 3 | 95,582 | 2,073 | 327 | $1,746 | $97,328 |
| 4 | 97,328 | 2,073 | 327 | $1,746 | $99,074 |
| 5 | 99,074 | 2,073 | 327 | $1,746 | $100,820 |
| 6 | 100,820 | 2,073 | 327 | $1,746 | $102,566 |
| 7 | 102,566 | 7,974 | 327 | $7,647 | $110,213 |
| 8 | 110,213 | 7,974 | 327 | $7,647 | $117,860 |
| 9 | 117,860 | 7,974 | 327 | $7,647 | $125,507 |
| 10 | 125,507 | 7,974 | 327 | $7,647 | $133,154 |
| 11 | 133,154 | 7,974 | 327 | $7,647 | $140,801 |
| 12 | 140,801 | 1,598 | 327 | $1,271 | $142,072 |
| 13 | 142,072 | 1,598 | 327 | $1,271 | $143,343 |
Tidecast 的表里,现金在前 6 周缓慢上升,第 7 到 11 周出现五个 7,974 的峰值,第 12 周开始回落到 1,598。峰值不是 12 月卖的,是 12 月卖、1 月到账的。13 周里现金从 92,090 涨到 143,343,增加 51,253,看起来很好;但第 13 周的周净额只有 1,271,年化后远低于 Q4 的水平。买断生意的周表永远在回答同一个问题:这一次脉冲之后,下一次脉冲在哪里。
Nord Studio 的表是另一种形状。咨询 110 小时、每小时 85 美元,月收入 9,350;产品 90 家、每月 39 美元,月收入 3,510;合计月收入 12,860。咨询按 Net 30 开票,产品按月收款,所以稳态下每周流入约 2,968。直接成本 24,000 和两人工资 96,000 按年除以 52,每周流出 2,308。Nord 同样没有给出期初现金,本节把可支配现金 25,740 当作起点。
| 周 | 期初 | 流入 | 流出 | 净额 | 期末 |
|---|---|---|---|---|---|
| 1 | $25,740 | 2,968 | 2,308 | $660 | $26,400 |
| 2 | 26,400 | 2,968 | 2,308 | $660 | $27,060 |
| 3 | 27,060 | 2,968 | 2,308 | $660 | $27,720 |
| 4 | 27,720 | 2,968 | 2,308 | $660 | $28,380 |
| 5 | 28,380 | 2,968 | 2,308 | $660 | $29,040 |
| 6 | 29,040 | 2,968 | 2,308 | $660 | $29,700 |
| 7 | 29,700 | 2,968 | 2,308 | $660 | $30,360 |
| 8 | 30,360 | 2,968 | 10,888 | −$7,920 | $22,440 |
| 9 | 22,440 | 2,968 | 2,308 | $660 | $23,100 |
| 10 | 23,100 | 2,968 | 2,308 | $660 | $23,760 |
| 11 | 23,760 | 2,968 | 2,308 | $660 | $24,420 |
| 12 | 24,420 | 2,968 | 2,308 | $660 | $25,080 |
| 13 | 25,080 | 2,968 | 2,308 | $660 | $25,740 |
Nord 的周表几乎是一条直线:每周 +660,第 8 周因为 8,580 的税款掉到 22,440,然后继续以每周 660 爬回 25,740。13 周结束时现金回到起点,等于说这个季度赚的钱刚好等于税。账期在稳态下看不出来,因为「本月开票、下月收款」的延迟被连续的项目抵消了;一旦停止接新单,第 13 周会少收一个月的咨询费 9,350。
| 案例 | 13 周现金变化 | 周净额形状 | 最低点 | 对周表的敏感点 |
|---|---|---|---|---|
| LedgerLite | +$5,609 | 平线加两次脉冲 | 第 5 周 $94,820 | 年付续约日 |
| Tidecast | +$51,253 | 长尾加五周峰值 | 期初 $92,090 | 平台结算延迟 |
| Nord Studio | $0 | 直线加一次税 | 第 8 周 $22,440 | 咨询项目连续性 |
三张表放在一起,13 周现金流表的价值就清楚了:它不是把年度利润除以 52,而是把每个案例最脆弱的那个时间点暴露出来。LedgerLite 脆弱在续约前,Tidecast 脆弱在脉冲后,Nord 脆弱在税周到账前。
把周表写成代码
下面的 cash13.ts 把周表固化成一条可复算的链路:输入期初现金和 13 组流入流出,输出每一行的期初、净额、期末,以及最低点所在的周。金额用整数美分,周数固定为 13,任何一周的流入流出不是整数都会被拒绝。
// file: src/cash/cash13.ts
// tier: L1
// verify: node --test test/cash/cash13.test.ts
import { type Cents, add, sub } from "../core/money.ts";
import { type Result, fail, ok } from "../core/result.ts";
export const WEEKS_PER_QUARTER = 13;
/** One week of planned receipts and payments, both in cents. */
export type WeekFlow = {
inflow: Cents;
outflow: Cents;
};
export type WeekRow = {
week: number;
opening: Cents;
inflow: Cents;
outflow: Cents;
net: Cents;
closing: Cents;
};
export type Cash13 = {
rows: WeekRow[];
totalInflow: Cents;
totalOutflow: Cents;
netChange: Cents;
openingCash: Cents;
closingCash: Cents;
minimumCash: Cents;
minimumWeek: number;
negativeFromWeek: number | null;
};
/**
* A 52-week year keeps the arithmetic reproducible from the case facts:
* LedgerLite ARR 240,240 / 52 = 4,620 per week exactly.
*/
export function weeklyFromAnnual(annual: Cents): Cents {
return Math.round(annual / 52);
}
export function weeklyFromMonthly(monthly: Cents): Cents {
return Math.round((monthly * 12) / 52);
}
function validate(openingCash: Cents, weeks: WeekFlow[]): Result<true> {
if (!Number.isInteger(openingCash)) {
return fail("opening cash must be an integer number of cents");
}
if (weeks.length !== WEEKS_PER_QUARTER) {
return fail(`a rolling cash table needs exactly ${WEEKS_PER_QUARTER} weeks`);
}
for (let i = 0; i < weeks.length; i += 1) {
const row = weeks[i];
if (!Number.isInteger(row.inflow)) {
return fail(`week ${i + 1} inflow must be an integer`);
}
if (!Number.isInteger(row.outflow)) {
return fail(`week ${i + 1} outflow must be an integer`);
}
}
return ok(true);
}
/**
* Rolling 13-week table. Each row closes into the next row's opening, so the
* closing balance is the only state that has to be carried forward. The
* minimum is tracked with the week it occurs: a table that never dips is not
* the same as a table that dips in week 13, even when both end positive.
*/
export function buildCash13(
openingCash: Cents,
weeks: WeekFlow[],
): Result<Cash13> {
const checked = validate(openingCash, weeks);
if (!checked.ok) {
return checked;
}
const rows: WeekRow[] = [];
let balance = openingCash;
let totalInflow = 0;
let totalOutflow = 0;
let minimumCash = openingCash;
let minimumWeek = 0;
let negativeFromWeek: number | null = null;
for (let i = 0; i < weeks.length; i += 1) {
const flow = weeks[i];
const net = sub(flow.inflow, flow.outflow);
const closing = add(balance, net);
rows.push({
week: i + 1,
opening: balance,
inflow: flow.inflow,
outflow: flow.outflow,
net,
closing,
});
totalInflow = add(totalInflow, flow.inflow);
totalOutflow = add(totalOutflow, flow.outflow);
if (closing < minimumCash) {
minimumCash = closing;
minimumWeek = i + 1;
}
if (closing < 0 && negativeFromWeek === null) {
negativeFromWeek = i + 1;
}
balance = closing;
}
return ok({
rows,
totalInflow,
totalOutflow,
netChange: sub(totalInflow, totalOutflow),
openingCash,
closingCash: balance,
minimumCash,
minimumWeek,
negativeFromWeek,
});
}
/**
* First week whose closing balance sits below the floor. Returns null when the
* whole horizon stays at or above the floor.
*/
export function firstWeekBelow(
rows: WeekRow[],
floor: Cents,
): number | null {
for (const row of rows) {
if (row.closing < floor) {
return row.week;
}
}
return null;
}
/** Weeks in the horizon that end below the floor. */
export function weeksBelow(rows: WeekRow[], floor: Cents): number {
return rows.filter((row) => row.closing < floor).length;
}
代码里有三个刻意的设计。第一,周数固定为 13,不做参数化:一旦允许 4 周或 26 周,口径就会在团队里分叉,表格之间不再可比。第二,最低点和最低点所在的周一起返回。只看期末现金会漏掉中途的深坑,而现金危机几乎总是发生在中途。第三,negativeFromWeek 记录第一次余额为负的周;只要它不为空,这张表就已经不是「紧不紧」的问题,而是「哪一周付不出来」的问题。
LedgerLite 的输入是期初 9,500,000 美分、13 组流入 323,400(第 6 周 2,125,200)、流出 327,000(第 9 周 1,521,100),输出期末 10,060,900、最低 9,482,000、最低周第 5 周。Tidecast 的输入是期初 9,209,000、13 组流入按发行节奏排列、流出 32,700,输出期末 14,334,300,最低点就是期初。这些数字与上面的表逐行对应,只是单位从美元换成了美分。
周表本身不解决「钱不够怎么办」,它只负责提前发现。发现之后的动作属于情景模拟与 runway 计算:8.2 节会把大客户流失、平台封禁、退款潮代入这张表,8.3 节会把期末现金换算成还能撑几个月。操作层的周表模板、银行流水导入与每周复盘节奏,见 13 周现金流 。
小结
- 13 周现金流表按周记录期初、流入、流出、净额、期末五个字段,只放已签约或已发生的收付,按现金日期而不是确认日期入表。
- LedgerLite 的常规周净额是 −36,靠第 6 周年付续约 18,018 补回,第 9 周税款 11,941 又把现金拉回;最低点在第 5 周,94,820。
- 1.5% 月流失在 13 周里只造成 868 美元的累计回款差,周表对它几乎无感;流失要看年度趋势,撞车要看周表。
- Tidecast 的 13 周现金增加 51,253,但峰值来自 12 月销售在 1 月到账,第 13 周的周净额只剩 1,271,脉冲之后必须问下一次脉冲在哪里。
- Nord Studio 的周表是一条每周 +660 的直线,第 8 周的 8,580 税款把低点压到 22,440;账期在稳态下被连续项目抵消,断单才会暴露。
- 三张表的共同结论:周表的价值不是期末余额,而是最低点出现在第几周、为什么出现在那一周。
阅读导航:上一节:7.3 续约、扩张与收缩收入 · 下一节:8.2 情景模拟:断供、退款潮、平台封禁 。
常见错误与边界
- 把年度利润除以 52 当周现金流。 LedgerLite 年净利 58,299,除以 52 是 1,121,但常规周净额是 −36;差额来自年付脉冲和税款集中到期。
- 把应收账款当现金。 LedgerLite 年末应收 12,000,账期 Net 15;这张应收要等到账才进周表,提前记入会把最低点整体抬高约 12,000。
- 把平台销售额当到账额。 Tidecast 12 月卖 2,400 份,现金要 30 天后才到;按销售周记账会让第 3 到第 7 周凭空多出五周峰值。
- 漏掉季度税款。 LedgerLite 的 11,941、Nord 的 8,580 都不是均匀支出;摊到 13 周会把第 8、第 9 周的深坑抹平。
- 只做一次,不滚动。 13 周表每周要往前滚一格;一个月不更新,表里的「未来」就已经是过去。
- 把最乐观的回款写进表。 只放已签约或已发生的收付;口头承诺和未开票项目属于销售预测,不属于现金预测。
- 只看期末现金。 Tidecast 期末 143,343 很漂亮,但第 13 周周净额只有 1,271;期末余额不告诉你脉冲已经结束。
- 把 13 周当全年。 一个季度覆盖不了完整的年度税季和续约季;周表要和 12 个月趋势一起读。
- 用账面现金当期初。 在途资金、未清算的平台款、被冻结的余额都不等于可用现金;期初必须来自银行对账单。
- 忽略周表之外的承诺。 已签但未开始的合同、已承诺但未下单的采购,都不在这 13 周的流入流出里,却会在第 14 周出现。
本节的边界是「13 周、单一情景、稳态参数」的现金模型:它假设回款节奏和历史一致,不含新签带来的增量,也不含融资、借款和分红。它足够回答「未来一个季度哪一周最紧」,但不足以回答「如果最大客户走了会怎样」——后者需要把冲击写成参数,代入同一张表重算。下一节做这件事:把大客户流失、平台封禁、退款潮和工资税费集中到期,分别变成可复算的情景。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。