先说结论
在 Odoo 里,account.move.line.display_type 绝不是一个“前端排版字段”。
它真正承担的是:
- 区分这是不是业务产品行
- 区分这是不是税行
- 区分这是不是应收应付付款条款行
- 区分这是不是纯展示用 section / subsection / note
- 让同一个
account.move.line模型,同时承载“用户看到的发票行”和“系统真正要过账的会计行”
所以如果你把它理解成“只是前端显示小节”,后面就很容易在这些地方踩坑:
- 自定义发票行时误给 section 行写金额
- 过滤
line_ids时把税行、付款条款行也一起算进业务统计 - 以为
invoice_line_ids就是全部行,结果漏掉税行和付款条款行
为什么 Odoo 要把这么多行塞进同一个模型
account.move.line 在源码里有一组很醒目的 display_type 枚举:
producttaxpayment_termdiscountroundingepdline_sectionline_subsectionline_note- 以及若干不可抵扣税相关类型
这说明 Odoo 的设计思路不是:
发票展示一套模型,会计过账再来一套模型。
而是:
一张单据上的所有关键行都统一落在
account.move.line,再用display_type区分语义。
这样做的好处是:
- 同一个排序序列里既能放 section,也能放产品,也能放税和付款条款
- 前端、报表、税计算、过账、对账都围绕同一批记录协作
- 系统不必维护“展示行”和“会计行”的双份同步
代价则是:
你做开发时必须先分清“这是什么类型的行”,再决定能不能算金额、能不能改科目、能不能参与统计。
_compute_display_type():默认不是空白,而是自动推断
在 addons/account/models/account_move_line.py 里,_compute_display_type() 的逻辑很关键。
当一行还没显式写 display_type 时,Odoo 会根据已有上下文推断:
- 如果
tax_line_id已经存在,这行会被归成tax - 如果科目已经确定,且科目类型属于
asset_receivable/liability_payable,这行会被归成payment_term - 其他发票普通行默认归成
product
这意味着:
display_type不是纯手工维护字段- 它是下游会计语义的归类结果
- 你如果用自定义代码提前塞错科目或税行关系,Odoo 后面可能把整行分到完全不同的类型里
所以很多“为什么这行突然不在 invoice_line_ids 里了”的问题,本质不是 domain 坏了,而是这行已经被系统识别成另一种 display type。
section / note 行为什么必须“零金额、无科目”
源码里一组 SQL 约束特别能说明问题。
对于 line_section / line_subsection / line_note,系统强制要求:
amount_currency = 0debit = 0credit = 0account_id IS NULL
这其实已经给出官方态度:
section/note 是排版与结构行,不是会计事实。
所以如果你在导入脚本、Studio、自定义模块里给 section 行硬塞科目或金额,问题不是“看起来怪一点”,而是会直接违反模型边界。
这也是为什么很多开发者第一次碰到报错会疑惑:
- 明明只是想在发票里插个小节
- 为什么数据库约束会拦我
答案是:因为 Odoo 不允许“展示行”和“记账行”混种。
invoice_line_ids 为什么看起来像“少了几行”
在 account.move 里,invoice_line_ids 的 domain 只保留:
productline_sectionline_subsectionline_note
也就是说,税行和付款条款行默认不在 invoice_line_ids 里。
它们仍然在 line_ids 里,只是被排除出“用户主视图上的发票业务行”。
这就是很多统计错乱的根源:
- 你如果拿
line_ids直接做“发票商品行统计”,会把税行和付款条款行一并算进去 - 你如果拿
invoice_line_ids做“整张会计分录完整还原”,又会漏掉真正承担应收应付与税务结算的行
更准确的理解应该是:
invoice_line_ids:给业务用户看的“发票主内容”line_ids:整张单据全部会计行
payment term 行不是“额外备注”,而是真正的应收应付落点
很多人第一次看到 payment_term 会误以为它只是把分期信息显示出来。
其实不是。
在 account_move_line.py 里,付款条款行会沿着应收/应付科目去匹配账户,并在后续成为:
- 客户发票上的应收款行
- 供应商账单上的应付款行
- 到期日、催收、对账、付款状态的关键载体
换句话说:
产品行负责描述卖了什么,payment_term 行负责承接“谁欠谁多少钱、什么时候到期”。
如果你只盯着产品行金额,而忽略付款条款行,你就会误解很多会计行为:
- 为什么一张发票会出现多条应收款行
- 为什么修改付款条件会重建付款条款行
- 为什么 follow-up / aging 看的是付款条款相关行,而不是产品行
tax 行为什么也要单独建模
在 account.move 的税计算逻辑里,display_type 不只是“显示标签”,而是决定这行在税引擎里扮演什么角色:
product行常作为税基 base linetax行是系统生成出来的税明细结果rounding/epd等行又有自己的会计语义
这说明:
Odoo 不是“产品行上挂一个税额字段就结束”,而是把税影响显式展开成真正的 move line。
这对开发的意义很大:
- 你调税时,别只看产品行
- 你做导出或对账时,要区分 base line 和 tax line
- 你重写发票行同步逻辑时,不能把税行当普通用户编辑行处理
最常见的三个误区
误区 1:把 display_type 当纯 UI 字段
结果是 section 行被写进金额,或者付款条款行被误删。
误区 2:所有统计都直接遍历 line_ids
这样经常把税、付款条款、rounding 一起算进“商品金额”。
误区 3:只看 invoice_line_ids,却试图解释完整会计结果
这会漏掉税行和应收应付行,最后对不上总账。
开发时更稳的判断顺序
如果你要扩展发票/账单逻辑,建议按这个顺序判断:
- 先确认当前遍历的是
line_ids还是invoice_line_ids - 再确认目标行的
display_type - 再决定它是否应该参与: - 金额汇总 - 税计算 - 科目写入 - 报表展示
- 最后再做自定义字段或自动化逻辑
这个顺序看起来啰嗦,但它能避免 80% 的“为什么一切都对不上”的问题。
最后的判断句
在 Odoo 里,account.move.line.display_type 的本质不是“前端展示字段”,而是:
用一套统一的会计行模型,同时承载业务行、税行、付款条款行与结构行的分层语义开关。
理解了这一点,你再看发票页面里那张“像表格一样”的行列表,就会明白:
- 用户看到的是一张表
- Odoo 底层维护的是一组不同会计职责的行
- 而
display_type正是这组职责分层的核心开关
DISCUSSION
评论区