订阅单最怕的不是字段多,而是语义不一致:明明有 recurring 产品却没有周期,或者挂了计划却找不到任何 recurring line。短期看似还能保存,长期一定会把续约、开票和分析全拖偏。
这篇文章主要参考了以下企业版源码入口:
enterprise/sale_subscription/models/sale_order.py
一、这篇功能真正解决什么问题
sale_subscription 的核心任务是把销售订单变成一台可持续推进的订阅状态机。要做到这一点,订单上所有关键字段都必须彼此解释得通:有没有 recurring 语义、属于什么 plan、当前处在什么订阅状态、是不是续约单或 upsell 单。
二、核心链路怎么走
1. is_subscription 不是手填标签,而是由 plan 驱动
_compute_is_subscription() 的逻辑很直接:没有 plan_id,或者当前状态是 upsell,就不被认定为标准订阅。也就是说,plan 不是附属属性,而是让订单进入订阅世界的入口。
2. subscription_state 取决于订单状态与父子关系
_compute_subscription_state() 会根据 state、is_subscription 与 subscription_id 判断当前是新订阅报价、续约报价还是别的形态。这让普通销售单、续约单、upsell 单不会混成一锅。
3. 真正兜底的是一组硬约束
_constraint_subscription_plan() 明确规定:确认态订阅若有 recurring line 就必须有 plan;若有 plan 却没有 recurring line 也不行;upsell 还不能跨币种。表面上这些规则很“较真”,实际上它们是在防止未来 next_invoice_date、MRR、续约链路失去依据。
三、新手最容易踩的坑
- 把 plan 当作可有可无的标签,结果 recurring 产品根本没有统一周期语义。
- 以为订阅状态能手工随便改。很多状态都是计算字段,背后依赖订单生命周期。
- 以为 upsell 只是普通报价的另一种叫法。源码对 upsell 的币种和订阅识别都给了特殊边界。
四、实战落地时最该盯的点
- 产品、模板和销售流程设计时就要决定 recurring line 从哪里来,不要等订单确认后再补 plan。
- 如果项目里经常出现“订阅单保存不了”的抱怨,先查是不是行和计划语义不一致,而不是先怀疑权限。
- 做数据迁移时,宁可慢一点把历史单梳理干净,也不要留下“有 recurring line 但无 plan”的灰色数据。
五、结论
订阅之所以必须同时有周期和 recurring line,不是因为 Odoo 爱设门槛,而是因为它要保证状态机、开票节奏和收入分析始终站在同一套事实之上。少了任何一个锚点,订阅单都会变成不可解释的半成品。
DISCUSSION
评论区