先说结论
很多人理解 Odoo 的 milestone billing,会说成:
- “里程碑完成了,就可以开票”
这话不算错,但太粗了。
从 sale_project 源码看,真正发生的事情是:
- 项目里程碑
project.milestone可以绑定一个sale.order.line - 每个 milestone 可以定义自己占销售行数量的百分比
quantity_percentage - 销售行的
qty_delivered_method = 'milestones' sale.order.line的 delivered quantity,会按“已 reached 里程碑的百分比总和 × 订单数量”计算
所以更准确的说法是:
Odoo 里程碑计费不是“达标后允许开票”,而是“里程碑 reached 之后,自动推动销售行的 delivered 数量,从而推动可开票数量”。
第一层:milestone billing 的核心,不在 project,而在 sale_project 扩展
基础 project.milestone 主要关心:
- 截止日期
- reached
- 任务归属
真正把它接到销售计费上的,是 sale_project 给 milestone 加的这些字段:
sale_line_idquantity_percentageproduct_uom_qty
这一步很关键。
因为 Odoo 没有把“里程碑计费”做成一个单独的开票按钮,而是把 milestone 变成销售行已交付数量的一个来源。
第二层:为什么 sale_line_id 只能选 milestones 交付法的销售行
源码里 sale_line_id 的 domain 明确要求:
qty_delivered_method = 'milestones'
而在产品 / 销售行侧,服务产品如果采用基于 milestone 的交付政策,就会让该销售行走 milestones delivered method。
这说明 Odoo 的设计很严格:
不是任意销售行都能绑 milestone,只有“交付数量由里程碑驱动”的服务行才可以。
也就是说,里程碑不是附加说明,而是交付量计算机制的一部分。
第三层:里程碑完成后,真正变化的不是 invoice state,而是 qty_delivered
这是最值得讲清楚的一点。
在 sale.order.line 里,源码通过:
reached_milestones_ids_prepare_qty_delivered()
来计算已交付数量。
逻辑是:
- 找出当前销售行下所有
is_reached = True的 milestone - 汇总它们的
quantity_percentage - 再乘以
product_uom_qty - 得到这条销售行的
qty_delivered
所以 milestone reached 不是直接改“能不能开票”,而是先改:
- 这条销售行已经交付了多少
后续发票能力,是销售模块基于 delivered quantity 再继续推出来的。
这就把边界讲清楚了。
第四层:为什么 Odoo 用 percentage,而不是“每个里程碑固定金额”
quantity_percentage 的设计说明,Odoo 默认把里程碑计费理解成:
- 销售行总交付量,被拆成几个 milestone 百分比
比如一条销售行数量是 100:
- milestone A 占 30%
- milestone B 占 20%
- milestone C 占 50%
那么 reached 一个 milestone,本质上是把该销售行的已交付数量推进一部分。
这和很多专业服务业务很接近:
- 不是每完成一个任务就单独开票
- 而是按项目阶段分批确认可计费交付
第五层:为什么 quantity_percentage 和 product_uom_qty 互相可推导
在 sale_project.models.project_milestone 里:
_compute_quantity_percentage()_compute_product_uom_qty()
会让“百分比”和“该 milestone 对应的数量”互相换算。
这说明 Odoo 在 UI 层其实兼顾了两类人:
- 有人习惯按比例思考:这个里程碑占 25%
- 有人习惯按数量思考:这个阶段交付 40 小时 / 4 个单位
但无论界面怎么填,底层最后都要回到一个统一问题:
这次 milestone reached 后,要把销售行的已交付数量推进多少。
第六层:为什么它适合项目分阶段收费,不适合“一个里程碑一个独立账单产品”
从源码结构看,milestone billing 绑定的是同一条销售行的交付推进。
也就是说,它更适合这种业务:
- 一项服务卖给客户
- 但分几个阶段确认交付与开票
而不是这种模式:
- 每个里程碑本身就是完全独立的产品行
后者不是不能做,而是那种情况下,直接拆成多条 sale order line 往往更直观。
Odoo 这里的设计重点,是“把一条服务销售行切成多个计费节点”,不是“拿里程碑替代产品行”。
新手最容易误解的 4 件事
1. 以为 reached milestone 会直接生成发票
不对。它先推进的是 delivered quantity。
2. 以为 milestone billing 只是 project 模块功能
不对。关键联动在 sale_project 和 sale.order.line。
3. 以为每个 milestone 都必须对应独立销售行
不对。一个销售行可以被多个里程碑按比例推进。
4. 以为 percentage 只是前端展示
不对。它直接参与 qty_delivered 计算。
实战上最该注意什么
1. 销售产品的交付政策一定要选对
如果不是 milestones 驱动,后面整条 delivered 链路就不会按这个逻辑走。
2. 里程碑比例总和要有业务纪律
源码允许逐个累加 reached 百分比,所以实施时最好确保总和设计清楚,不要出现失控的重叠和超配。
3. 报表解释时,要把“reached → delivered → to invoice”这三层讲给业务听
否则业务很容易误以为“系统怎么没直接开票”。
一句话记忆法
Odoo milestone billing 的核心不是“达到里程碑就开票”,而是“里程碑 reached 后推进销售行已交付数量,再由销售开票逻辑继续往下走”。
DISCUSSION
评论区