先说结论
很多人把 Odoo Peppol 接入理解成“把公司注册上网络,然后就能发收电子发票”。
但 /home/ubuntu/odoo-temp/addons/account_peppol 里的源码告诉你,真正复杂的部分并不是第一次注册,而是连接生命周期管理:
- 数据库一旦从备份恢复或被复制,Peppol token 可能失步;
- Odoo 不把 webhook 当成一次性回调,而是把它当成长期保活基础设施;
- 公司状态不是简单的已开通/未开通,而是一个业务含义很强的状态机;
- 注册向导不仅采集资料,还在实时判断公司是否已在外部接入点上注册。
所以 Peppol 在 Odoo 里不是“发票格式模块”,而是一套受外部网络状态驱动的连接控制平面。
最危险的问题:数据库复制后 token 失步
account_edi_proxy_user.py 里有一段非常重要的错误提示:
This might happen if you restored a database from a backup or copied it without neutralization.
这句话其实已经把最大风险说穿了。
Peppol 连接不是一个只存在于本地数据库里的配置,它同时依赖:
- 本地数据库记录;
- 远端代理服务器上的连接状态;
- 当前数据库身份与 token 同步版本。
如果你做了以下操作:
- 用生产库克隆测试库;
- 从旧备份恢复一份数据库;
- 多个环境共享了同一套 Peppol 连接信息;
那么远端服务看到的“谁才是当前有效数据库”与本地数据库自以为的状态,可能已经不一致。
这就是源码里 is_token_out_of_sync 与 token_sync_version 存在的原因。
为什么 Odoo 对失步反应这么强硬?
因为电子发票网络不是普通 OAuth 登录。
一旦两份数据库都以为自己拥有发送/接收权:
- 可能重复收消息;
- 可能错误更新状态;
- 可能把同一企业身份在多个环境中并发使用;
- 审计边界会迅速崩塌。
所以 Odoo 的策略不是“尽量继续跑”,而是:
- 检测到
invalid_signature时,直接_mark_connection_out_of_sync(); - 置
is_token_out_of_sync = True; - 清空
refresh_token; - 要求管理员去设置里点 Reconnect this database。
这是非常正确的保守策略。
“Reconnect this database” 不是修复按钮,而是重新宣告所有权
源码里的 _peppol_out_of_sync_reconnect_this_database() 做了几件关键事:
- 断言当前确实处于失步状态;
token_sync_version += 1;- 调用
/api/peppol/1/resync_connection; - 拿到新的
refresh_token; - 清除失步标记;
- 异步触发参与方状态更新 cron。
这里的重点是:它不是简单重试旧 token。
它是在告诉远端代理:
现在这份数据库才是新一轮同步基线,请重新发放一致的连接状态。
这也解释了为什么如果远端返回 connection_superseded,Odoo 会直接走 _peppol_out_of_sync_disconnect_this_database(),把当前连接软重置掉。
换句话说,Peppol 连接所有权在设计上就是排他的。
为什么公司状态不是布尔值,而是状态机?
res.company 里的 account_peppol_proxy_state 定义了:
not_registeredsendersmp_registrationreceiverrejected
这不是为了 UI 好看,而是因为 Peppol 的接入过程天然不是一步到位。
这些状态分别在表达什么?
not_registered
还没有建立有效网络身份,既不能稳定发送,也不能接收。
sender
已经具备发送能力,但接收侧还没完全打通。
这通常意味着:
- 可以发出文档;
- 但还不能作为成熟接收端从网络收件。
smp_registration
这是最值得注意的中间态。
源码注释写得很清楚:
- “Can send, pending registration to receive”
也就是说,发送能力先可用,接收能力仍在 SMP 注册流程中等待确认。
这很现实,因为在 Peppol 世界里,“能发”和“能被别人发现并向你投递”不是完全同一件事。
receiver
发送与接收都完整开通,系统进入成熟运行态。
rejected
注册或审核流程被外部网络拒绝,不是临时同步问题,而是业务层面没通过。
这套状态机之所以重要,是因为它让 Odoo 能对外部网络的渐进式开通做出诚实表达,而不是把所有过程都伪装成“已启用”。
注册向导不是单纯收集表单,而是在实时判断网络位置
peppol_registration.py 很有意思。
它不是把用户填的:
- EAS
- Endpoint
- 联系邮箱
- 手机号
直接提交完事,而是在 _compute_smp_registration_external_provider() 里实时查:
- 当前标识是否已经在 Peppol 网络上;
- 如果已存在,是不是已经挂在别的 external provider 上。
然后得出:
smp_registration = not is_company_on_peppolpeppol_external_provider = external_provider
这意味着 Odoo 注册向导在做两层工作:
- 本地表单校验;
- 外部网络占位检查。
因此它不是“填资料向导”,更像“企业身份接入协调器”。
Odoo 为什么频繁触发 participant status cron?
_cron_peppol_get_participant_status() 有个细节非常关键:
- 如果仍然存在
smp_registration状态的公司,它会在一小时后再次触发自己。
这说明 Odoo 很清楚,Peppol 参与方状态不是一次请求就能稳定落地的。
尤其在注册中间态,系统需要持续追踪:
- 外部注册是否完成;
- 是否从
smp_registration进入receiver; - 本地数据库状态是否仍与远端一致。
这也再次说明:Peppol 集成不是同步按钮,而是持续对账型连接。
为什么 webhook 不是锦上添花,而是核心链路?
controllers/webhooks.py 定义了三个公开 POST 入口:
/peppol/webhook/new-message/peppol/webhook/message-state-update/peppol/webhook/user-state-update
它们本身不直接做大量业务处理,而是去触发对应 cron:
- 拉新文档
- 拉消息状态
- 拉参与方状态
这套设计非常稳
因为 webhook 本来就不该承载复杂事务:
- 外部服务调用时间不可控;
- 重试可能发生;
- 入口要尽量短平快;
- 真实业务处理放到内部任务队列/cron 更安全。
Odoo 在这里做的是一个典型的“回调触发拉取”模型:
- webhook 负责通知;
- Odoo 自己决定何时、如何批量同步;
- 内部处理失败,也不会把外部回调链条拖死。
为什么还要专门做 webhook keepalive?
_cron_peppol_webhook_keepalive() 会定期调用 _peppol_reset_webhook(),重新把:
webhook_urltoken
推到代理服务上。
这说明 webhook 并不是“一次设置永久有效”的配置,而是带过期与续租语义的连接部件。
源码里甚至提到 webhook token 的 TTL 是 30 天。
这非常合理:
- 降低长期暴露风险;
- 给 URL / token 轮换留空间;
- 减少数据库搬迁后旧回调继续打进来的风险。
很多系统做 webhook,只做“注册成功”那一步;Odoo 这里更成熟,它把 webhook 当作需要长期维护的租约。
分支公司与父公司连接复用:不是共享按钮,而是治理选择
注册向导里还有一条经常被忽略的逻辑:
- 分公司有机会复用父公司的连接;
- 也可以选择自己注册;
- 但如果分支和主公司使用同一个 Peppol ID,会被阻止。
这意味着 Odoo 不是简单假设“一个公司树只有一个电子发票身份”,而是承认企业集团内部可能存在两种治理方式:
- 集中式接入:从母公司统一发送;
- 分布式接入:分支独立注册自己的网络身份。
这是非常接近真实组织结构的设计。
实战里最该盯的 5 个信号
1. 数据库是否被复制过
只要 Peppol 连着远端代理,复制数据库就不是小事。测试环境克隆生产库时尤其要谨慎。
2. 当前状态是不是卡在 smp_registration
如果长期卡住,问题不一定在 Odoo 本地,也可能在外部网络登记流程尚未完成。
3. webhook 有没有持续刷新
能注册成功不代表长期可用。Webhook 续租失败,会导致后续状态变更通知逐渐失效。
4. 是否出现 invalid_signature 或 connection_superseded
这两类错误别当偶发网络抖动看,它们通常在提示连接身份边界出了问题。
5. 采购日记账是否已设置
接收文档时,如果公司没配置 Peppol purchase journal,本地导入链路也会卡住。网络打通不等于会计落账链路完整。
最后一句
Peppol 模块最值得学习的,不是它会发电子发票,而是它对“连接所有权”这件事足够严肃:
- token 会失步;
- 状态要轮询;
- webhook 要续租;
- 数据库复制不能装作没发生;
- 中间态必须被诚实表达。
这才是企业级外部网络集成该有的样子:不是一次配置成功,而是长期保持一致。
DISCUSSION
评论区