会计行类型

Odoo 会计行里的 display_type 为什么不是“前端展示字段”:section / tax / payment term 行的会计边界讲透

很多人把 account.move.line 里的 display_type 误以为只是 UI 用来画 section 和 note 的字段。实际上,它同时决定一行是不是产品行、税行、付款条款行,甚至决定这行应不应该有科目、金额和后续会计语义。本文结合 account_move_line.py 与 account_move.py 把这条边界讲透。

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

先说结论

在 Odoo 里,account.move.line.display_type 绝不是一个“前端排版字段”。

它真正承担的是:

  • 区分这是不是业务产品行
  • 区分这是不是税行
  • 区分这是不是应收应付付款条款行
  • 区分这是不是纯展示用 section / subsection / note
  • 让同一个 account.move.line 模型,同时承载“用户看到的发票行”和“系统真正要过账的会计行”

所以如果你把它理解成“只是前端显示小节”,后面就很容易在这些地方踩坑:

  • 自定义发票行时误给 section 行写金额
  • 过滤 line_ids 时把税行、付款条款行也一起算进业务统计
  • 以为 invoice_line_ids 就是全部行,结果漏掉税行和付款条款行

为什么 Odoo 要把这么多行塞进同一个模型

account.move.line 在源码里有一组很醒目的 display_type 枚举:

  • product
  • tax
  • payment_term
  • discount
  • rounding
  • epd
  • line_section
  • line_subsection
  • line_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

这意味着:

  1. display_type 不是纯手工维护字段
  2. 它是下游会计语义的归类结果
  3. 你如果用自定义代码提前塞错科目或税行关系,Odoo 后面可能把整行分到完全不同的类型里

所以很多“为什么这行突然不在 invoice_line_ids 里了”的问题,本质不是 domain 坏了,而是这行已经被系统识别成另一种 display type。


section / note 行为什么必须“零金额、无科目”

源码里一组 SQL 约束特别能说明问题。

对于 line_section / line_subsection / line_note,系统强制要求:

  • amount_currency = 0
  • debit = 0
  • credit = 0
  • account_id IS NULL

这其实已经给出官方态度:

section/note 是排版与结构行,不是会计事实。

所以如果你在导入脚本、Studio、自定义模块里给 section 行硬塞科目或金额,问题不是“看起来怪一点”,而是会直接违反模型边界。

这也是为什么很多开发者第一次碰到报错会疑惑:

  • 明明只是想在发票里插个小节
  • 为什么数据库约束会拦我

答案是:因为 Odoo 不允许“展示行”和“记账行”混种。


invoice_line_ids 为什么看起来像“少了几行”

account.move 里,invoice_line_ids 的 domain 只保留:

  • product
  • line_section
  • line_subsection
  • line_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 line
  • tax 行是系统生成出来的税明细结果
  • rounding / epd 等行又有自己的会计语义

这说明:

Odoo 不是“产品行上挂一个税额字段就结束”,而是把税影响显式展开成真正的 move line。

这对开发的意义很大:

  • 你调税时,别只看产品行
  • 你做导出或对账时,要区分 base line 和 tax line
  • 你重写发票行同步逻辑时,不能把税行当普通用户编辑行处理

最常见的三个误区

误区 1:把 display_type 当纯 UI 字段

结果是 section 行被写进金额,或者付款条款行被误删。

误区 2:所有统计都直接遍历 line_ids

这样经常把税、付款条款、rounding 一起算进“商品金额”。

误区 3:只看 invoice_line_ids,却试图解释完整会计结果

这会漏掉税行和应收应付行,最后对不上总账。


开发时更稳的判断顺序

如果你要扩展发票/账单逻辑,建议按这个顺序判断:

  1. 先确认当前遍历的是 line_ids 还是 invoice_line_ids
  2. 再确认目标行的 display_type
  3. 再决定它是否应该参与: - 金额汇总 - 税计算 - 科目写入 - 报表展示
  4. 最后再做自定义字段或自动化逻辑

这个顺序看起来啰嗦,但它能避免 80% 的“为什么一切都对不上”的问题。


最后的判断句

在 Odoo 里,account.move.line.display_type 的本质不是“前端展示字段”,而是:

用一套统一的会计行模型,同时承载业务行、税行、付款条款行与结构行的分层语义开关。

理解了这一点,你再看发票页面里那张“像表格一样”的行列表,就会明白:

  • 用户看到的是一张表
  • Odoo 底层维护的是一组不同会计职责的行
  • display_type 正是这组职责分层的核心开关

DISCUSSION

评论区

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