先说结论
埃及 ETA 模块最容易被低估的地方,是很多人以为“电子发票 = 生成一份 JSON 发出去”。
但从 /home/ubuntu/odoo-temp/addons/l10n_eg_edi_eta/models/account_edi_format.py、account_move.py、res_company.py、product_template.py 看,Odoo 实际上把这条链路拆成了几个层次:
- 先在本地按 ETA 结构组装 JSON。
- 再借助个人 thumb drive 证书完成签名。
- 提交时使用公司级 client credentials 去 ETA 拿 access token。
- 回写 UUID、longId、submissionId,形成远端文档身份。
- 后续取消、查状态、下载政府 PDF,都围绕这组身份继续。
所以它不是一个“导出器”,而是一条完整的合规链路。
为什么签名和提交被故意拆成两段
action_post_sign_invoices() 的逻辑非常值得看。
它不会在发票过账时直接联网提交,而是先做几件事:
- 只处理已 posted、尚未提交 ETA 的埃及发票;
- 强制一次只能处理一个 company;
- 检查当前用户在该公司下是否配置了个人 thumb drive;
- 检查 drive 上是否已经有证书;
- 先把 ETA JSON 以附件形式写入
l10n_eg_eta_json_doc_file; - 然后再调用 drive 的签名动作。
这条链路透露出一个很强的设计判断:
“生成监管文档”和“拿个人证书签名”不是一回事。
前者是公司级业务数据拼装; 后者是个人持证动作,而且 middleware 一次只接受一个 drive。
这也解释了为什么模块会限制“只能一次签同一家公司的票”。 不是 Odoo 任性,而是外部签名设备本来就有物理与会话边界。
JSON 不是临时字符串,而是有附件生命周期的正式文档
ETA JSON 会被保存到二进制附件字段 l10n_eg_eta_json_doc_file。
之后很多关键字段都从这里反解:
l10n_eg_uuidl10n_eg_long_idl10n_eg_submission_number
也就是说,在 Odoo 里,ETA JSON 不是“发完就扔”的中间产物,而是整条监管链路的核心载体。
这个设计非常合理,因为后面你还要靠同一个附件去:
- 查提交结果;
- 生成二维码链接;
- 下载官方 PDF;
- 甚至判断取消状态。
client_credentials 说明这是公司级 API 身份,不是用户 OAuth
_l10n_eg_eta_get_access_token() 采用的是:
l10n_eg_client_identifierl10n_eg_client_secretgrant_type = client_credentials
这和很多常见 SaaS 集成很不一样。
它不是员工本人点一次授权,而是公司作为 API client 向 ETA 换取 access token。
这意味着实施里要分清两个身份层:
- 公司级身份:调用 ETA API;
- 个人证书身份:对具体发票 JSON 做签名。
如果把这两件事混在一起理解,就很容易把问题归错层。
为什么 preproduction / production 域名要拆三种
ETA_DOMAINS 不只是 prod / test 两个域名,而是拆成:
- API domain
- token domain
- invoice / QR domain
并且 preproduction 与 production 各有对应地址。
这说明 ETA 集成不是“改一个 base URL 就切环境”,而是:
- 取 token 的入口可能不同;
- 提交文档的 API 入口可能不同;
- 最终二维码与分享页面域名也不同。
所以实战里一旦有人说“测试环境明明通了,怎么二维码链接不对”,你就知道不该只检查一个 endpoint。
阈值、税号、商品编码这些“烦人的字段”,其实都是 ETA 结构的一部分
模块里有几条很关键的本地校验:
_l10n_eg_validate_info_address()会检查国家、省州、城市、街道、楼号;- 当发票金额超过
l10n_eg_invoicing_threshold,或者对方不是个人税类时,还会要求 VAT; - 产品上有
l10n_eg_eta_code,可对应 EGS / GS1 代码; - 最佳实践甚至建议把它直接当 barcode 使用。
这意味着 ETA 集成不是“财务最后导出一下”。 它会反向要求:
- 主数据必须干净;
- 地址结构不能偷懒;
- 商品编码体系不能只停留在内部命名。
很多电子发票项目失败,不是 API 失败,而是主数据一上监管接口就露馅。
提交成功后,真正重要的是那组三元身份
_l10n_eg_edi_post_invoice_web_service() 成功后,会把 response 回写成:
l10n_eg_uuidl10n_eg_long_idl10n_eg_internal_idl10n_eg_hash_keyl10n_eg_submission_number
其中最重要的是 UUID、longId、submissionId。
原因很简单:
- UUID 用来指向政府侧文档;
- submissionId 用来查这一批提交;
- longId 决定最终分享链接与二维码。
后面的 _compute_eta_qr_code_str() 也是根据:
- 环境域名
- UUID
- longId
拼出最终二维码地址。
所以 ETA 集成不是“收到 200 就结束”,而是从成功响应里拿到后续一切动作的主索引。
取消、查状态、取 PDF 都说明远端文档是长期对象
这个模块没有把提交当成单向动作。 它后续还支持:
_l10n_eg_get_einvoice_document_summary()_l10n_eg_get_einvoice_status()_cancel_invoice_edi_eta()_l10n_eg_get_eta_invoice_pdf()
而且取消前还会先查状态:
- 如果已经是
Cancelled或Rejected,就直接返回 success; - 否则再调状态修改接口。
这种写法说明 Odoo 默认认为:
ETA 文档一旦发出去,就是一个可查询、可取消、可取回官方呈现件的长期远端对象。
这和“把数据推给第三方一次”是完全不同的系统心智。
实施里最容易踩的 5 个误区
1. 以为公司配置好 client id/secret 就够了
不够。还要处理用户自己的 thumb drive 与证书。
2. 以为 JSON 是临时产物
不对。它是后续 UUID、submissionId、PDF、QR 的共同载体。
3. 以为环境切换只改一个 URL
不对。token、API、二维码域名都可能不同。
4. 以为监管失败都是接口问题
很多时候是主数据、税号阈值、商品编码先有问题。
5. 以为提交成功就结束
不对。后面还有状态、取消和官方 PDF 拉取链路。
最后一句
埃及 ETA 模块真正体现的,不是“如何生成 JSON”,而是如何把本地业务数据、个人证书签名、公司级 API 身份与远端文档生命周期拼成一条可追踪的合规流水线。
理解这一点,很多看起来零碎的字段和限制,都会突然变得很合理。
DISCUSSION
评论区