EDI

Odoo EDI 为什么不是“生成一份 XML 发出去”而已:文档生命周期、阻塞级别与取消边界讲透

电子发票 / EDI 出问题时,很多人只看到附件和报错提示,但 account_edi 真正建模的是文档状态、阻塞级别、同步与异步发送、以及已发送后还能不能回草稿或取消。本文把这条链路讲透。

框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

在 Odoo 里,EDI 从来不只是“发票过账时顺手生成一份 XML”。

/home/ubuntu/odoo-temp/addons/account_edi/models/account_edi_document.pyaccount_move.pyaccount_edi_format.py 可以看出,官方真正设计的是:

  • 每张发票可对应多份 EDI 文档
  • 每份文档都有独立状态
  • 错误不只是“有 / 没有”,还分阻塞级别
  • 有些格式同步完成,有些要走异步 web service
  • 一旦已发送,后续编辑、撤销、作废都受外部监管链路影响

所以一句话概括:

Odoo 的 EDI 不是一个附件生成功能,而是一套受监管约束的文档状态机。


为什么 account.edi.document 要单独成模

一个 account.move 可以挂多条 edi_document_ids,而且 (edi_format_id, move_id) 强制唯一。

这说明 Odoo 并不把 EDI 当成“发票上的一个字段”,而是把它建模成独立对象。

这么做的原因很现实:

  • 一张发票可能对应多个 EDI 格式
  • 每个格式的发送、取消、附件、错误状态都不同
  • 有些国家 / 平台需要单独 web service 生命周期

如果只在发票上塞几个字段,系统很快就会失去表达能力。

独立成模之后,平台才有办法分别描述:

  • 哪个格式已经 sent
  • 哪个还在 to_send
  • 哪个取消中 to_cancel
  • 哪个已 cancelled

这就是平台层与“简单导出文件”之间的根本区别。


为什么 blocking_level 比单纯 error 更重要

很多系统出错时只会告诉你:

  • 成功
  • 失败

account.edi.document 还额外区分:

  • info
  • warning
  • error

这意味着 Odoo 不是把所有问题都当成同一种严重性。

这背后是在回答什么问题

它在回答:

  • 这个问题是否会阻断当前 EDI 流程
  • 还是只是提醒你存在异常,但允许继续

这比“有错误就全卡死”成熟很多。

因为真实世界的电子发票链路常常会遇到:

  • 可接受但需要提醒的偏差
  • 暂时不影响发送的警告
  • 以及必须立即阻断的硬错误

把这些都压成一个红字,只会让运维和财务团队疲于判断优先级。


_get_move_applicability() 为什么是整个框架的核心抽象

account_edi_format.py 里明确说了,格式实现会返回一个 applicability 字典,里面可能包含:

  • post
  • cancel
  • post_batching
  • cancel_batching
  • edi_content

这个抽象非常漂亮,因为它把“某种 EDI 格式如何对某张 move 生效”压成了统一契约。

也就是说,核心框架不用知道:

  • 这是哪个国家
  • 这是哪家税局
  • 这是同步接口还是异步接口

它只需要知道:

  • 这张单据是否适用该格式
  • 若适用,该怎么发、怎么取消、能否批处理、内容怎么导出

这就是为什么 account_edi 能承载那么多本地化与监管模块,而核心代码仍保持相对克制。


发票过账时到底发生了什么

account.move._post() 里,Odoo 会:

  1. 遍历 journal 上启用的 EDI 格式
  2. 对每个格式判断 applicability
  3. 做配置校验 _check_move_configuration()
  4. 为适用格式创建或重置 account.edi.document
  5. 将状态置为 to_send
  6. 立即处理不需要 web service 的文档
  7. 对需要 web service 的文档触发 cron

这条链路说明两件事:

第一,EDI 是过账后的正式动作,不是预览动作

因为单据一旦过账,就进入了更正式的会计 / 合规阶段,EDI 才具有业务意义。

第二,Odoo 明确承认“有些 EDI 不可能同步完成”

这就是为什么框架里原生区分:

  • 可立即处理
  • 必须异步排队

如果团队排错时总问“为什么按钮点了没有立刻 sent”,要先接受这件事:

  • 对某些监管链路来说,异步才是常态。

为什么已发送后的回草稿会被卡住

button_draft() 在 account_edi 中有非常明确的保护:

  • 如果相关 EDI 已发送,且该格式支持取消流程
  • 就不能直接回草稿
  • 必须先走 “Request EDI Cancellation”

这一步特别像现实监管世界的逻辑:

  • 单据不是你本地想改就改
  • 你已经把事实发给外部系统了
  • 要改,就得先和外部世界对齐取消或作废

所以很多财务用户会抱怨:

  • 为什么这张发票昨天还能改,今天突然不能?

答案通常不是 Odoo 在找麻烦,而是:

一旦外部监管链路已经承认这份电子文档,本地编辑自由度就必须收紧。


为什么取消流程不是直接 button_cancel() 完事

在 EDI 场景下,button_cancel() 会:

  • 对未 sent 的文档直接标记 cancelled
  • 对已 sent 的文档置成 to_cancel
  • 先处理同步可取消的部分
  • 再触发异步流程继续取消

这说明“取消”在 Odoo 里其实有两层含义:

  1. 本地想取消
  2. 外部监管系统也认可取消

只有第二件事完成,很多链路才算真正闭环。

这也是为什么源码里还有:

  • edi_show_cancel_button
  • edi_show_abandon_cancel_button
  • edi_show_force_cancel_button

因为取消不是一个单步动作,而是一个可申请、可撤回、可强制推进的状态过程。


_process_documents_web_services() 为什么强调锁和 commit

异步 web service 处理里,Odoo 会:

  • lock_for_update()
  • 尝试锁定 move 和潜在附件
  • 多 job 情况下在 job 间 commit
  • 若已被别的事务锁住,则跳过或报“正在被另一进程发送”

这说明官方非常清楚 EDI 处理的现实问题:

  • cron 可能并发
  • 人工点击和后台任务可能同时来
  • 附件与状态更新可能互相踩踏

所以 EDI 的麻烦常常不只在“税局接口回了什么”,也在“平台如何避免双发、重试和并发冲突”。


新手最容易误解的 5 件事

1. 误以为 EDI 就是一份导出附件

真正核心是独立文档生命周期,不是附件本身。

2. 误以为有错误就一定不能继续

还要看 blocking_level,不是所有问题都同级。

3. 误以为发票过账后 EDI 应该立刻 sent

很多格式天然就要异步 web service。

4. 误以为已发送电子发票还能像普通凭证一样随意回草稿

一旦外部世界已收到,边界就完全不同。

5. 误以为取消只是本地状态切换

对 EDI 来说,取消通常还要等待监管链路确认。


实战排错顺序

如果你遇到“EDI 一直发不出去”或“为什么不能改回草稿”这类问题,建议按这个顺序查:

  1. 先看具体是哪条 account.edi.document 卡住,而不是只看 move 总状态
  2. 确认 blocking_level 是 info、warning 还是 error
  3. 检查 _check_move_configuration() 是否在发票过账前就埋下配置错误
  4. 区分这是同步格式还是需要 web service 的异步格式
  5. 若是取消链路,再看当前是 sentto_cancel 还是已 cancelled,不要把本地想取消和外部已取消混为一谈

一句话记忆

Odoo EDI 的本质不是“发 XML”,而是把电子文档、错误严重级别、同步 / 异步处理和外部监管取消流程统一进一套可追踪状态机。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。