先说结论
很多人把 Odoo 任务可计费理解成:
- 任务上多一个
sale_line_id - 最后能开票
但 /home/ubuntu/odoo-temp/addons/sale_project/models/project_task.py 告诉你,事情远没这么简单。
一张可计费任务真正要解决的是:
- 这张任务应该挂哪条销售订单行
- 如果没手填,应该从父任务、里程碑还是项目继承
- 当前客户和销售单客户是否一致
- 如果保存时挂上了销售行,相关销售单要不要被自动确认
所以可计费任务不是“多绑一个 Many2one”,而是 项目任务和销售语义的对齐机制。
第一层:sale_line_id 不是孤立字段,它代表任务的计费归属
在 sale_project 扩展里,任务多了几个关键字段:
sale_line_idsale_order_idtask_to_invoiceallow_billable
这里最重要的不是字段本身,而是它们表达的业务语义:
任务是否可计费,决定这张任务是不是销售履约的一部分。
一旦是销售履约的一部分,就必须回答两个问题:
- 它到底归属于哪条销售行
- 它服务的是哪个客户
如果这两个问题答不清,后面所有“项目利润”“待开票”“门户客户查看销售单”等能力都会变得不可靠。
第二层:为什么 Odoo 不是只从项目继承 sale_line_id
_compute_sale_line() 非常值得读。
当任务可计费、但自身又没有显式 sale_line_id 时,Odoo 的优先顺序不是随便来的,而是:
- 先看父任务的
sale_line_id - 再看
milestone_id.sale_line_id - 最后再看
project_id.sale_line_id
而且第一条还有额外条件:
- 父任务客户和当前任务客户的商业伙伴要一致
这段设计很有味道。
它表达的不是“项目默认一条销售行,大家都继承”,而是:
任务计费归属优先跟工作拆分走,其次跟交付节点走,最后才退回到项目默认销售关系。
这非常符合真实交付逻辑:
- 子任务常常是父任务工作包的继续
- 里程碑任务可能要跟特定销售行绑定
- 项目级默认销售行只是兜底
第三层:为什么 Odoo 一直在校验 customer consistency
_compute_sale_order_id() 和 _inverse_partner_id() 都在反复做同一件事:
- 核对任务客户
- 核对销售单客户 / 开票客户 / 收货客户
- 最后统一映射到
commercial_partner_id
一旦任务客户不在这组“可接受客户集合”里,Odoo 会把:
sale_order_idsale_line_id
直接清掉。
这说明官方非常警惕一种常见错误:
- 任务看起来挂着某销售行
- 但服务对象已经不是这张销售单对应的客户
如果不做这个校验,项目和销售就会出现一种很难排查的“半连通状态”:
- 表面有关联
- 实际不能安全计费
所以这里的客户一致性,不是吹毛求疵,而是在防止计费语义跑偏。
第四层:为什么里程碑计费和普通项目计费不能混成一个概念
已有很多人知道 Odoo 支持 milestone billing,但从 _compute_sale_line() 可以更明确看出:
- 里程碑本身也可能携带
sale_line_id - 任务的销售归属可以来自 milestone
这意味着里程碑不是“纯项目进度节点”,它还可能是计费归属节点。
所以你不能把“任务可计费”理解成一条单线:
- 项目 → 任务 → 开票
实际上更像是三种来源在竞争:
- 父任务拆分来源
- 里程碑交付来源
- 项目默认来源
而 Odoo 做的是一套优先级选择器。
第五层:为什么挂上销售行后,系统还会去确认销售单
create() 和 write() 里还有一个很有意思的动作:
- 如果传入了
sale_line_id - 会调用
_ensure_sale_order_linked() - 该方法会找到还处于草稿状态的报价单并执行
action_confirm()
很多人第一次读到这里会愣一下:
“我只是给任务挂了销售行,为什么系统还要动销售单状态?”
答案是:
因为 Odoo 认为已经被项目/任务正式引用的销售行,不该继续停留在“未确认报价”语义。
否则你就会得到一种很奇怪的状态:
- 项目任务在按它交付
- 但销售单还没真正成立
这在管理语义上是站不住的。
所以 Odoo 选择在记录保存时尽量把这两边拉齐。
第六层:为什么不是所有销售行都能绑到任务上
_check_sale_line_type() 还做了一层约束:
- 只有服务类销售行才适合绑定任务
- re-invoiced expense 这种场景会被拦掉
这也很好理解。
项目任务代表的是“工作履约”,天然更接近服务交付。
如果把报销型、费用转嫁型销售行也混进来,任务的计费边界会被搞乱。
所以官方在这里不是技术限制,而是在保护业务模型。
新手最容易误解的 4 件事
1. 以为 sale_line_id 总是从项目直接继承
不对。父任务和里程碑都可能优先。
2. 以为只要挂上销售行,客户不一致也无所谓
不对。源码会主动清掉不一致的销售关联。
3. 以为挂销售行只是项目模块内部的小改动
不对。保存时甚至可能推动销售单确认。
4. 以为任何销售行都能绑定任务
不对。绑定对象必须符合服务交付语义。
实战上最该注意什么
1. 自定义任务可计费逻辑时,一定要保住客户一致性校验
否则利润、开票和门户展示都可能出错。
2. 如果你做子任务自动分派,别忘了 sale_line 继承优先级
父任务来源往往比项目默认来源更精确。
3. 调试“为什么任务没法开票”时,不要只看任务字段
要一起看:
allow_billablepartner_idsale_line_idsale_order_id- 里程碑来源
- 父任务来源
一句话记忆法
Odoo 可计费任务不是“任务挂销售行”,而是“在父任务、里程碑、项目之间选计费归属,并持续校验客户边界与销售状态”的一整套对齐机制。
DISCUSSION
评论区