发票发送

Odoo 发票发送为什么不是“点邮件模板发出去”:sending settings、PDF/EDI、动态附件与 partner 兜底链讲透

很多人排查 Odoo 发票发送失败时,只盯着邮件模板,但 `/home/ubuntu/odoo-temp/addons/account/models/account_move_send.py` 表明真正的发送链更长:先确定 sending settings,再决定 PDF/EDI、附件组件、收件人 partner、动态报表和最终 mail params,失败点往往根本不在模板本身。

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

很多团队一遇到“发票发不出去”,第一反应就是:

  • 邮件模板坏了;
  • 发件人邮箱不对;
  • 模板里附件没带上;
  • 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_methods
  • extra_edis
  • pdf_report
  • author_user_id
  • author_partner_id
  • invoice_edi_format
  • mail_template
  • mail_lang
  • mail_body
  • mail_subject
  • mail_partner_ids
  • mail_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_cc
  • partner_to
  • move 的默认收件人推导
  • 从邮箱字符串反查 partner

最终得出一组 mail_partner_ids

这一步非常重要,因为它表达的是:

Odoo 发送邮件时,优先想拿到的是一组“收件 partner”,而不只是一个裸邮箱字符串。

这是因为后续很多逻辑都依赖 partner,而不只是 email:

  • 通知对象是谁;
  • 联系人是否存在;
  • 多联系人是否需要一起发送;
  • 是否允许没有邮箱的 partner 被保留。

所以常见抱怨“partner 明明有 email,怎么发送还是报错”,往往不该只查 email 字段,而要查这整条 recipient 解析链。


四、附件组件不是文件列表,而是 widget 数据结构

_get_default_mail_attachments_widget() 会拼出四类附件来源:

  1. placeholder 的 legal PDF
  2. 模板动态报表 placeholder
  3. 发票自身已有附件
  4. 邮件模板静态附件

这里最值得注意的是 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() 说明发送前还有一整条文档准备链

这个方法把发送前文档准备拆成了几个阶段:

  1. before hook
  2. web service before PDF render
  3. PDF 分批生成
  4. after hook
  5. 必要时 fallback PDF
  6. web service after PDF render
  7. 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

评论区

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