销售文档

Odoo PDF 报价单为什么不是“拼几份附件导出”:header/footer、product document 与表单字段映射链路讲透

结合 sale_pdf_quote_builder 源码,讲清 Odoo 如何校验 PDF、抽取表单字段、把报价头尾与产品文档分层挂接,再通过路径映射把 sale.order / sale.order.line 数据灌进 PDF。

前端 销售
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

很多人第一次看 Odoo 的 PDF 报价功能,会以为它只是“多塞几份附件”而已。

sale_pdf_quote_builder 的实际思路更像是:

先把 PDF 当成一个“可填表的文档容器”,再把销售单据数据映射进去。

它不是简单拼接文件,而是在处理三件事:

  1. 哪些 PDF 可以被当成报价文档用;
  2. PDF 里的表单字段应该怎么识别和映射;
  3. 报价头尾、产品文档和正文之间怎么分层插入。

1. Odoo 不是把 PDF 当附件,而是把它当成一种业务文档类型

quotation.document 这个模型很关键。 它通过 _inherits = {'ir.attachment': 'ir_attachment_id'} 直接借用了附件存储能力,说明它本质上还是文件,但业务语义已经变了。

在它身上,Odoo 额外加了:

  • document_type:header / footer
  • quotation_template_ids:关联报价模板
  • form_field_ids:当前 PDF 里抽出来的表单字段
  • add_by_default:是否默认挂到新报价

这告诉我们:

报价头尾不是“静态附件”,而是可以被模板和字段系统驱动的业务部件。

product.document 也被扩展了一个 attached_on_sale 选项:inside

它的含义不是“附在销售单上”,而是“嵌入报价 PDF 的中间部分”。

所以整个模块真正做的是一套“报价 PDF 组装器”。


2. 上传 PDF 之前,Odoo 先检查它能不能被当成表单

上传入口在 controller: addons/sale_pdf_quote_builder/controllers/quotation_document.py

它接收用户上传的文件,直接创建 quotation.document 记录,并在失败时回滚。

但真正的第一道校验在模型层:

  • 只能是 PDF
  • 不能是加密 PDF

utils._ensure_document_not_encrypted() 会尝试用 PDF reader 解析,遇到加密或不支持格式就直接报错。

这一步很关键,因为如果 PDF 本身都不能解析,后面的字段抽取就没有意义。

换句话说,Odoo 在这里的判断不是“文件能不能上传”,而是:

这个文件能不能进入后面的“表单字段提取管线”。


3. 表单字段不是手工建的,而是从 PDF 里抽出来的

sale.pdf.form.field 是这个模块的桥梁模型。

它的职责不是保存附件,而是保存“PDF 字段名 → Odoo 路径”的映射关系。

它有两个很实用的约束:

  • 字段名必须唯一,并且只在同一 document_type 下比较
  • path 必须合法,且路径中每一跳都要真实存在

比如它允许这样的路径:

  • partner_id.name
  • order_id.amount_total
  • user_id.login

但不允许你随便写一个不存在的字段。

更严格的是:

  • 路径只能在关系字段上层层走
  • 最后一跳才是具体值
  • header/footer 里的字段名不能以 sol_id_ 开头

这说明 Odoo 不希望这套系统变成“谁都能随便写个模板变量”的弱约束引擎。 它要的是可验证、可回填、可维护的映射。


4. 默认映射不是空的,Odoo 先帮你铺一层常用字段

_add_basic_mapped_form_fields() 很值得看。

它预置了两套映射:

  • quotation_document
  • product_document

例如报价头尾里可以直接映射:

  • amount_total
  • amount_untaxed
  • client_order_ref
  • delivery_date
  • order_date
  • partner_id__name
  • user_id__email

产品文档里则更多从 sale.order.lineproduct_id 取值:

  • description
  • price_unit
  • quantity
  • tax_incl_price
  • uom
  • product_sale_price

这一步的意义是:

Odoo 不是让你从零写模板字段,而是先给你一组“常见销售语义”的默认桥接。

这样用户在做 PDF 设计时,更多是改少量路径,而不是自己想象一整套字段体系。


5. 表单字段如何从 PDF 里生成出来

QuotationDocument._compute_form_field_ids()ProductDocument._compute_form_field_ids() 的套路一致:

  • 先清空旧的 field 关联
  • 再过滤出有 datas 的 PDF
  • 然后调用 sale.pdf.form.field._create_or_update_form_fields_on_pdf_records()

在这个函数里,Odoo 会:

  1. utils._get_form_fields_from_pdf() 解析 PDF 文本字段;
  2. 如果数据库里没有同名字段,就创建新的 sale.pdf.form.field
  3. 如果已经存在同名字段,就直接 link 过去。

这意味着“字段抽取”其实是一个去重 + 复用过程。

Odoo 不会因为每份 PDF 都长得像就重复建一堆字段记录。 它会尽量把同一字段名复用成统一映射点。


6. 为什么这里要区分“quote 里显示”与“order confirmation 后发送”

product.document.attached_on_sale 有几个语义:

  • on quote
  • on order confirmation
  • inside quote

这不是简单的可见性开关,而是单据生命周期中的插入时机

比如:

  • on quote:客户在报价阶段就能看到
  • on order confirmation:确认订单后才发给客户
  • inside quote:直接嵌入 PDF 中间正文

这三种语义对应的是三种不同的客户沟通场景。

所以它不是“附件展示策略”,而是“销售文档发布策略”。


7. 实战里最容易踩的坑

最常见的坑有三个:

  1. PDF 被加密 - 系统会直接拒绝

  2. 路径写错 - 不是所有字段名都能直接当 path - 关系链写错会在校验期就失败

  3. 把 URL 当 PDF 用 - inside 模式要求是 binary file,不是外链

如果你在定制这套功能,建议先把字段设计成稳定的业务名,再把路径映射到 sale.order / sale.order.line。 这样后续改 PDF 版式,不会把业务字段也一起改崩。


结论

sale_pdf_quote_builder 不是“把附件拼进报价单”,而是把 PDF 变成一个可映射、可校验、可复用的销售文档容器。

它最有价值的地方在于:

用字段系统把静态 PDF 和动态销售数据连接起来,同时保留了头尾、正文和产品文档的语义分层。

这才是它和普通附件功能的真正区别。

DISCUSSION

评论区

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