很多人看到 Odoo 发票上有付款二维码,会把它理解成一个很轻的功能:
报表模板里插一张二维码图片,不就完了吗?
但从 /home/ubuntu/odoo-temp/addons/account 和 account_qr_code_emv 的源码看,官方设计其实更像一条 支付信息编码管线。
它至少分成四层:
- 发票决定自己要不要显示二维码;
- 系统从银行账户支持的方法里挑一个可用的二维码标准;
- 对应标准模块把金额、币种、商户信息、附言等组装成规范字符串;
- 最后再交给条码引擎渲染成真正可显示的 QR 图像。
所以二维码不是模板层的小花活,而是会计、银行账户配置、国家标准和报表输出一起协作的结果。
一、入口不在报表,而在 account.move 的 _generate_qr_code()
在 account_move.py 里,真正的主入口是 _generate_qr_code()。
它的大致顺序是:
- 先判断
display_qr_code; - 看发票上有没有手工指定
qr_code_method; - 如果没有,就去银行账户支持的方法列表里找第一个“可用”的;
- 然后调用
partner_bank_id.build_qr_code_base64(...)生成二维码; - 成功后再把选中的
qr_code_method回写到发票上。
这里最值得注意的是最后一步。
源码里特意等生成成功之后,才把 qr_code_method 写回去。原因也写得很直白:
如果提前写,万一生成失败,数据库状态和界面显示就可能不一致。
这说明官方把二维码生成看成一个 有可能失败的业务动作,而不是无脑渲染。
二、真正决定“能不能生成”的,不是发票,而是 res.partner.bank
account/models/res_partner_bank.py 提供了统一的 QR 能力框架。
核心方法是 _build_qr_code_vals()。
它会:
- 拿到当前银行账户;
- 枚举所有可用的二维码方法;
- 对每个方法先跑
_get_error_messages_for_qr(),看它在当前国家、当前账户、当前币种下是否“理论上适用”; - 再跑
_check_for_qr_code_errors(),看必填数据是不是齐全; - 只有都通过,才返回构造二维码需要的参数。
这一步很像面试筛选:
- 第一关看“资格是否匹配”;
- 第二关看“资料是否齐全”;
- 两关都过,才真的去生成二维码。
所以你在业务现场看到“发票上没出二维码”,不一定是报表模板问题,更可能是:
- 当前银行账户不支持该国家的 QR 方案;
- 金额、币种、商户信息或引用数据不符合要求;
- 系统找不到任何 eligible 的方法。
三、account_qr_code_emv 不是直接给你一个国家方案,而是先提供一套 EMV 骨架
account_qr_code_emv 最值得注意的地方,是它并没有把所有国家规则都写死。
相反,它先做了一层通用 EMV Merchant-Presented QR 的骨架:
_get_available_qr_methods()里新增emv_qr;_get_qr_vals()负责把各字段序列化成 EMV 字符串;_get_qr_code_generation_params()把结果交给条码引擎,指定barcode_type='QR';_get_crc16()负责最后的校验码。
但与此同时,它又把几个关键点留成了扩展钩子:
_get_merchant_account_info()_get_additional_data_field()_compute_display_qr_setting()
而这些默认实现里,很多其实返回的是空值或 False。
这说明一个非常重要的设计事实:
account_qr_code_emv更像“EMV 容器层”,真正落地到某国支付二维码,还需要继承模块补上国家规则。
所以不是装了它就一定能出二维码。
四、EMV 不是“随便拼个 JSON”,而是 TLV 结构 + CRC16
在 models/res_bank.py 里,_serialize() 把每一段内容编码成:
- 两位 header
- 两位长度
- 再加实际值
这就是典型的 TLV(Tag-Length-Value)思路。
接着 _get_qr_code_vals_list() 会把这些字段按规范排好:
- Payload Format Indicator
- 动态二维码标识
- Merchant Account Information
- Merchant Category Code
- Transaction Currency
- Transaction Amount
- Country Code
- Merchant Name
- Merchant City
- Additional Data Field
最后 _get_qr_vals() 会把这些字段串起来,再补上 6304 这个 CRC 段头,然后调用 _get_crc16() 算最终校验值。
这意味着二维码内容不是“图片资产”,而是一段严格编码后的支付指令字符串。
五、为什么币种、商户名、城市和附言都不是随便放
account_qr_code_emv/const.py 里有 CURRENCY_MAPPING,把 Odoo 货币代码映射成 EMV 规范里的数字编码。
同时,_get_qr_code_vals_list() 还做了很多清洗:
- 金额为 0 时可以置空;
- 商户名和城市会去重音并截断长度;
- comment 会过滤成允许字符集合;
- 只有
include_reference为真时,才尝试放 additional data。
这些细节很能说明问题。
二维码不是“你有个文本就塞进去”,而是:
- 要符合长度限制;
- 要符合字符集限制;
- 要符合支付标准约束;
- 还要保证扫码方能稳定解析。
六、为什么“模块装了但没法用”往往是设计使然,不是 bug
_check_for_qr_code_errors() 对 emv_qr 额外检查了几件事:
- Merchant Account Information 是否存在;
- 商户城市是否有值;
proxy_type是否存在;proxy_value是否存在。
而 _get_error_messages_for_qr() 又明确写着:
- 没有银行账户就不能生成;
- 如果当前国家没有任何可用 EMV 实现,就提示该账户所属国家没有可用 QR。
也就是说,官方并不想“尽量猜一个二维码出来”,而是宁可不给,也不要给出一张看起来有图、实际上不可支付的假二维码。
这是很典型的会计/支付模块思路:
可缺省,不可乱猜。
七、对实施顾问和开发最重要的边界:显示逻辑与国家适配是两回事
很多人会把“发票上显示二维码”理解成单一配置问题,但这里至少有两层:
1)显示层
account.move 侧判断当前发票是否应该展示 QR。
2)标准层
res.partner.bank 和 account_qr_code_emv 判断当前账户、国家、币种、配置能不能生成符合规范的 EMV 内容。
所以你在项目里排查时,不该只问:
- 为什么模板没显示?
还该问:
- 当前银行账户属于哪个国家?
- 有没有对应国家扩展实现 merchant account info?
- 城市、引用、代理字段是否满足规则?
- 发票 residual、币种、partner bank 是否完整?
八、新手最容易误解的 4 件事
1)二维码就是报表图片
不对。图片只是最后一步,前面还有方法选择、资格校验、数据组包。
2)装了 account_qr_code_emv 就所有国家都能扫
不对。这个模块本身更像 EMV 框架层,很多国家规则需要继承扩展补充。
3)发票自己决定二维码长什么样
不对。真正的编码逻辑主要在银行账户模型及其扩展上。
4)只要银行账号有值就能生成
不对。商户城市、代理信息、国家匹配、币种映射、引用格式都可能成为拦截条件。
总结
Odoo 发票二维码的实现思路,不是“模板插图”,而是:
- 发票先决定要不要展示;
- 银行账户框架再挑一个符合条件的 QR 方法;
account_qr_code_emv把支付信息编码成 EMV TLV 字符串并附上 CRC16;- 最后才由报表/条码引擎渲染成二维码。
如果只记一句,可以记这句:
在 Odoo 里,付款二维码不是 UI 功能,而是“发票 → 银行账户 → 国家标准 → 条码渲染”的编码链路。
DISCUSSION
评论区