很多财务团队第一次看到 Odoo 企业版把 SEPA 批量付款 接进在线银行链路,都会下意识地把它理解成:
- 批次建好;
- 点一次 Validate;
- 银行那边就算处理完成。
但如果你去读 /home/ubuntu/odoo-temp/enterprise/account_online_payment 的源码,会发现官方压根没把它设计成一个“点完即 done”的按钮。
它真正拆开的,是至少五层问题:
- 这条 bank link 是否允许 payments?
- payments 能力是否已激活,KYC 是否走完?
- 这个 batch 目前只是发起,还是已经签署?
- 银行/聚合服务是否已经回传最新状态?
- 一旦送行,底层 payment 还能不能再退回 draft 修改?
本文主要基于:
account_online_payment/models/account_online_link.pyaccount_online_payment/models/account_batch_payment.pyaccount_online_payment/models/account_payment.pyaccount_online_payment/controllers/odoofin_webhooks.pyaccount_online_payment/tests/test_account_online_payment_activation.pyaccount_online_payment/tests/test_account_online_payment_webhook.py
先说结论:Odoo 企业版在线批量付款不是“验证即完成”,而是把连接启用、支付激活、KYC、批次发起、签署、银行回传、后续锁定拆成多层状态机。你在界面上看到的一次点击,源码里通常只是把批次送进下一阶段。
一、第一道门槛不是付款批次,而是 bank link 有没有开通 payments 能力
先看 models/account_online_link.py。
企业版在 account.online.link 上新增了两个字段:
is_payment_enabledis_payment_activated
这两个字段看起来很像,但语义完全不同。
is_payment_enabled:这条连接有没有付款能力
_activate_payments() 一上来先检查:
- 如果
not self.is_payment_enabled,直接报错; - 错误文案明确写着,必须在连接银行账户时先启用 payments。
也就是说:
并不是所有在线银行连接,天然都能走在线付款。
如果连接时就没勾上 payments,这条 link 后面根本不会进入激活流程。
is_payment_activated:能力开了,但是否已真正激活
同一个方法还会拦第二层:
- 如果已经激活,就报 “Payments are already activated.”
所以源码明确区分:
- 具备资格 去激活;
- 已经完成 激活。
这就是很多财务用户会迷惑的第一层原因:
- “不是都连上银行了吗,为什么还不能在线发款?”
答案是:
连上银行账户,不等于付款能力已开;付款能力已开,也不等于激活手续完成。
二、激活 payments 不是本地改个布尔值,而是会进入外部 KYC/跳转流程
_activate_payments() 的实现不是简单写 is_payment_activated = True。
它会反复请求:
/proxy/v1/activate_payments
如果响应里带 next_data,就继续下一轮请求;直到没有 next_data 为止。最后返回的是:
redirect_url
并用 ir.actions.act_url 打开新窗口。
这说明 Odoo 在这里做的不是“本地开户”,而是:
把用户送到外部支付/KYC 流程去完成下一步。
为什么 success link 之后还不一定马上能付款?
更关键的是 _success_link()。
连 bank link 成功后,Odoo 还会尝试 _activate_payments()。如果拿到了跳转链接,它会:
- 发 activation 邮件模板;
- 给当前用户排一个 Todo activity;
- 弹出通知,提示去完成 KYC;
- 文案明确说明:没完成 KYC,还不能直接从 Odoo 处理付款。
这一步非常关键,因为它说明:
在线付款能力不是“银行连接成功”时就算完整开通,KYC 是一个被单独建模、并且可能持续几天的异步流程。
这也是为什么很多团队会以为系统卡住,实际只是还在等合规链路走完。
三、在线批量付款只在特定条件下接管 validate_batch()
再看 models/account_batch_payment.py。
validate_batch() 并不是无条件走在线支付。
如果满足任一条件,Odoo 就回退到父类原逻辑:
payment_method_code != 'sepa_ct'not account_online_link_payments_enabled- 上下文里带
xml_export
这三条的意思其实很清楚:
- 不是 SEPA Credit Transfer 批次,就别走这条在线链路;
- journal 背后的在线 link 没开 payment,也别走;
- 如果用户明确要求 XML 导出,就继续传统导出流。
所以在线付款不是在取代 XML,而是在特定前提下接管 SEPA 批次。
为什么这点特别重要?
因为很多人会误会:
- “装了这个模块,以后批量付款都必须在线走。”
源码给出的答案恰好相反:
Odoo 保留了传统 XML 导出路径,把在线付款作为可条件触发的增强流,而不是暴力替换。
四、发起批次后,不代表已签署,更不代表银行已执行
这是这个模块最值得讲清的一点。
AccountBatchPayment 新增了:
payment_identifierpayment_online_status
状态集合包括:
uninitiatedunsignedpendingacceptedcanceledrejected
这串状态已经在告诉你:
线上批次付款不是 done / not done 两态,而是一个多阶段状态机。
第一次 validate 做了什么?
当在线付款条件满足时,validate_batch() 会:
- 先
_check_batch_validity(); - 组装
_prepare_payment_data(); - 调
/proxy/v1/initiate_payment; - 若响应带
next_data,继续请求直到结束; - 然后分两种结果。
情况 A:触发 kyc_flow
如果响应里有 kyc_flow,Odoo 不会假装批次已送行,而是:
- 用超级用户在 chatter 发一条消息;
- 明确告诉你:需要 KYC,可能要几天;
- meantime 请先使用 SEPA XML export。
这段逻辑很漂亮,因为它没有强行失败,也没有假装成功,而是明确告诉你:
在线通道暂时不能继续,但业务可以退回到 XML 备份路径。
情况 B:没有 kyc_flow
这时 Odoo 才会:
_send_after_validation()- 写入
payment_identifier - 写入
payment_online_status - 打开
redirect_url
注意,这里也只是“发起并记录外部识别号 + 当前在线状态”,不是“银行已经完成执行”。
五、unsigned 不是失败,而是等待真正签署
initiate_payment() 里还有一个特别容易被忽略的分支。
如果批次当前是:
payment_online_status == 'unsigned'- 且
state == 'sent'
那么 Odoo 不会重复普通 validate,而是:
- 先
check_online_payment_status()刷一下最新状态; - 如果已经不是
unsigned,弹个 warning 通知并 soft reload; - 如果仍是
unsigned,走_sign_payment()。
这背后表达的是:
“已发起”与“已签署”是两个动作。
也就是说,用户第一次点完之后,批次可能只是进入了一个待签署的在线状态,而不是已经完成银行确认。
_sign_payment() 又做了什么?
它会调用:
/proxy/v1/sign_payment
并继续处理 next_data,然后刷新:
payment_online_statuspayment_identifier
再跳转到外部 URL。
所以从业务视角看,一笔在线批量付款至少可能经历:
- 建批次;
- 发起;
- 签署;
- 等银行处理;
- 接收状态回传。
而不是一个单按钮动作。
六、Webhook 和 cron 共同负责“外部状态回写”,不是靠用户一直手点刷新
在线支付最难的一件事,是 状态不在 Odoo 本地闭环。
源码里对此给了两种回写机制。
1)Webhook:用于 payment activation 完成回写
控制器 controllers/odoofin_webhooks.py 暴露:
/webhook/odoofin/payment_activated
逻辑很简单但很关键:
- 按
client_id找到account.online.link; - 发成功邮件;
- 把
is_payment_activated写成 True。
配套测试 test_account_online_payment_webhook.py 也验证了:
- client 存在时,状态会更新;
- client 不存在时,Webhook 仍返回成功,但不会误改别的 link。
这说明激活状态不是靠用户手工勾选,而是:
外部服务完成流程后,再异步回写 Odoo。
2)cron / 手工查询:用于批次付款状态跟进
check_online_payment_status() 会按批次调用:
/proxy/v1/get_payment_status
然后把返回值写回 payment_online_status。
而 _cron_check_payment_status() 又会定时扫描:
- 未 reconciled
sepa_ct- journal link 已 payment activated
- 状态属于
unsigned或pending
的批次,自动刷新状态。
这说明官方很清楚:
银行或支付聚合器的状态变化,本来就可能晚于用户点击动作,所以系统必须有异步回查机制。
七、一旦送行到一定阶段,底层 payment 就不允许再回 draft 修改
models/account_payment.py 只扩了一小段,但业务意义特别重。
在 action_draft() 里,如果某笔 payment:
- 挂在 batch 上;
- 且 payment method 是
sepa_ct; - 且 batch 的
payment_online_status属于pending或accepted;
就直接抛错:
You cannot modify a payment that has already been sent to the bank.
这条规则非常重要,因为它在说:
一旦 Odoo 认为这笔付款已进入银行处理链,就不能再把底层 payment 当成本地草稿单随意重写。
这不是界面限制,而是资金控制边界。
八、为什么导出 XML 时,系统还会提醒“有些已签署付款可能已经送行”
export_batch_payment() 里还有个很容易忽略的安全提示。
对已经可在线付款的 sepa_ct 批次,如果不是 xml_export 场景,Odoo 会跳过常规导出逻辑。
而当确实执行导出后,如果有批次的 payment_online_status 属于:
pendingaccepted
系统会发消息提醒:
- 已签署付款可能已经被发送到银行。
这相当于给财务一个非常现实的防重警告:
在线送行和 XML 导出不是绝对隔离的两个宇宙;如果状态已推进到 pending/accepted,再导出就可能造成重复处理风险。
这也是为什么企业版保留 fallback,但又不断提醒你注意状态边界。
九、_prepare_payment_data() 说明 Odoo 发给外部的不只是金额
很多人以为在线付款只是把总额扔给银行。
但 _prepare_payment_data() 实际上传的是一整包结构化付款信息,每笔 payment 包含:
amountaccount_numberaccount_type(IBAN)creditor_namecurrencydatereferencestructured_referenceend_to_end_uuid
批次层还会带上:
account_idbatch_bookingdatepayment_type = bulkreference = self.name
特别值得注意的是:
structured_reference会调用is_valid_structured_reference(payment.memo)
也就是说,Odoo 在发起前就区分了普通备注和结构化参考号。
这说明这条集成链不是“导个总文件”而已,而是:
把单笔付款级别的银行字段结构化后,再交给外部支付服务。
十、实战里最容易误解的 6 件事
1. 以为银行连接成功就等于在线付款已可用
不对。
还要看 is_payment_enabled 和 is_payment_activated。
2. 以为 validate 一次就等于银行执行完成
不对。
那往往只是 initiate 或进入待签署状态。
3. 以为 unsigned 是失败状态
不对。
它通常表示批次还没完成签署。
4. 以为 KYC 只是开户时一次性动作
不对。
源码明确把它视为可能阻塞在线付款、且需异步完成的流程。
5. 以为在线付款开了,XML 导出路径就不存在了
不对。
xml_export 仍然是保留的 fallback。
6. 以为 payment 已挂进 batch,就还能随时回 draft 改资料
不对。
当状态进入 pending / accepted,底层 payment 会被锁住。
十一、如果你要二开这条链,最该尊重哪些边界?
如果你准备在企业版在线付款上做扩展,我建议优先守住这些边界:
1. 不要把 is_payment_enabled 和 is_payment_activated 混成一个字段
资格、启用、激活,是三层不同语义。
2. 不要把 initiate 和 sign 合并成“一个完成动作”
源码明确允许批次停在 unsigned,这层过渡很关键。
3. 不要取消 KYC 分支里的 XML fallback
这是官方留给业务连续性的后备通道。
4. 不要忽略 webhook / cron 回写
在线支付状态天然是异步的,本地强行同步化反而会失真。
5. 不要放开 pending / accepted 后的 draft 回退
这会直接破坏送行后的资金控制边界。
总结
account_online_payment 最值得学的一点,是它没有把“在线批量付款”做成一个假装全能的单按钮,而是老老实实拆成一条跨系统状态链:
- link 是否允许 payments
- payments 是否已激活,KYC 是否完成
- batch 是否已 initiate
- batch 是否已 sign
- 外部服务是否通过 webhook / status query 回写最新结果
- 送行后的 payment 是否应禁止再改
所以在 Odoo 企业版里,“我点了发送”通常只代表批次进入下一阶段,不代表银行已经处理完成;而真正让这条链可靠的,恰恰是那些看起来繁琐的激活、KYC、状态查询、Webhook 和修改限制。
如果把这一点看懂,你在做财务实施、培训用户、设计操作 SOP,或者做支付集成二开时,就不会再把在线批量付款理解成“XML 导出换了个按钮皮肤”。
DISCUSSION
评论区