先说结论
Odoo 的报价模板不是“复制一张旧报价”。
在 sale_management 里,sale.order.template 真正承担的是:
- 给新报价提供一套受控的默认结构
- 把条款、有效期、签署要求、在线付款要求带进
sale.order - 把模板行转换成正式的
sale.order.line - 允许一部分行标记为
is_optional,作为前台可加购选项
但它不会做这些事:
- 不会把任意旧订单的状态、发票、消息历史一并继承
- 不会对电商订单强行套默认模板
- 不会把“模板里写了条款”理解成以后销售单改了客户、公司、语言还永远自动联动
所以理解模板的关键,不是“能复制什么”,而是:
哪些字段在创建报价时被灌入订单,哪些只在模板层存在,哪些又只在特定入口才会触发。
先看源码入口:模板不是在确认时生效,而是在报价编辑阶段生效
关键文件是:
sale_management/models/sale_order.pysale_management/models/sale_order_template.pysale_management/models/sale_order_template_line.py
在 sale.order 上,sale_order_template_id 是一个可存储、可编辑、可预计算的 Many2one。更重要的是几个 compute / onchange:
_compute_sale_order_template_id()_compute_note()_compute_require_signature()_compute_require_payment()_compute_prepayment_percent()_compute_validity_date()_compute_journal_id()_onchange_sale_order_template_id()
这已经说明模板的定位:
它是报价编辑时的结构注入器,而不是确认后的装饰器。
也就是说,很多继承边界在“onchange 阶段”就决定了,而不是等到 action_confirm() 才处理。
条款为什么不是“公司默认条款 + 模板条款自动拼接”
sale.order.template 上的 note 字段就是模板层的 Terms and conditions。
而 sale.order 的 _compute_note() 做的事情很直接:
- 先跑
super(),让销售单拿到原本的 note 逻辑 - 如果订单有模板,就把模板用
partner_id.lang语言上下文化 - 只要模板 note 不是空 HTML,就直接把
order.note设成template.note
这意味着几个非常容易被误解的点:
1. 模板条款是覆盖式,不是自动拼接式
源码没有做“公司条款 + 模板条款”的串联。
所以如果你想表达:
- 公司统一法律条款
- 再加某个行业报价模板的补充说明
那不是天然自动合并的能力。默认语义更接近:
- 有模板条款,就优先用模板条款
- 模板条款为空,才退回原本订单 note 的来源
2. 条款会跟着客户语言重新取模板翻译
这里的 with_context(lang=order.partner_id.lang) 很关键。
这说明 Odoo 不是把模板 note 当原始字符串复制,而是把它当可翻译模板内容取当前语言版本。
所以模板条款的“继承”其实是:
- 继承模板记录
- 按当前客户语言取翻译
- 再写到订单 note
3. 这不是永久联动
一旦销售单已经有自己的 note,后续你再改模板,不代表历史订单会自动一起更新。
模板继承是生成时/变更时的赋值语义,不是模板与订单之间的实时绑定。
可选产品为什么也不是“写几行建议文案”
SaleOrderTemplateLine 上有一个明确字段:is_optional。
在 _prepare_order_line_values() 里,模板行会被转换成订单行,并把这些字段带过去:
display_typeproduct_idproduct_uom_qtyproduct_uom_idis_optionalsequencename
这说明 Optional Products 在模板链路里不是注释,而是正式结构字段。
进一步看 _onchange_sale_order_template_id():
- 先
fields.Command.clear()清空现有订单行 - 再把模板中的所有 template lines 逐条
Command.create(...)成订单行
也就是说,模板里的可选产品不是“前台临时推荐卡片”,而是会进入销售订单明细结构,只是这些行带着 is_optional 语义。
这和很多人脑中的理解不同。很多人以为:
- 主商品才是 order line
- Optional 只是门户页上显示一下
但在 Odoo 里,Optional 首先是销售订单结构的一部分,然后才可能在门户/报价前台表现为“客户可加购”。
最容易踩坑的边界:换模板会清空并重建订单行
_onchange_sale_order_template_id() 最值得警惕的一句不是复制,而是:
order_lines_data = [fields.Command.clear()]
这意味着切换模板时,默认语义是:
把当前订单行清空,再按模板重建。
所以模板不是“向现有订单追加一些推荐项”,而是“重置当前报价结构”。
这也是为什么 Odoo 又写了一层 _onchange_partner_id() 的保护逻辑:
- 仅在未保存订单且已有模板时考虑重载
- 只有当当前订单行和模板行在
product_id / product_uom_id / product_uom_qty / display_type上等价时,才重新套模板
这段逻辑很有意思,因为它实际上在回答一个业务问题:
- 如果销售只是先选了客户,再选模板,系统可以安全重载
- 如果销售已经手改过行内容,再因为切客户而自动重载,就会把人工编辑冲掉
所以 Odoo 采用的是:
只有在“当前行看起来仍像模板原样”时,才允许 partner 变化触发模板重载。
这就是典型的“继承边界保护”。
为什么公司默认模板不会覆盖所有订单来源
_compute_sale_order_template_id() 里有一条非常重要的特判:
- 如果订单有
website_id,就 不要 自动应用公司默认 quotation template
源码注释写得很直白:
don't apply quotation template for order created via eCommerce
这背后反映的是场景边界:
后台人工报价
适合套模板,因为:
- 需要销售流程标准化
- 需要固定条款/签署/预付款规则
- 需要可选产品结构
电商订单
不适合强塞后台报价模板,因为:
- 商品、价格、加购逻辑来自网站购物流程
- 条款和页面文案由电商前台控制
- 电商订单不应该突然被后台默认模板改写行结构
所以“公司默认模板”不是全局统治规则,而是后台销售录单场景的默认值。
签署、付款、有效期、journal 也都是模板继承,但语义不同
模板会把这些内容带入订单:
require_signaturerequire_paymentprepayment_percentnumber_of_days -> validity_datejournal_id
但这些字段和 order line / note 的继承语义并不完全一样。
条款 / 订单行:更像内容复制
它们会在模板变更时直接写进订单。
签署 / 付款 / 预付款比例 / 有效期 / journal:更像配置默认值
这些字段由 compute 从模板得出,强调的是:
- 这个报价按什么接受规则成交
- 这个报价默认有效多久
- 后续开票默认走哪个销售日记账
所以模板其实同时承载两层东西:
- 内容层:条款、行、可选产品
- 政策层:签署、付款、预付款比例、有效期、日记账
很多实施把模板只当内容复用工具,就会错失第二层价值。
多公司和模板产品为什么也有硬边界
sale.order.template 上有 _check_company_id() 约束。
它检查模板里的产品公司归属,防止出现这种情况:
- 模板被多个公司共享
- 但模板行里塞了某个特定公司的产品
- 结果别的公司根本无权访问或不该销售这个产品
所以模板不是“随便配,反正最后录单时再说”。
Odoo 在模板层就要求:
- 共享模板不能乱带公司专属产品
- 限定公司模板也不能包含对该公司不可访问的产品
这也再次说明模板是正式业务对象,不是轻量草稿。
实战里最该记住的 4 条
1. 模板条款默认是覆盖,不是拼接
如果你要“统一法务条款 + 模板补充条款”,通常需要二开或明确人工编辑流程。
2. Optional Product 是结构字段,不是备注
它会随着模板行一起进入 sale.order.line,不是纯前台展示文案。
3. 切模板本质上会重建订单行
不要把模板切换操作暴露给已经大量人工编辑的报价而不加培训。
4. 网站单和后台单的模板边界是故意分开的
电商单不自动吃公司默认报价模板,这不是缺功能,而是避免链路互相污染。
一句话总结
Odoo 的 Quotation Template 真正继承的,从来不只是几条商品行。
它把:
- 条款
- 可选产品
- 签署/付款策略
- 有效期
- 默认开票 journal
一起注入到销售订单里;但它又明确限制:
- 电商订单不自动套模板
- 手工改过的未保存订单不应被 partner onchange 轻易重载
- 多公司产品边界必须在模板层就守住
所以这套设计的重点不是“复制更快”,而是:
让报价模板成为可控、可翻译、可约束的销售结构,而不是一张旧单的影子副本。
DISCUSSION
评论区