项目深度

Odoo 可计费任务为什么不只是绑一条销售行:sale_line_id 继承、客户一致性与自动确认链路讲透

Odoo 项目里把任务做成可计费后,真正麻烦的不是多一个 sale_line_id 字段,而是它要从父任务、里程碑、项目之间继承,还要校验客户边界,甚至在保存时推动销售单确认。本文把这条链讲透。

项目
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

很多人把 Odoo 任务可计费理解成:

  • 任务上多一个 sale_line_id
  • 最后能开票

/home/ubuntu/odoo-temp/addons/sale_project/models/project_task.py 告诉你,事情远没这么简单。

一张可计费任务真正要解决的是:

  • 这张任务应该挂哪条销售订单行
  • 如果没手填,应该从父任务、里程碑还是项目继承
  • 当前客户和销售单客户是否一致
  • 如果保存时挂上了销售行,相关销售单要不要被自动确认

所以可计费任务不是“多绑一个 Many2one”,而是 项目任务和销售语义的对齐机制


第一层:sale_line_id 不是孤立字段,它代表任务的计费归属

sale_project 扩展里,任务多了几个关键字段:

  • sale_line_id
  • sale_order_id
  • task_to_invoice
  • allow_billable

这里最重要的不是字段本身,而是它们表达的业务语义:

任务是否可计费,决定这张任务是不是销售履约的一部分。

一旦是销售履约的一部分,就必须回答两个问题:

  1. 它到底归属于哪条销售行
  2. 它服务的是哪个客户

如果这两个问题答不清,后面所有“项目利润”“待开票”“门户客户查看销售单”等能力都会变得不可靠。


第二层:为什么 Odoo 不是只从项目继承 sale_line_id

_compute_sale_line() 非常值得读。

当任务可计费、但自身又没有显式 sale_line_id 时,Odoo 的优先顺序不是随便来的,而是:

  1. 先看父任务的 sale_line_id
  2. 再看 milestone_id.sale_line_id
  3. 最后再看 project_id.sale_line_id

而且第一条还有额外条件:

  • 父任务客户和当前任务客户的商业伙伴要一致

这段设计很有味道。

它表达的不是“项目默认一条销售行,大家都继承”,而是:

任务计费归属优先跟工作拆分走,其次跟交付节点走,最后才退回到项目默认销售关系。

这非常符合真实交付逻辑:

  • 子任务常常是父任务工作包的继续
  • 里程碑任务可能要跟特定销售行绑定
  • 项目级默认销售行只是兜底

第三层:为什么 Odoo 一直在校验 customer consistency

_compute_sale_order_id()_inverse_partner_id() 都在反复做同一件事:

  • 核对任务客户
  • 核对销售单客户 / 开票客户 / 收货客户
  • 最后统一映射到 commercial_partner_id

一旦任务客户不在这组“可接受客户集合”里,Odoo 会把:

  • sale_order_id
  • sale_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_billable
  • partner_id
  • sale_line_id
  • sale_order_id
  • 里程碑来源
  • 父任务来源

一句话记忆法

Odoo 可计费任务不是“任务挂销售行”,而是“在父任务、里程碑、项目之间选计费归属,并持续校验客户边界与销售状态”的一整套对齐机制。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。