以下情境为教学假设,人物与数字不代表经核实的真实案例。
第 3 章已经把三个案例各自的利润表、现金流量表和资产负债表算过一遍。那一章的任务是「每一张表怎么搭」,这一章的任务是「三张表放在一起看什么」。后者更难,因为横向对照会把口径差异放大:同一行数字在不同案例里可能代表不同的东西,直接比大小会得出错误结论。
这一节做三件事:把三案例压到同一套五层结构上,检查资产负债表是否勾稽,再用现金转换率解释「净利润为什么不是现金」。最后列出五处不能直接比的地方,作为进入卷二前的口径备忘录。
统一后的五层结构
利润表有十几行,但真正决定结构的只有五层:收入、毛利、SDE、税前利润、净利润。把三案例代入同一套公式,得到下表。金额单位为美元,比率按精确金额计算后四舍五入。
| 层 | 公式 | LedgerLite | Tidecast | Nord Studio |
|---|---|---|---|---|
| 收入 | 确认收入 | $240,240 | $138,171 | $154,320 |
| 毛利 | 收入 − 变动成本 | $202,240 | $138,171 | $130,320 |
| SDE | 毛利 − 运营费用 | $130,240 | $121,171 | $34,320 |
| 税前利润 | SDE − 所有者薪酬 | $70,240 | $121,171 | $34,320 |
| 净利润 | 税前 − 所得税 | $58,299 | $92,090 | $25,740 |
三行的毛利率分别是 84.2%、100.0%、84.4%,看起来差距不大;但 SDE 率是 54.2%、87.7%、22.2%,差了近四倍。差距出在运营费用:LedgerLite 72,000,Tidecast 17,000,Nord 96,000。
Tidecast 的 100.0% 毛利率不是经营优势,而是列示口径。它的平台分成、退款、支付费已经在收入之前扣掉,利润表上不再重复列为变动成本。如果把标价收入 191,904 美元放在第一行,毛利率会变成 72.0%,但 SDE 仍是 121,171 美元。横向对照时,先看毛利率的分子分母是不是同一种收入,再比大小。
Nord 的 SDE 率 22.2% 也不能简单读成「效率低」。它的 96,000 美元是两位合伙人的工资,属于服务交付的必要成本。把工资移到毛利之下,毛利率会从 84.4% 掉到 22.2%,SDE 不变。同一个生意,分类位置不同,两个比率就互换。这就是第 3 章反复强调「分类决定比率」的原因。
资产负债表的勾稽
三个案例里只有 LedgerLite 给出了完整的资产负债表。它的资产合计 110,000 美元,负债 30,000 美元,权益 80,000 美元,两边相等。
| 项 | 金额 | 说明 |
|---|---|---|
| 现金 | $95,000 | 银行对账单口径 |
| 应收 | $12,000 | 已开票未回款 |
| 固定资产 | $3,000 | 设备,未计折旧 |
| 资产合计 | $110,000 | |
| 递延收入 | $22,000 | 已收未确认 |
| 应付 | $8,000 | 供应商账期 |
| 权益 | $80,000 | 期初权益加留存收益 |
| 负债与权益合计 | $110,000 |
勾稽检查只有一条:资产 = 负债 + 权益。这条不成立时,先查三处:递延收入的期间归属、固定资产的折旧、权益里的留存收益结转。三处都查过仍然对不上,通常是有一笔应计费用没有入账,或者一笔在途资金被同时记在两边。
Tidecast 和 Nord Studio 没有完整的资产负债表,这不是省略,而是案例设定只给出了利润表与现金口径。真实尽调中,这两张缺失的表恰恰是买方最先索要的材料:没有资产负债表,就无法确认应收、递延收入、应付和权益的真实余额。第 12 章的数据室清单把「三张报表齐全」列为必备项,原因就在这里。
现金转换率
净利润和经营现金流的差额,来自非现金项目与营运资金变化。用「经营现金流 ÷ 净利润」得到的比率,可以快速判断利润的含金量。
| 案例 | 净利润 | 经营现金流 | 转换率 | 差额来源 |
|---|---|---|---|---|
| LedgerLite | $58,299 | $64,299 | 110.3% | 递延收入 +$8,000、应收 −$2,000 |
| Tidecast | $92,090 | $92,090 | 100.0% | 案例按可支配现金列示,无营运资金调整 |
| Nord Studio | $25,740 | $25,740 | 100.0% | 同上,应收变化未单列 |
LedgerLite 的转换率高于 100%,是因为年付客户提前付钱,递延收入增加 8,000 美元,现金比利润多。这不是异常,而是订阅生意的常态:只要年付比例稳定,递延收入会持续为现金流提供缓冲。
Tidecast 和 Nord 的 100.0% 是简化口径,不代表它们没有营运资金变化。Tidecast 的平台月结 30 天、Nord 的 Net 30 账期,都会在年末形成应收或应付。案例只给出了年末应收占年收入的比例(0% 与 7.3%),没有给出期初期末的完整余额,所以无法还原完整的间接法现金流量表。
这里有一条实用规则:转换率高于 100% 时,先确认递延收入是否可持续;转换率低于 80% 时,先查应收是否在堆积。两端都正常,才说明利润和现金在同一条线上。
五处不能直接比的地方
横向对照最容易犯的错,是把不同口径的数字放在同一列比较。以下五处必须加注,否则结论会反。
第一,Tidecast 的收入是净得,不是标价。 它的 138,171 美元已经扣掉平台分成、退款与支付费。拿它和 LedgerLite 的 240,240 美元比「收入规模」,等于拿净额比总额。
第二,SDE 是否扣除所有者薪酬不一致。 LedgerLite 扣了 60,000 美元创始人薪酬,Nord 没有扣两位合伙人的 96,000 美元工资。比较 SDE 前先把口径拉平,否则 130,240 与 34,320 的差距会被高估。
第三,毛利率的分母不同。 Tidecast 的 100.0% 以净得为分母,LedgerLite 与 Nord 以确认收入为分母。三者只有在同一分母下才能比较。
第四,现金流的口径不同。 LedgerLite 给出的是完整经营现金流,Tidecast 与 Nord 给出的是「可支配现金」,即 SDE 减所得税,没有营运资金调整。
第五,资产负债表只有一家完整。 另外两家缺表,不代表它们的资产负债结构简单,只代表案例没有给出。
把这五条写进自己的对照表,横向比较才有意义。缺一条,数字就会「看起来能比」,实际上不能。
小结
三张表的横向对照,先统一五层结构,再查资产负债表勾稽,最后用现金转换率判断利润含金量。三个案例的 SDE 率从 22.2% 到 87.7%,差异主要来自运营费用与列示口径,不是经营效率的简单排序。
baseline.ts 把这三步写成了可复算的函数:buildBaseline 统一五层结构,checkBalanceSheet 做勾稽,cashBridge 还原净利润到现金的桥。三个案例的数据代入后,得到的数字与上表一致。
阅读导航:上一节:12.3 常见红旗与提前修复 · 下一节:13.2 单位经济与估值区间 。
常见错误与边界
- 把净得当收入比规模。 Tidecast 的 138,171 是净得,标价收入是 191,904。跨案例比收入前先确认是不是同一层。
- 用毛利率给生意排序。 毛利率受列示口径影响极大,Tidecast 的 100.0% 与 Nord 的 84.4% 不在同一个分母上。
- 看到 SDE 高就认为效率高。 SDE 没有扣除所有者薪酬时,会把「人少」误读成「效率高」。
- 忽略缺失的资产负债表。 缺表不是没问题,而是没证据。尽调时缺表会被当作风险。
- 把转换率当成质量指标单独用。 转换率高可能来自递延收入堆积,转换率低可能来自应收账期,必须结合营运资金一起看。
- 勾稽对不上就调数字。 先找未入账的应计项,不要为了让表平而改一个科目。
边界:本节只处理案例给出的口径。真实报表可能有更多科目、更多期间调整,横向对照前应先做一次科目映射,把三家的科目对齐到同一套五层结构。
// file: src/fin/baseline.ts
// tier: L1
// verify: node --test test/fin/baseline.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 BaselineInput = {
name: string;
revenueCents: Cents;
variableCostsCents: Cents;
operatingExpensesCents: Cents;
founderSalaryCents: Cents;
taxRateBps: Bps;
operatingCashFlowCents: Cents;
};
export type BaselineRow = {
name: string;
revenueCents: Cents;
grossProfitCents: Cents;
grossMarginBps: Bps;
sdeCents: Cents;
sdeMarginBps: Bps;
pretaxCents: Cents;
taxCents: Cents;
netIncomeCents: Cents;
netMarginBps: Bps;
cashConversionBps: Bps;
};
export type BalanceCheck = {
assetsCents: Cents;
claimsCents: Cents;
balanced: boolean;
differenceCents: Cents;
};
function checkAmount(value: Cents, label: string): string | null {
if (!Number.isInteger(value) || value < 0) {
return `${label} must be a non-negative integer amount in cents`;
}
return null;
}
/**
* Reduce one business to the same five-line structure used across the book.
* Gross profit, SDE, pretax income and net income are derived here so the
* three cases can be compared without re-deriving formulas by hand.
*/
export function buildBaseline(input: BaselineInput): Result<BaselineRow> {
const fields: Array<[Cents, string]> = [
[input.revenueCents, "revenue"],
[input.variableCostsCents, "variable costs"],
[input.operatingExpensesCents, "operating expenses"],
[input.founderSalaryCents, "founder salary"],
[input.operatingCashFlowCents, "operating cash flow"],
];
for (const [value, label] of fields) {
const error = checkAmount(value, label);
if (error) {
return fail(error);
}
}
if (
!Number.isInteger(input.taxRateBps) ||
input.taxRateBps < 0 ||
input.taxRateBps > 10000
) {
return fail("tax rate must be between 0 and 10000 bps");
}
if (input.variableCostsCents > input.revenueCents) {
return fail("variable costs cannot exceed revenue");
}
const grossProfitCents = input.revenueCents - input.variableCostsCents;
const sdeCents = grossProfitCents - input.operatingExpensesCents;
const pretaxCents = sdeCents - input.founderSalaryCents;
const taxCents = pretaxCents > 0 ? applyRate(pretaxCents, input.taxRateBps) : 0;
const netIncomeCents = pretaxCents - taxCents;
return ok({
name: input.name,
revenueCents: input.revenueCents,
grossProfitCents,
grossMarginBps: divide(grossProfitCents, input.revenueCents),
sdeCents,
sdeMarginBps: divide(sdeCents, input.revenueCents),
pretaxCents,
taxCents,
netIncomeCents,
netMarginBps: divide(netIncomeCents, input.revenueCents),
cashConversionBps: divide(input.operatingCashFlowCents, netIncomeCents),
});
}
/**
* The balance sheet must satisfy assets = liabilities + equity. A non-zero
* difference is almost always an unbooked accrual, a missing deferred balance
* or an equity roll-forward that was never closed.
*/
export function checkBalanceSheet(
assetsCents: Cents,
liabilitiesCents: Cents,
equityCents: Cents,
): Result<BalanceCheck> {
for (const [value, label] of [
[assetsCents, "assets"],
[liabilitiesCents, "liabilities"],
[equityCents, "equity"],
] as Array<[Cents, string]>) {
const error = checkAmount(value, label);
if (error) {
return fail(error);
}
}
const claimsCents = liabilitiesCents + equityCents;
return ok({
assetsCents,
claimsCents,
balanced: assetsCents === claimsCents,
differenceCents: assetsCents - claimsCents,
});
}
/**
* Net income and operating cash flow differ by non-cash items and working
* capital. This helper keeps the sign convention explicit: a deferred revenue
* increase is a positive adjustment because cash arrived before revenue.
*/
export function cashBridge(
netIncomeCents: Cents,
nonCashCents: Cents,
deferredRevenueChangeCents: Cents,
receivableChangeCents: Cents,
): Cents {
return (
netIncomeCents + nonCashCents + deferredRevenueChangeCents - receivableChangeCents
);
}
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。