先说结论
很多人一提到 Odoo 单号,就只想到 ir.sequence。
但在实际业务里,像会计分录这类对象,编号往往不是“点一下拿个纯递增值”这么简单。
官方源码里的 sequence.mixin 解决的是一个更难的问题:
单号既要允许业务上可编辑,又要尽量保持日期一致、格式连续,并在并发下避免重号。
这也是为什么你会觉得 Odoo 的某些编号机制“既灵活又严格”。
为什么 sequence.mixin 比普通 sequence 更难
因为它处理的不是简单“发号器”,而是可编辑编号体系。
这意味着系统要同时面对几种张力:
- 用户可能会手工改前缀
- 不同月份 / 年度可能要重置编号
- 旧号已经存在,下一号要沿着现有格式继续
- 并发过账时又不能撞号
你会发现,这不是单一技术问题,而是:
- 格式识别
- 日期约束
- 历史续号
- 并发锁定
四件事一起存在。
官方源码在做什么
看 addons/account/models/sequence_mixin.py,这套机制大致分成四层。
1. 先把现有编号拆成“前缀 + 数字 + 日期片段”
源码里准备了多组 regex:
- 月度重置
- 年度重置
- 年区间
- 固定序列
它不是假设所有编号都长一样,而是先尝试从已有编号里反推出格式。
这点很关键。
因为 Odoo 在很多场景不是“完全自己定义未来的号”,而是:
根据你现在已经用起来的格式,继续往后编号。
2. 再检查“编号和日期是不是匹配”
源码里 _constrains_date_sequence() 会校验:
- 单号里带的年份 / 月份
- 是否和记录日期一致
所以如果你把日期改到另一月、另一年,但编号前缀还是旧时期,系统就可能要求你清空编号重新取。
这就是很多人感受到的“为什么我明明只是改个日期,编号却被卡住”。
因为在这套设计里,日期不是装饰,而是编号语义的一部分。
3. _get_last_sequence() 不是随便找最大数字
源码注释专门提醒了一件容易忽略的事:
- 它是在指定 domain 内
- 找到字母序最大的前缀链
- 再取其中
sequence_number最大的那条
所以如果你中途手工把前缀改成另一个字母序更小的前缀,下一号不一定会沿着你刚改的那条继续。
这也是官方文档式注释反复提醒“改前缀要小心”的原因。
4. _locked_increment() 处理并发取号
这是最值得开发者认真看的部分。
源码里没有天真地假设“查到最后一个号,再 +1”就安全。
它明确用了:
- 唯一约束保护
- savepoint
- 事务内 cache
- 重试机制
核心思路是:
- 先尝试更新受唯一约束保护的那一行
- 如果并发下撞到唯一冲突,就 rollback 到 savepoint 再试
- 一旦当前事务拿住这条序列链,后续同事务递增可借助 cache,减少 savepoint 开销
这说明 sequence.mixin 的重点根本不只是“格式化字符串”,而是在业务可编辑前提下尽量守住并发唯一性。
为什么它允许编辑,又不鼓励乱编辑
因为这套机制的目标,从来不是“让用户随便改号”。
它真正支持的是:
- 某些业务场景下需要修正编号
- 系统仍尽量根据历史编号继续工作
- 但会对日期一致性和唯一性加护栏
所以如果你把它理解成“可编辑版 ir.sequence”,只对了一半。
更准确地说,它是:
带历史续号能力和并发保护的可编辑编号框架。
新手最容易误解的点
1. 以为下一号只看最大数字
不对。
它还会受到前缀链和 domain 的影响。
2. 以为手工改前缀不会影响后续编号
实际上很可能影响,尤其当字母序链发生变化时。
3. 以为日期只是显示字段
在这套机制里,日期经常是编号合法性的一部分。
4. 以为并发安全是数据库“自然保证”的
不是自动白送的。
源码里为了处理这件事,专门用了 savepoint、唯一索引和 retry 逻辑。
实战开发该怎么用这层理解
如果你在自定义一个也需要“业务可读、可编辑、按周期续号”的对象,我会先问:
- 这个编号是否真的允许用户改?
- 日期是否要嵌入编号语义?
- 前缀变化会不会让历史续号变得混乱?
- 并发下谁来守住唯一性?
如果这些问题没想清楚,只是“做个看起来像单号的字符串”,后面迟早出事。
一句话记忆法
sequence.mixin不是普通发号器,而是 Odoo 用来平衡“可编辑单号、日期一致性、历史续号和并发唯一性”的一整套机制。
理解这一点,很多“为什么单号能改、又为什么改了会报错”的现象就都顺了。
DISCUSSION
评论区