很多团队一遇到“发票发不出去”,第一反应就是:
- 邮件模板坏了;
- 发件人邮箱不对;
- 模板里附件没带上;
- partner 没邮箱。
这些当然都可能是原因。
但如果你真的去看 /home/ubuntu/odoo-temp/addons/account/models/account_move_send.py,会发现 Odoo 发票发送根本不是“套个模板发封信”那么简单。
里面至少牵涉这些关键方法:
_get_default_sending_settings()_get_default_mail_partner_ids()_get_default_mail_attachments_widget()_display_attachments_widget()_generate_dynamic_reports()_generate_invoice_documents()_get_mail_params()_send_mails()/_send_mail()
把它们连起来看,Odoo 的真实发送链是:
先决定“这张发票该以什么方式发送”,再决定要生成什么 PDF/EDI/附件,再决定发给哪些 partner,最后才是邮件模板把这些东西组装成真正的 outgoing mail。
所以很多发票发送问题,压根不是模板单点故障,而是前面的 sending pipeline 某个环节已经偏了。
一、真正的起点不是模板,而是 sending settings
_get_default_sending_settings() 是最值得先看的入口。
它会组织出一整包发送配置,包括:
sending_methodsextra_edispdf_reportauthor_user_idauthor_partner_idinvoice_edi_formatmail_templatemail_langmail_bodymail_subjectmail_partner_idsmail_attachments_widget
这说明发票发送不是“模板 + 对象”这么简单,而是一个带多种发送方式的配置对象。
尤其关键的是 sending_methods。
默认值来自伙伴或公司上下文里的 invoice_sending_method,而不是邮件模板本身。
也就是说:
先决定渠道,再决定模板;不是反过来。
如果你一上来只查模板,很可能已经查晚了一步。
二、为什么发票发送同时谈 PDF 和 EDI
很多人把“发票发送”理解成发邮件,但 account_move_send 的设计明显不是这样。
它同时考虑:
- 传统 email
- 发票 PDF
- EDI 格式
- extra EDI
- 动态附件
- 邮件模板附件
这背后是一个很现实的事实:
在会计世界里,“发送一张发票”有时意味着:
- 发一封邮件;
- 带上正式 legal PDF;
- 还要附上电子发票格式文件;
- 或者附加其他上下游系统需要的输出。
所以 Odoo 才把发送逻辑建模成文档管线而不是“发信动作”。
三、收件人不是简单读 partner.email
_get_default_mail_partner_ids() 很能说明 Odoo 的克制。
它不会只读一格邮箱然后结束,而是会根据:
- 模板是否
use_default_to - 模板里的
email_to email_ccpartner_to- move 的默认收件人推导
- 从邮箱字符串反查 partner
最终得出一组 mail_partner_ids。
这一步非常重要,因为它表达的是:
Odoo 发送邮件时,优先想拿到的是一组“收件 partner”,而不只是一个裸邮箱字符串。
这是因为后续很多逻辑都依赖 partner,而不只是 email:
- 通知对象是谁;
- 联系人是否存在;
- 多联系人是否需要一起发送;
- 是否允许没有邮箱的 partner 被保留。
所以常见抱怨“partner 明明有 email,怎么发送还是报错”,往往不该只查 email 字段,而要查这整条 recipient 解析链。
四、附件组件不是文件列表,而是 widget 数据结构
_get_default_mail_attachments_widget() 会拼出四类附件来源:
- placeholder 的 legal PDF
- 模板动态报表 placeholder
- 发票自身已有附件
- 邮件模板静态附件
这里最值得注意的是 placeholder。
Odoo 在用户真正发送前,很多附件其实还没物理生成,只是先在 UI 上构造一个“将要存在的附件占位描述”。
这就是为什么你有时会看到:
- 附件已经在发送窗口里;
- 但数据库里还没有对应的新 PDF attachment;
- 真正生成发生在后续发送阶段。
这不是 bug,而是故意把:
- 展示
- 配置
- 真正生成
拆成了不同阶段。
五、为什么有时 PDF 会复用旧附件,有时会重新生成
如果 move.invoice_pdf_report_id 已存在,_get_placeholder_mail_attachments_data() 甚至可能直接返回空,因为系统知道这张发票已经有正式 PDF 了。
而 _generate_invoice_documents() 里也明确写着:
- 若没有错误且
invoice.invoice_pdf_report_id不存在,才把它加入pdf_to_generate - 已有 PDF 时不会无脑重生
这意味着 Odoo 的策略不是“每发一次就重新渲染一次 PDF”,而是:
优先复用已存在的正式发票文档,只在需要时才生成新的 legal PDF。
这能避免:
- 重复渲染;
- 无意义的附件膨胀;
- 多次发送导致多个近似但并非同一法律文档版本的 PDF 漂在系统里。
六、动态报表是最容易被低估的一环
_generate_dynamic_reports() 会扫描 mail_attachments_widget 中带 dynamic_report 且未被跳过的项目,然后现场渲染报表、创建 attachment,再回填进 widget。
这一步特别容易被忽略,因为很多人把“模板附件”都当静态文件理解。
但这里其实有一类附件不是预先存在的,而是:
- 按当前 move
- 按当前 company
- 按当前发送上下文
动态生成。
这解释了很多“为什么同一模板在不同发票上附件不一样”的现象。
不是模板不稳定,而是它本来就允许动态产出。
七、_generate_invoice_documents() 说明发送前还有一整条文档准备链
这个方法把发送前文档准备拆成了几个阶段:
- before hook
- web service before PDF render
- PDF 分批生成
- after hook
- 必要时 fallback PDF
- web service after PDF render
- link attachments 到发票
最重要的不是阶段多,而是阶段顺序。
它说明 Odoo 在发送发票时并不只是“先生成个 PDF 再发邮件”,而是:
把外部服务、法律文档生成、容错回退和附件回链都放在一个发送前编排里。
尤其 allow_fallback_pdf 这一点很说明问题。
某些情况下,系统即使遇到非致命错误,也允许继续生成 fallback 文档,而不是整条发送链完全中断。
这是一种偏业务连续性的设计。
八、真正进入邮件层前,还要再做一次附件筛选
_get_mail_params() 会把:
- 发票已有额外附件
- widget 中保留下来的附件
汇总起来,并排除:
- 被
skip标记的附件 - 不该进入最终邮件的占位对象
最终才得到真正塞进 message_post() 的 (attachment.name, attachment.raw) 列表。
这也说明:
- 发送窗口看到的附件集合;
- 最终邮件真正带出去的附件集合;
并不一定天然一模一样。
中间还要经过一次“按最终发送规则过滤”。
九、真正发信时,Odoo 发的是消息,不是裸 SMTP 调用
_send_mail() 最终走的是 move.message_post(...)。
这件事很重要,因为它意味着发票发送并不是孤立的邮件子系统,而是和 Odoo 的消息系统、跟踪记录、附件归属一起工作的。
后面它还会把新消息上的附件重新挂到 message 记录上,避免和发票本身的附件归属混在一起。
这就是为什么“发票文档附件”和“邮件消息附件”虽然看起来相似,实际上是两层不同归属。
十、排查“发票发不出去”,最稳的顺序是什么
如果你的症状是:
- 看起来模板没问题,但还是不能发;
- 附件有时有、有时没有;
- partner 明明有邮箱却报错;
- PDF 不生成或老是复用旧文档;
建议优先按这个顺序排查:
1)先看 sending_methods
是不是这张发票本来就不只是 email。
2)看 mail_partner_ids 是如何推出来的
不要只看 partner.email。
3)看 mail_attachments_widget 里哪些是 placeholder,哪些是真附件
不要把占位项误当已生成文件。
4)看 invoice_pdf_report_id 是否已存在
已有 legal PDF 时,系统可能不会重生。
5)看是否有动态报表或 extra EDI 在发送前生成失败
失败点经常在这里,不在模板。
6)最后再看模板正文、标题和发件人
模板当然重要,但通常不是第一层原因。
结论
Odoo 发票发送真正建模的不是“发一封带附件的邮件”,而是一条完整文档发送管线:
- 先确定 sending settings;
- 再决定 PDF / EDI / extra EDI;
- 再解析收件人 partner;
- 再构造附件 widget 和动态报表;
- 再生成或复用 legal PDF;
- 最后才进入邮件消息发送层。
所以“发票发送失败”很多时候不是邮件模板坏了,而是更前面的发送配置、附件编排、收件人解析或文档生成阶段已经出了问题。
把这条 pipeline 看清楚,排错效率会高很多。
DISCUSSION
评论区