先说结论
sale.order.line.display_type 不是一个“为了让报价单更好看”的小字段。
在 Odoo 销售源码里,它真正划的是一条很重要的线:
- 哪些行是可计价、可交付、可开票的业务行
- 哪些行只是结构展示
- 哪些行虽然不是普通商品行,但又要参与“开票状态”这类业务判断,比如 down payment
所以如果你把它简单看成 section / note 的 UI 标记,很快就会在这些地方掉坑:
qty_to_invoice算不对invoice_status看起来反常- 自定义脚本把 note 行也当商品行处理
- 预付款行明明没交付,却已经可以开票
源码先把边界写死在约束里
在 addons/sale/models/sale_order_line.py 顶部,Odoo 先用两组约束把边界钉住了。
对普通可记账业务行
如果 display_type IS NULL,又不是 is_downpayment,那它必须具备:
product_idproduct_uom_id- 等一组“像正常销售行”的必需信息
对非业务结构行
如果 display_type 不为空,也就是 section / subsection / note 行,那么约束要求:
product_id IS NULLprice_unit = 0product_uom_qty = 0product_uom_id IS NULLcustomer_lead = 0
这已经说明官方立场非常明确:
display_type 行不是“精简版商品行”,而是完全不同语义的记录。
为什么报价单明明是一张表,底层却不是“同类行”
销售单页面给人的感觉是:
- section
- note
- 商品行
- 预付款相关行
都混在一张列表里。
但 Odoo 在底层并没有把它们看成“只有显示方式不同”的同类对象。
更准确地说:
- section / note 行:负责结构与叙述
- 普通商品行:负责数量、价格、交付、开票
- down payment 行:负责预收款类开票语义,但不走普通交付逻辑
这也是为什么同一个模型会同时有:
display_typeis_downpaymentqty_deliveredqty_to_invoiceuntaxed_amount_to_invoiceinvoice_status
这些字段并不是随便堆在一起,而是在表达同一个核心问题:
这条销售行到底属于哪类业务语义。
_compute_qty_to_invoice():只要有 display_type,就直接归零
在 _compute_qty_to_invoice() 里,逻辑非常直白。
只有满足这些条件的行,系统才会继续算待开票数量:
state == 'sale'not line.display_type
一旦是 section / note / subsection 行:
qty_to_invoice = 0
这说明 display_type 的真正含义之一是:
把结构行彻底排除出“按数量开票”的主链路。
这也是很多自定义 bug 的根源:
- 前端看着都在一张报价单里
- 开发者就误以为都该参与统一计算
实际上 Odoo 从源码层就明确说了:不是。
那为什么 down payment 又是例外
预付款行最容易让人困惑。
它通常不是靠正常交付数量来决定是否能开票,但它又显然不是 section / note 那样的纯结构行。
所以 Odoo 没有用 display_type 来标记 down payment,而是单独用:
is_downpayment
这是一种很典型的 Odoo 设计:
display_type解决“结构行 vs 业务行”is_downpayment解决“这是业务行,但语义不是普通商品行”
这也解释了为什么 _compute_invoice_status() 里会有专门判断:
- 如果是 down payment,且
untaxed_amount_to_invoice == 0 - 则直接视为
invoiced
也就是说,预付款不是按“交了多少货”来走,而是按“该开的预付款金额还有没有剩余”来走。
_compute_untaxed_amount_to_invoice():预付款与普通商品行其实走的是两套认知
源码里 untaxed_amount_to_invoice 的逻辑也很能说明问题。
普通商品行主要看:
- 产品开票策略是按订单还是按交付
- 已开票金额多少
- 含税价是否要倒推未税金额
但 down payment 行在实际业务上关心的是:
- 已经为这条预付款行开了多少
- 还剩多少预付款额度未开
所以你会发现:
- 它不依赖普通商品交付语义
- 却仍然属于“能改变 invoice_status 的业务行”
这就是为什么把预付款理解成“特殊商品”常常不够准确。
更准确地说,它是:
销售模型里一类特殊的可开票业务行。
最容易出现的三个误区
误区 1:section / note 行也能参与业务统计
如果你自己做金额、数量、导出统计时没先排除 display_type,结果几乎一定会脏。
误区 2:预付款行应该像普通商品一样看 qty_delivered
不是。预付款的核心不是交付,而是预收/预开票语义。
误区 3:报价单上一张列表里的行,底层就应该用同一套逻辑处理
这正是很多错误自动化和 Studio 定制的起点。
实战开发时更稳的判断顺序
扩展销售行逻辑时,建议先问这四个问题:
- 这行有没有
display_type - 这行是不是
is_downpayment - 我要处理的是“交付逻辑”还是“开票逻辑”
- 我当前要遍历的,是全部行还是只有普通业务行
把这四个问题先问清楚,很多看似复杂的 bug 其实当场就能定位。
最后的判断句
在 Odoo 销售里,display_type 的本质不是排版,而是:
把结构行从交付、计价、开票主链路中明确剥离出去的模型边界。
而 is_downpayment 则补上另一层语义:
这不是普通商品行,但它仍然是一条需要参与开票判断的业务行。
把这两层分开看,销售行相关的很多“为什么状态不合直觉”的问题,基本就能解释清了。
DISCUSSION
评论区