先说结论
Odoo Fleet 里的合同不是“放在车辆卡片上的一条说明”,而是一个会自己变化、会自己提醒、还会把风险反馈回车辆对象的业务记录。
从 /home/ubuntu/odoo-temp/addons/fleet/models/fleet_vehicle_log_contract.py 与 fleet_vehicle.py 看,官方真正设计的是:
- 合同自己有生命周期状态
- 状态会随着日期自动切换
- 到期前会创建活动提醒负责人
- 车辆层再把这些风险做成聚合信号
所以 Fleet 合同本质上不是文档存档,而是车辆责任与续约风险的运行对象。
第一层:为什么合同对象要有 futur / open / expired / closed 四种状态
fleet.vehicle.log.contract 里定义的状态不是简单“有效/无效”,而是:
futuropenexpiredclosed
这套状态机很有现实感。
因为车辆合同的业务语义往往至少分四种:
- 已签但尚未生效
- 正在生效中
- 已过期但还保留历史责任意义
- 被主动取消或关闭
如果只做成一个布尔值,很多判断都会变糊:
- 尚未生效的未来合同,算不算已配置?
- 过期合同和人为关闭合同,为什么业务含义要一样?
- 车辆当前到底有没有“真正可用”的有效合同?
所以 Odoo 这里明显不是为了显示颜色,而是在给后续自动化和报表打基础。
第二层:为什么改开始日或到期日后,系统会重新归类状态
write() 里有个很关键的逻辑:
只要你改了 start_date 或 expiration_date,系统就会重新把合同分流到:
- future_contracts
- running_contracts
- expired_contracts
然后分别执行:
action_draft()action_open()action_expire()
这说明 Odoo 默认认为:
合同状态不是人工标签,而是日期驱动的业务结论。
这点特别重要。
因为现实中最容易出错的,就是后台人员改了日期,却忘了同步改状态,结果页面上看着“open”,其实早就过期;或者写着“expired”,其实下个月才开始生效。
Odoo 直接把状态和日期耦合起来,可以显著降低这种脏数据。
第三层:为什么 days_left 和 expires_today 要单独算
源码里专门计算:
days_leftexpires_today
而且还区分:
- open / expired 状态下如何显示剩余天数
- closed 情况返回特殊值
这代表官方知道,业务团队对合同最关心的问题不是抽象状态,而是:
- 还有几天到期
- 今天是不是必须处理
- 这个提醒是不是已经错过
也就是说,合同管理真正服务的是时间感知,不是单纯存档。
第四层:为什么提醒不是直接发邮件,而是创建 activity
scheduler_manage_contract_expiration() 里做的不是一刀切发通知,而是:
- 读取系统参数
hr_fleet.delay_alert_contract - 找出即将到期的 open 合同
- 给负责人
user_id安排fleet.mail_act_fleet_contract_to_renewactivity
这个设计很 Odoo。
因为 activity 的好处是:
- 它挂在具体合同对象上
- 有负责人
- 能在待办里统一看到
- 后续可转派、可 reschedule、可留痕
相比一封孤零零的邮件,activity 更像真正的流程任务。
第五层:为什么 reminder activity type 要在 mail.activity.type 里声明模型信息
mail_activity_type.py 里专门把 fleet.mail_act_fleet_contract_to_renew 映射到了 fleet.vehicle.log.contract。
这不是多余动作。
它说明官方希望这个提醒类型被系统明确识别为“属于合同对象”的活动,而不是泛化消息。
这样做的收益是:
- cron 可以更稳定地避免重复创建
- 前端待办与模型关系更清晰
- 后续自动化更容易围绕正确模型展开
第六层:为什么车辆本身还要再计算一层 contract risk 信号
fleet.vehicle 上还有这些聚合字段:
contract_renewal_due_sooncontract_renewal_overduecontract_state
这很关键,因为大多数运营者打开 Fleet,先看到的是车辆,而不是合同清单。
所以 Odoo 的做法是:
- 合同层保留细粒度事实
- 车辆层暴露最重要的风险信号
这样一来,车队经理在看车辆 kanban 或列表时,就能先知道哪辆车有续约风险,再下钻到合同明细。
这就是很典型的“明细在下层,预警在上层”。
第七层:为什么合同里还放 recurring cost 和 included services
合同对象里除了日期状态,还有:
cost_generatedcost_frequencyservice_idscost_subtype_id
这说明在 Odoo 看来,车辆合同不只是到期问题,它还承担:
- 费用节奏表达
- 覆盖服务范围表达
- 合同类别表达
也就是说,合同既是风险边界,也是成本边界。
如果实施时只把它当保险到期日记录,Fleet 的价值会被用掉一半。
最容易误解的三个点
误区一:合同状态就是手工维护的标签
不是。 在 Odoo 里,它更接近日期驱动的自动状态机。
误区二:续约提醒发封邮件就够了
不够。 真正可跟踪的做法是 activity,而不是一次性通知。
误区三:只要合同记录存在,车辆风险就算可见
未必。 如果不把风险聚合到车辆层,日常运营很容易看不到即将爆雷的车。
实施时怎么用最稳
如果你要把 Fleet 合同用起来,我更建议这样落地:
- 明确区分未来合同、运行合同、过期合同和关闭合同,不要人为混写
- 把
delay_alert_contract配成符合团队节奏的提前量 - 给每份合同明确
user_id负责人,否则提醒难以闭环 - 在车辆列表上重点利用 due soon / overdue 这类聚合信号做巡检
- 把 recurring cost 和 included services 一起维护,别把合同降级成附件柜
最后总结
Odoo Fleet 合同的真正价值,在于它把三件事连起来了:
- 日期决定状态
- 状态触发提醒
- 提醒再回流成车辆风险视图
理解这条链之后,你会发现 Fleet 管的不是“这台车有没有合同文件”,而是“这台车当前承担的责任边界是否还在有效运转”。
DISCUSSION
评论区