很多人把 Odoo 企业版 Sign 理解成“选几个人,系统发几封邮件,对方点进去签掉就好”。
但只要认真看 enterprise/sign,你会发现官方真正在意的不是“把 PDF 群发出去”,而是另外三件更难的事:
- 多个签署人到底是不是同时收到请求;
- 前一批签完以后,下一批由谁触发、何时触发;
- 要求额外认证时,短信验证码、信用额度不足、签署继续与否如何收口。
所以这篇文章的核心结论是:
Odoo 企业版 Sign 的重点,不是生成签署链接,而是把“投递顺序、提醒节奏、认证边界”绑成一条可持续推进的状态机。
一、签署顺序不是 UI 排序,而是 mail_sent_order
入口在 enterprise/sign/wizard/sign_send_request.py。
最值得看的不是按钮,而是 _compute_signer_ids()。
这个方法会根据模板里的 responsible_id 角色生成 sign.send.request.signer,并给每个 signer 写一个 mail_sent_order:
- 如果没启用
set_sign_order,所有 signer 默认都是1; - 如果启用了顺序签署,则按角色顺序把 signer 依次标成
1、2、3...。
这里的关键在于:
- 顺序签署不是依赖前端列表显示顺序;
- 它是持久化到 signer 数据里的投递层级。
所以当用户在向导里调整签署顺序时,系统真正保存的是“第几批发送”,不是一个好看的排序号。
这也是为什么源码里会特别判断:如果当前 signer 还是默认顺序,就跟着新的顺序重算;如果用户已经手工改过 mail_sent_order,则尽量保留现值。
二、create_request() 创建的不是一封邮件,而是一份可分批推进的请求
同一个 wizard 里的 create_request() 会把 signer 列表转成 sign.request 和它的 request_item_ids。
每个 item 都带上:
partner_idrole_idmail_sent_order
这三个字段组合起来,才构成后续真正的签署投递策略。
也就是说,sign.request 创建时并没有“直接群发给所有 signer”,而是先把:
- 谁来签;
- 以什么角色签;
- 在第几批收到邀请;
全部落成结构化数据。
这一步的设计非常像审批系统,而不是普通发邮件动作。
三、真正决定“现在该发给谁”的是 send_signature_accesses()
enterprise/sign/models/sign_request.py 里的 send_signature_accesses() 是整条链路的关键。
它不会无脑遍历所有 signer,而是:
- 先只处理
state == 'sent'的请求; - 再对每个 request 调用
_get_next_sign_request_items(); - 只给当前“应该出场的那一批” item 发送签署入口;
- 发送后更新
last_reminder。
这说明顺序签署的本质不是“前一位签完以后把状态改一下”,而是:
系统每次真正对外发邮件时,都只挑当前 mail_sent_order 那一层。
于是顺序控制被稳定地落在投递动作上,而不只是落在页面规则上。
对企业协同尤其重要,因为很多合规场景在意的不是“签名顺序在数据库里写了什么”,而是“第二位到底有没有在第一位签之前收到链接”。
四、Reminder Cron 不只是提醒,它还是顺序流转的继续器
很多人以为 _cron_reminder() 只是定时补发提醒邮件。
其实它做的事情更大。
源码逻辑是:
- 扫描所有
state = 'sent'且active = True的请求; - 如果
validity < today,直接把请求标成expired; - 否则,如果开启 reminder,且
last_reminder + reminder interval <= today,把请求加入待发送集合; - 最后对这批 request 调用
send_signature_accesses()。
注意最后一步。
由于 send_signature_accesses() 本身只会发送“下一批应收到链接的人”,所以 cron 并不只是提醒老 signer,它也可能承担顺序链继续推进的职责。
换句话说:
- 当前批次还没签完时,它是 reminder;
- 当前批次已完成、下一批已具备发送条件时,它也是顺序投递的再触发器。
这就是为什么 Odoo 把 reminder 和顺序发送放在同一条业务链里,而不是拆成两套完全独立的 job。
五、SMS 二次认证不是附属插件,而是签署闭环的一道门
额外认证主要落在两个位置:
sign.request.item._send_sms()sign.controllers.main._validate_auth_method()
1. _send_sms() 先重置验证码,再发送
每次发短信时,_send_sms() 都会先调用 _reset_sms_token(),重新生成六位验证码,再通过 sms.sms 发送。
这意味着短信验证码不是创建请求时一次生成、长期有效,而是每次触发短信发送时刷新。
2. _validate_auth_method() 真正决定“能不能签”
当 signer 进入 /sign/sign/... 路由时,如果当前角色配置了 auth_method,controller 会先走 _validate_auth_method()。
对于 sms 场景,逻辑并不只是“验证码对不对”,而是更细:
- 有短信认证要求;
- 如果系统已经没有 SMS credits,且用户没有填验证码,Odoo 仍允许签署继续;
- 同时把
signed_without_extra_auth = True标出来; - 如果有 credits 却没填或填错验证码,就返回
{'success': False, 'sms': True},阻止签署; - 验证成功后,还会往 sign request 写一条日志,记录签署人和校验手机号。
这套逻辑特别值得注意,因为它体现了一个很现实的企业取舍:
额外认证很重要,但业务闭环也重要。
如果因为 IAP 短信余额耗尽,所有签署全部卡死,很多企业流程会直接停摆。所以官方做了“可追踪的降级”:允许继续签,但明确标记这次签署没有完成原定的额外认证。
六、有效期、提醒周期、签署顺序三者不是各管各的
在 wizard 里,_check_validity() 限制了 validity 不能早于今天;_onchange_reminder() 又把 reminder 最大值限制在 365。
看起来这些只是表单校验,但它们其实是给整个状态机画边界:
validity决定这份请求还能不能继续推进;reminder决定下一次系统尝试推动流程是什么时候;mail_sent_order决定下一次尝试时应该发给哪一批人。
三者合在一起,才是 Odoo Sign 的“节奏控制层”。
七、最容易误解的地方
1. “启用签署顺序,就是前端上排个 1、2、3”
不对。真正起作用的是 request item 上持久化的 mail_sent_order,以及发送时只挑当前批次的逻辑。
2. “Reminder 只是催当前 signer”
不完全对。它也可能成为顺序流转继续前进的触发器。
3. “开了 SMS 认证,就一定必须输验证码才能签”
不绝对。源码明确允许在短信 credits 不足时做可审计的降级签署。
八、实战建议
如果你要在企业里排查“为什么第二位签署人还没收到邮件”这类问题,建议按下面顺序看:
- wizard 里生成的 signer 是否写对了
mail_sent_order; sign.request当前 state 是否仍为sent;- 前一批 signer 是否已满足
_get_next_sign_request_items()的推进条件; last_reminder、reminder与validity是否让 cron 有机会继续触发;- 如果是 SMS 认证场景,再看短信 credits 与
_validate_auth_method()的返回分支。
这样比单看“邮件有没有发出”更接近 Odoo 官方实现。
九、结论
Odoo 企业版 Sign 做得最像企业系统的地方,不在 PDF 签名本身,而在流程推进:
- 用
mail_sent_order把签署人拆成一批批; - 用
send_signature_accesses()保证每次只放行该出场的人; - 用
_cron_reminder()在过期与继续推进之间做调度; - 用 SMS 认证和可审计降级,把安全要求接进真实业务场景。
所以它不是“群发签字请求”,而是一套围绕顺序、节奏和认证组织起来的签署协作机制。
DISCUSSION
评论区