很多人第一次看 Odoo 会计编号,都会把它脑补成一个很朴素的流程:
- 查最后一张凭证;
- 把末尾数字加 1;
- 写回去。
这个理解只在“单线程、纯数字、永不换年换月、没人改前缀”的世界里成立。
但 /home/ubuntu/odoo-temp/addons/account/models/sequence_mixin.py 真正解决的是一个复杂得多的问题:
同一套编号既要允许人类读懂,又要允许按日期重置,还要在多人同时过账时不撞号。
所以 SequenceMixin 真正的重点,不是“加一”,而是三件事:
- 先识别这条编号属于哪一种重置制度;
- 再从旧编号里拆出 prefix、year、month、seq;
- 最后在并发事务里安全地产生唯一的下一个号码。
一、Odoo 先关心的不是数字,而是“这条链按什么制度重置”
在 _deduce_sequence_number_reset() 里,源码会按几组正则去判断当前编号属于哪类序列:
year_range_monthmonthyear_rangeyearnever
这一步非常关键。
因为 Odoo 不是先拿到一个数字再决定怎么展示,而是反过来:
先从现有编号长相里,推断它的时间语义。
比如:
INV/2026/03/0007很像月度重置;INV/2026/0007很像年度重置;0007则更像固定不重置。
这解释了一个实战里很常见的误区:
为什么“我只改了前缀”,后面整条编号链却开始怪了?
因为你改的不是装饰文本,而是序列格式本身的证据。
一旦格式变了,Odoo 之后再看这条编号时,推断出来的 reset 周期可能就变了; 再往后取“上一条”时,搜索域和格式化结果也可能跟着变。
二、正则拆段不是为了炫技,而是为了把旧编号还原成一张模板
_get_sequence_format_param(previous) 的作用,不是简单解析字符串,而是把上一条编号拆成:
- prefix
- year / year_end
- month
- seq
- suffix
- 各字段长度
然后再生成一个 Python format 模板。
可以把它理解成:
- 旧编号是一个样本;
- Odoo 把样本拆成“结构 + 变量”;
- 下一条编号是在这个模板上替换年份、月份和 seq 之后得到的。
这就是为什么源码不只是保存 sequence_number,还会保存:
sequence_prefixsequence_number
因为它后面要按“前缀链”找最后一条,也要按数字比较大小,而不是只靠整串字符串硬算。
三、为什么 _get_last_sequence() 不只是“找最大数字”
_get_last_sequence() 里最容易被忽略的一点是:
它并不是全表找一个最大的
sequence_number,而是先按 domain 找到最近的前缀链,再在这条链里取最大序号。
源码里的说明写得很直白:
如果你把最后一条从 INV/2026/0002 改成 FACT/2026/0001,下一次未必会继续从 FACT/2026/0002 开始;
因为对数据库来说,前缀字母顺序本身会影响“最后那条链”是谁。
这意味着:
前缀不是 UI 皮肤,而是排序与选链条件的一部分
因此在同一期间中途换前缀,往往是危险操作。
更准确地说,SequenceMixin 的思路不是:
- “无论你怎么改,我都能聪明接上。”
而是:
- “请尽量保持一段期间内的链稳定;如果之前改乱了,用 resequence 明确修复。”
这也是 _constrains_date_sequence() 存在的意义:
- 日期和编号链不匹配时,不让你继续假装这是一条正常链;
- 想修,先清空编号或走 resequence。
四、为什么 Odoo 要校验“日期和编号是否匹配”
_sequence_matches_date() 会把编号重新拆开,再检查:
- 年份是否和记录日期一致;
- 月份是否和记录日期一致;
- year range 场景里起止年份是否合理。
这一步不是形式主义,而是为了防止两类真实事故:
1)人为改日期,不改编号
比如一张 2026 年 3 月的分录,被改到了 4 月,但名字还是 INV/2026/03/xxxx。
2)人为改编号,不顾日期语义
比如把 2026 年的凭证手工改成 2025 前缀,只因为“想补一个空号”。
在会计上,这类行为最大的问题不是“显示不好看”,而是:
时间维度的编号链会失真,后续过账、审计、重排都很难判断什么才是正常顺序。
五、真正最难的部分,其实是并发抢号
如果系统只有一个人过账,SequenceMixin 根本不需要这么复杂。
真正麻烦的是:
- 两个人同时过账;
- 或者一个批任务批量过账几十张;
- 或者 webhook / cron / 异步流程同时生成需要占号的记录。
这时,最危险的写法就是:
- 读最后一个号;
- 本地 +1;
- 再写回。
因为两个事务可能同时读到同一个“最后号码”。
_locked_increment() 在解决什么
源码里的 _locked_increment() 做了一件非常关键的事:
- 通过对受唯一约束保护的行执行
UPDATE,拿到数据库层的锁; - 如果发现新号码已被别的事务抢走,就在 savepoint 内回滚重试;
- 一旦当前事务拿到这条序列的锁,后续同事务的连续取号优先走事务级缓存。
这背后体现的是很典型的 Odoo 风格:
不要自己发明并发控制,尽量借数据库唯一约束和事务锁来保证编号唯一。
这也是 init() 里会警告缺少 unique index 的原因。
源码甚至明确提醒:
- 如果
sequence.mixin相关唯一索引缺失,重载场景下就可能出现重复编号。
换句话说,SequenceMixin 的正确性,一半在 Python,另一半在 PostgreSQL 的唯一约束与锁语义里。
六、为什么还要搞一个 transaction cache
_get_sequence_cache() 乍看像性能优化,实际上它同时解决了性能和稳定性。
原因是:
- 每取一个号都走 savepoint,成本很高;
- 大量 savepoint 在高并发下会明显拖慢事务;
- 但完全不做保护,又会撞号。
所以 Odoo 的折中方案是:
- 第一次取号时,先通过数据库锁拿稳;
- 拿稳后,把当前格式串与 sequence index 作为 cache key;
- 在同一事务里连续取后续号码时,优先走缓存累加。
这个设计说明了一点:
SequenceMixin 不是“每次都重新算一次最新值”,而是“先取得一段事务内的占号权,再顺着往下发”。
因此它既保住了唯一性,也尽量避免重复 savepoint 带来的性能损耗。
七、_set_next_sequence() 为什么还要同时改 ORM 和数据库
_set_next_sequence() 的注释很值得看。
它强调:
- 编号必须同时在 ORM 和数据库层被设置;
- 因为后续找“上一条编号”是直接走 SQL 查询;
- 如果 ORM 状态和数据库状态不同步,下一次求号就可能拿到过期数据。
这就是为什么它不是简单 self.name = new_name,而是:
- 先调用
_locked_increment()拿到新号码; - 再通过上下文控制避免误清缓存;
- 然后补跑受该字段触发的 compute 字段;
- 最后重算
sequence_prefix与sequence_number。
也就是说,SequenceMixin 不是“改字符串字段”而已,它还在维护:
- 序列链索引;
- 后续依赖字段;
- 供 SQL 查询继续使用的一致状态。
八、为什么 resequence 不能被理解成“补个空号”
很多团队看到中间断号,就会本能地想:
- 把后面的号码手工往前挪一下。
但从 SequenceMixin 的设计看,resequence 真正处理的是三层问题:
- 日期与编号语义重新对齐;
- 同一前缀链重新排序;
- 保持未来继续生成新号时仍能接在正确链上。
所以 resequence 不只是补一个空缺,它实质上是在重建一条时间和格式都可继续使用的链。
如果你只是手动改几张名字,看起来像修好了,实际上可能只是把问题推迟到下一次过账。
九、给实施和开发的几个实战建议
1)不要在同一会计期间里频繁改前缀
尤其不要边过账边改。
因为 _get_last_sequence() 很依赖前缀链的稳定性。
2)日期错了,优先修日期或走 resequence,不要直接硬改名字
否则 _constrains_date_sequence() 迟早会把你拦下来,或者更糟:
短期没拦住,但之后整条链开始不可信。
3)高并发场景一定确认唯一索引真实存在
源码已经明说,少了 unique index,重载时可能重复编号。
4)不要自己写“查最后一条 +1”的土办法覆盖官方逻辑
那样最容易在批量过账、异步任务、并发操作下撞号。
5)把编号理解成“受日期语义约束的链”,不是普通显示字段
这会直接改变你处理问题的顺序。
总结
SequenceMixin 最值得记住的一点是:
Odoo 会计编号的难点从来不是数字加一,而是让“可读格式、日期重置、数据库唯一性、并发安全”四件事同时成立。
所以当你遇到这些现象时:
- 编号看起来跳了;
- 改个前缀后接不上;
- 换月后编号不对;
- 批量过账时偶发重复号;
不要只盯着“最后一个数字”。
真正该看的,是这四层:
- 当前编号格式属于哪种 reset 制度;
- 日期和编号是否仍匹配;
- 前缀链是否被人为打断;
- 数据库唯一约束和并发锁是否真正生效。
把这四层看清之后,SequenceMixin 就不再像一团正则黑魔法,而是一套很务实的“可编辑编号 + 会计审计 + 并发安全”折中方案。
DISCUSSION
评论区