Nemhandel

Odoo Nemhandel 接入为什么不是“换个丹麦版 Peppol”而已:参与方校验、入网注册与收票边界讲透

l10n_dk_nemhandel 表面上像一套丹麦本地化电子单据模块,但源码真正难点并不只在发 OIOUBL,而在于 partner 是否真在网、公司是否完成 receiver 注册、Webhook 和 Proxy 是否维持住、收进来的 XML 能不能安全落成应付发票。本文把这条接入链讲透。

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

先说结论

l10n_dk_nemhandel 真正复杂的地方,不是“再多支持一个丹麦电子单据格式”。

res_partner.pyaccount_edi_proxy_user.pyaccount_move.py 一路看下来,Nemhandel 接入至少包含四个层次:

  1. 对端 partner 是否真的是网络里的有效参与方
  2. 我方公司是否已经通过 proxy 完成 receiver 注册
  3. Webhook、消息状态轮询、ack 是否维持住收票通道
  4. 收到的 XML 能否被正确识别并安全导入成账单

所以它不是“丹麦版 Peppol UI”,而是一条完整的本地化接入链路。

真正难点发生在“文档还没开始发”之前,以及“文档已经收到但还没安全落账”之间。


Partner 校验为什么是第一关

res_partner.py 里,button_nemhandel_check_partner_endpoint() 最终会走 _get_nemhandel_verification_state()

  • 如果 identifier type / value 不完整,直接 not_verified
  • 若不是 oioubl_21,也不算有效 Nemhandel 目标
  • 然后拼 edi_identification
  • 通过 _nemhandel_lookup_participant() 去查网络参与方信息
  • 最后用 _check_nemhandel_participant_exists() 验证 identifier 和 SMP 域名是否匹配

这条链很重要,因为它说明 Nemhandel 的“可发送”不是配置一个邮箱地址那么简单,而是要验证:

  • 这个对端标识真的存在
  • 它的服务元数据来自正确的 Nemhandel 域
  • 当前 partner 的电子发票格式也对得上

也就是说,partner 能不能被设为 Nemhandel 发送目标,本身就是合规判断,不是普通主数据录入。

_nemhandel_lookup_participant() 的代理查询说明了什么

它不是直接在本地做所有解析,而是通过 Odoo 的 proxy URL 去查 lookup 接口,并带上对应 zone

这说明官方在设计上已经承认:

  • Nemhandel 参与方发现不是纯本地逻辑
  • 不同 mode(test / prod / demo)对应不同网络环境
  • 平台要把外部发现链路封装到代理能力里

这和很多人想象中的“读取一组 endpoint scheme 映射表”完全不同。

真正的复杂度在于,参与方是否在线、在哪个 zone、服务元数据是不是合法,都是动态网络事实,而不是静态配置。

为什么 receiver 注册有这么多前置条件

_register_proxy_user()_nemhandel_register_as_receiver() 一起看,能看到非常典型的平台边界:

  • 先为公司生成 RSA 私钥
  • 以公司标识拼出 edi_identification
  • 通过 /api/nemhandel/1/connect 建立 proxy user
  • receiver 注册前要求公司状态在 in_verification
  • 若用 CVR(0184)注册,还要校验公司 VAT 一致
  • 还要检查是否已在 alternative service 上注册,避免重复占位

这说明 Nemhandel onboarding 真正要解决的不是“点按钮开通”,而是:

  • 谁代表公司接入网络
  • 这个公司身份和税号是否一致
  • 网络上是否已存在另一份冲突注册
  • 我们是否已经具备收票所需的身份材料

这就是典型的本地化合规层问题:业务上像配置,实际上是注册治理。

Webhook reset 和 cron 为什么不可少

很多团队做电子单据接入时,只盯着“发送调用成功”。但 _nemhandel_reset_webhook()_nemhandel_get_new_documents()_nemhandel_get_message_status() 一起看,会发现官方真正关心的是通道持续可用性

它做了三件事:

  1. 定期重设 webhook,带签名 token 保持回调可信
  2. 定时拉取新文档,必要时按批次继续 retrigger
  3. 查询 message status,并对成功 / processing / error 做不同处理

这说明 Nemhandel 不是“发请求 -> 收结果”的单同步 API,而是一条需要长期保活、确认、补拉、ack 的异步通道。

如果你只做一次 connect 而不维护 webhook 和轮询任务,系统表面上“已接入”,实际上收票能力会慢慢失真。

_nemhandel_get_new_documents() 为什么先收附件、再 ack

这个方法的链路很值得学习:

  • 先按 receiver_identifier 拉尚未确认的消息
  • 再逐条 get_document
  • 解密 documentenc_key
  • 创建 ir.attachment
  • _nemhandel_import_invoice() 落成 account.move
  • 成功后再 ack

这条顺序非常关键。

因为如果先 ack 再导入,任何导入失败都会造成“网络已确认收取,但本地并未成功入账”的丢单风险。Odoo 选择的是:只有当本地落单成功后,才去确认已接收

这是一条非常成熟的消息处理边界。

收票为什么最终又回到 account.move 识别链

_nemhandel_import_invoice() 创建了应付发票 account.move,随后 move._extend_with_attachments(...) 把 XML 真正交给账单导入链处理。

再结合 account_move.py_get_import_file_type()

  • CustomizationID == 'OIOUBL-2.1'
  • 就识别为 account.edi.xml.oioubl_21

这说明 Nemhandel 并没有自己再造一套完全独立的收票解析器,而是把本地化接入层和通用 UBL/CII 导入能力接起来。

这是一种很好的平台分层:

  • Nemhandel 负责网络接入、参与方验证、代理、消息状态
  • OIOUBL builder / importer 负责文档语义解析
  • account.move 负责最终落账

分层清楚,系统才容易维护。

实战最容易踩的 6 个坑

坑 1:把 Nemhandel 当成“多一个发送渠道”

实际上 partner 校验、receiver 注册和通道保活都在发送前。

坑 2:只验证 partner 标识,不验证服务元数据域

源码明确会对 identifier 和 SMP 域做双重匹配。

坑 3:公司主体资料没对齐就直接注册

CVR/VAT 不一致、状态不在 in_verification,都会导致注册链条有问题。

坑 4:Webhook 配完一次就不管了

真实系统里,回调端点和 token 需要持续维护。

坑 5:先 ack 再导入

这是消息系统里最危险的顺序之一,极易造成不可追的漏单。

坑 6:把网络接入层和文档解析层混写

接入、解密、状态管理和 XML 业务解析,本来就该分层。

结语

l10n_dk_nemhandel 最值得学习的地方,不是它支持了丹麦格式,而是它把参与方发现、公司注册、Webhook 保活、消息确认、账单导入拆成了一条有边界的接入流水线。

这也是很多本地化电子单据项目最终成败的关键:

  • 不是 XML 能不能生成
  • 而是整条网络与落账链路能不能长期、稳定、可追溯地运转

Nemhandel 在 Odoo 里的价值,正是在这里。

DISCUSSION

评论区

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