框架深潜

Odoo 罗马尼亚 e-Factura 为什么难点不只在“成功上传”:SPV 索引、异步同步与拒绝恢复边界讲透

罗马尼亚 e-Factura 在 Odoo 里最容易被低估的,不是 XML 生成,而是发出去之后并不会立刻得到最终结论:有时先拿到发送成功与索引 key,有时压根拿不到 index,要靠后续 synchronize 去补回;已发送文档还可能被替换成 validated 或 refused 文档,甚至超过保留天数后只能做“推定拒绝”处理。

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

先说结论

Odoo 的罗马尼亚 l10n_ro_edi 真正难的地方,不是“把 CIUS-RO XML 传给 SPV”这一步,而是:上传之后,系统往往还处在一个信息不完整、需要异步追认的中间态。

/home/ubuntu/odoo-temp/addons/l10n_ro_edi/models/account_move.py 看,这套实现的关键不是单次请求,而是下面这套状态恢复思路:

  1. 先发送,再等 SPV 异步验证。
  2. 发送时最好拿到 key_loading 作为索引;但拿不到也不一定代表失败。
  3. 后续要通过 fetch / synchronize 补拿状态、签名和拒绝原因。
  4. 原先的“sent”文档并不是最终结果,后面会被 validated / refused 文档替换。

所以罗马尼亚 e-Factura 的本质不是“同步上传接口”,而是一个带追踪、补偿和恢复逻辑的异步监管通道。

为什么发送成功后,发票也还没真正“结束”

_l10n_ro_edi_send_invoice() 这段逻辑非常有代表性。

发送前,Odoo 先做预检查:

  • move 必须已 posted;
  • 公司必须有 access token;
  • 必须找到 CIUS-RO XML;
  • 这张发票之前不能已经有 e-Factura document。

这说明“能不能发”本身就是一层业务门槛。

但更重要的是:即使发送请求成功,发票也只是进入 invoice_sent,不是最终完成。

因为 SPV 还要继续处理、验证,再决定是接收还是拒绝。

很多实施项目的问题就出在这里:把“已提交”当成“已合规落地”。

key_loading 为什么这么重要

发送返回里,如果 SPV 给了 key_loading,Odoo 会把它存到:

l10n_ro_edi_index

同时 chatter 里提示:现在 SPV 正在用这个 index 验证。

这个 index 的意义,不只是“有个编号好看”,而是它成为后续追状态的主要抓手。

没有 index 会怎样?

源码里专门处理了这种情况:

  • 如果发送成功但没拿到 key_loading
  • Odoo 把状态记成 invoice_not_indexed
  • 并提醒后续需要 synchronize 去恢复索引和状态。

这说明在罗马尼亚 e-Factura 里,“上传成功但暂时没拿到索引”是被标准实现预期到的正常异常。

它不是流程外的边缘 bug,而是必须被恢复机制兜住的情况。

为什么发送文档后面还会被删掉再重建

_l10n_ro_edi_fetch_invoice_sent_documents() 和后面的同步处理逻辑里,有一个第一次看会觉得很奇怪的动作:

  • 原先 invoice_sent 的 document 会被删除;
  • 然后创建一条新的 invoice_validatedinvoice_refused document。

这背后其实是一种“阶段性凭证替换”思想。

为什么不直接把 sent 文档原地改状态?

因为发送态和最终态携带的信息不同:

  • sent 只是证明“我交上去了”;
  • validated/refused 还带回签名、证书 key、下载 key,甚至拒绝原因。

换句话说,最终文档不只是“状态更改后的同一对象”,而是带着更多监管回执内容的新阶段记录

这能让每个阶段的证据更清晰。

fetch 和 synchronize 的差别,很多团队其实没分清

fetch:针对已发送且有 index 的发票继续追状态

_l10n_ro_edi_fetch_invoice_sent_documents() 主要处理:

  • 现在状态还是 invoice_sent
  • 已经有 l10n_ro_edi_index
  • 去 SPV 问“处理完了吗”;
  • 处理完了再下载签名与结果。

这更像“按票追单”。

synchronize:更大范围地和 SPV 消息箱对账

_l10n_ro_edi_fetch_invoices() 做的事更广:

  • 处理已发送成功的 accepted messages;
  • 处理已发送但被 refused 的 messages;
  • 处理接收到的供应商 bill messages;
  • 还顺带补救那些没 index 的发票。

这更像“系统级同步收件箱”。

所以如果你把 synchronize 只理解成“再点一次刷新”,就低估了它。它其实承担了状态恢复与消息回补的核心职责。

为什么缺失 index 的发票还能靠名字补回来

_l10n_ro_edi_process_invoice_accepted_messages() 里处理了一个很现实的场景:

  • 发票其实发成功了;
  • 但当时没收到 index;
  • 后续同步时,SPV 消息里带回了发票名与 index;
  • Odoo 会按 name 去匹配数据库里的 invoice_not_indexed 发票,再补写 index。

这一步非常实用,因为真实世界网络不会永远完美。

但源码也很诚实地写了边界:如果重编号或重复发送导致同名碰撞,可能出现一个名字对应多条可能签名的棘手情况,需要人工判定。

这说明恢复逻辑虽然强,但不是无限神奇。发票编号治理如果混乱,恢复成本会急剧上升。

为什么超过保留时间后,会直接做“推定拒绝”

对于 invoice_not_indexed 且长期没能恢复的单据,源码定义了:

HOLDING_DAYS = 3

超过这个时间后,Odoo 会:

  • 删除旧 sent 文档;
  • 新建一条 invoice_refused 文档;
  • 在消息里说明:很可能被 SPV 拒绝了,但因为没拿到 index,无法恢复真正原因;
  • 建议 duplicate 发票后重新发送。

这看起来有点“保守”,但非常现实。

因为监管接口里最危险的状态不是明确失败,而是长时间悬而未决却无法继续操作。Odoo 选择在无法恢复时给出一个明确的业务落点,让人可以继续处理,而不是永久卡死。

采购侧为什么也被纳入 synchronize

_l10n_ro_edi_process_bill_messages() 说明这套实现不只是发销项,还能从 SPV 同步进项 bill。

它会:

  • 根据 SPV index 或卖方 VAT + 金额 + 日期去找相似 bill;
  • 如果找不到,就创建新的 vendor bill;
  • 保存签名附件;
  • 把 XML 挂到 account.move,再走导入扩展。

这说明 Odoo 对罗马尼亚 e-Factura 的理解不是“单向上传”,而是双向监管通道

  • 销项发票要发出去;
  • 进项发票也可能从平台同步回来。

这套链路里最容易踩的坑

1. 把 invoice_sent 当成最终成功

它只是“已提交,待处理”。

2. 看到没 index 就判断发送失败

其实很多时候只是暂时未回 index,需要后续 synchronize 恢复。

3. 发票编号管理混乱

名字碰撞会让“按 name 补回 index”的恢复逻辑变得非常尴尬。

4. 不看 chatter 只看字段

很多关键恢复信息、拒绝原因、操作建议都写在消息里。

5. 忽略采购侧同步

最后会把系统理解成只会发、不知道还能收。

最后一句

罗马尼亚 e-Factura 在 Odoo 里最值得看懂的,不是 XML 结构,而是这条异步恢复链:

  • 先发送;
  • 可能拿到 index,也可能暂时拿不到;
  • 后续通过 fetch / synchronize 继续追状态;
  • sent 文档会被带签名的 validated / refused 文档替换;
  • 缺失 index 还能尝试按名字恢复;
  • 超时太久则做推定拒绝并建议重发;
  • 采购 bill 还可以从 SPV 反向同步进来。

这说明它不是“一次 HTTP 上传”,而是一套围绕不确定响应设计的监管状态机

DISCUSSION

评论区

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