☰
Odoo 17数据字典解析:ORM映射、表名约定与SQL查询避坑
2026/11/4 7:51:40 网站建设 项目流程

简介:面向Odoo 17二次开发、模块定制与数据库运维人员的表结构速查文档,将官方PostgreSQL数据字典完整翻译为中文,用于解决英文表名与字段含义难以对应、开发调试时反复查库的痛点。压缩包内仅1个PDF文件,约8.07MB,由Navicat从本地PostgreSQL实例导出,按「服务器—数据库—模式—表—字段」的层级编排,目录可逐级跳转检索。内容覆盖account_account、account_account_account_journal_rel、account_account_tag、account_account_tag_account_move_line_rel等会计核心表,逐表列出字段名称、数据类型与关联关系,中英对照便于快速定位模型对应的物理表。对刚接触Odoo框架的开发者,可借此理解ORM模型与数据库表的映射规则;对实施与运维人员,则可用于排查数据异常、编写SQL查询与迁移脚本。目前已有135人学习,适合作为日常开发中的随查手册。

1. Odoo 17 数据字典不是表清单,而是一层 ORM 快照

很多人拿到「odoo 17 数据字典(已翻译为中文)」的第一反应,是从account_account往下按字母顺序一路翻,翻到第三十张表就开始头大。反直觉的地方在于:这份字典真正有用的信息不是「一共有多少张表」,而是它把 Odoo 的 ORM 层原样摊开成了 PostgreSQL 的 public 模式结构——表对应模型,列对应字段,注释被译成中文方便对照。它由 Navicat 从localhost的odoo库导出,一路展开到表 → 列 → 类型,等于给了你一份模型层到 SQL 层的对照表。

所以它的读者也很明确:做二次开发,要先确认某个字段到底落到数据库列还是只算在内存里的;写报表 SQL,要绕开 ORM 直接查库的;以及做数据迁移、要把外部系统的数据灌进来的。整份字典的结构其实分四层堆叠:业务主表、单据行表、关系中间表、向导临时表,外面再套一层ir_/res_的系统表。先把这四层的边界认清楚,后面几百张表就只是往格子里填空。

2. 表名即索引:前缀定归属,后缀定角色

真正把几百张表压成可记忆规模的,是 Odoo 的命名约定。它几乎把所有信息都编码进了表名,只要你会拆前缀和后缀,看到陌生表也能猜出七八分归属,再回字典里验证列结构就行。

2.1 前缀决定这张表归哪个模块管

res_是 resource 的缩写,放的是跨模块共用的主数据,res_partner(伙伴)、res_company(公司)、res_currency(货币)、res_users(用户)、res_country(国家)都在这一组。ir_是 internal resource,属于框架自身的元数据层:ir_model记录有哪些模型,ir_model_fields记录每个模型有哪些字段,ir_ui_view存视图定义,ir_attachment存附件,ir_cron存定时任务,ir_model_data存外部 ID(xmlid)。这一组表是「字典的字典」,你查不到某个字段来源时,最后往往要回到ir_model_fields。

业务模块的前缀直接跟着模块名走:account_归会计,sale_归销售,purchase_归采购,stock_归库存,product_归产品,mrp_归制造,mail_归消息与活动,base_放底层配置(语言、时区、国家组),web_和bus_偏前端与长连接。前缀相同不代表同属一张业务单据,account_journal(日记账)是配置,account_move(凭证)是业务单据,account_account(科目)是主数据,三者的读写频率和生命周期完全不同,混在一起看就容易乱。

2.2 后缀暴露这张表扮演什么角色

后缀的信息量比前缀更大,梳理下来大概有这么几类:

后缀 / 形态含义典型例子
_rel多对多关系的中间表,只有两列外键account_account_account_journal_rel
_line单据行(一对多的「多」端)account_move_line、sale_order_line
_wizard瞬态模型,数据用完即清account_accrued_orders_wizard
_tag标签类主数据,常与业务表多对多account_account_tag
_group分组配置account_group、account_journal_group
_type类型枚举表常见于分类体系
_stage/_state阶段或状态配置流程类模块常见

_rel这一类的命名规则值得单独记:它由两张被关联的表名拼起来,按字母序排列,末尾加_rel。account_account和account_journal拼出来就是account_account_account_journal_rel,看名字顺序就能反推谁是左表、谁是右表。这类表里通常只有xxx_id和yyy_id两列加一个唯一索引,没有业务字段,把它当业务实体查一定会翻车。

_wizard也不是绝对规律。瞬态模型(TransientModel)在库里确实会被定期清理,但表名不一定带这个后缀,account_payment_register、account_move_send、account_move_reversal都是向导性质的表,名字里却没有_wizard。判断依据应该是「这张表的数据是不是一次操作就作废」,而不是后缀。

2.3 表名和模型名之间隔着一层转换

绝大多数情况下,表名就是把模型名的点换成下划线:account.move.line对应account_move_line,stock.quant对应stock_quant。但历史遗留会打破这个规律,最典型的是ir_act_window对应ir.actions.act_window,ir_act_report_xml对应ir.actions.report。这类表你按点换下划线的思路去搜是搜不到的,必须先在ir_model里查模型名,再拿model字段回查表名。

还有一个反直觉的点:不是所有模型都有表。抽象模型(AbstractModel)只提供字段和行为,不建表,最典型的是mail.thread——你在字典里找不到mail_thread这张表,但几乎每张业务表都可能有message_main_attachment_id、message_has_error这类由它注入的列。它真正落地的数据在mail_message里,靠model+res_id两列做多态关联,字典里看不到外键约束。这也是为什么纯看字典画不出完整 ER 图,多态关联的那部分关系是隐式的。

查陌生业务时的顺序建议固定下来:先按前缀锁定模块,再找主表(不带后缀的那张),然后找_line看明细,找_rel看多对多,最后回到account_journal/account_payment_term这类配置表补齐语义。按这个顺序走,比按字母序翻要快得多。

3. 核心业务表群:四组主干与它们的从表

把使用频率最高的几张主干表拆开看,字典才真正变成能查的东西。会计、产品、库存、销售这四组基本覆盖了日常查数的八成场景,其余的模块大多是同一套模式换个前缀。

3.1 会计:凭证头与凭证行的双层结构

account_move是凭证头,account_move_line是凭证行,一张凭证对应一到多行。理解这两张表的关键是三个字段。move_type决定这张凭证是什么单据:entry是手工分录,out_invoice是销项发票,out_refund是销售退款,in_invoice是进项发票,in_refund是采购退款,out_receipt/in_receipt是收据。state控制生命周期:draft草稿、posted已过账、cancel已取消,只有posted的数据才影响账面。journal_id指向account_journal,而日记账的type又决定了这张凭证走销售、采购、现金、银行还是杂项。

account_move_line上最容易被误读的是金额字段。debit和credit永远是正数,方向靠两列谁为零来表达;balance等于debit - credit,是带符号的净额;amount_currency和currency_id只有在原币与公司本位币不同时才有意义。行上还有一个display_type,取值包括product、tax、payment_term、line_section、line_note等,用来区分这行是真实业务行还是小计、说明、付款条款这类展示行。做汇总时如果不加display_type = 'product'过滤,金额会翻倍。

account_account是科目主数据,code是科目编码,account_type决定它在报表里的归属(资产、负债、收入、费用等),reconcile标记该科目是否允许核销。多公司库里这张表的company_id必须一起带上,否则科目编码会串。account_partial_reconcile和account_full_reconcile记录核销关系,前者是部分核销的明细,后者是把一组行标记为完全核销后的头记录。

3.2 产品与伙伴:模板、变体和层级

product_template存的是「通用属性」——名称、分类、售价、类型、计量单位;product_product存的是「具体 SKU」,通过product_tmpl_id回指模板。只有一个规格的商品,两张表是一对一;有颜色、尺寸等多属性的商品,一个模板对多个变体。查询时最容易犯的错是想查条码和内部参考却只查了模板表,barcode和default_code这类编码字段的落点在变体层,商品编码是跟着具体 SKU 走的。

product_template的type取值在 17 里是consu(可消耗)、service(服务)、product(存储商品),其中consu又靠is_storable再分是否管理库存。categ_id指向product_category,分类本身是树形结构,靠parent_id串起来,complete_name是拼好的全路径。standard_price是成本价,list_price是销售价,前者在多公司场景下按公司存值,取数方式和普通数值列不一样。

res_partner是客户、供应商、联系人共用的一张表,靠customer_rank和supplier_rank两个整数区分身份——大于零就是客户或供应商,而不是靠某个布尔开关。parent_id表达公司-联系人的从属关系,company_id表达这条记录属于哪个公司,commercial_partner_id指向最终结算主体(顺着parent_id一路往上找的公司级记录)。做客户维度汇总时用commercial_partner_id比用partner_id更稳,否则同一家公司的多个联系人会被拆成多行。

3.3 库存与销售:移动、库存量和单据行

stock_move记录每一次库存移动的意图和结果,product_id是商品,location_id是来源库位,location_dest_id是目标库位,product_uom_qty是需求数量,quantity是实际完成数量,state从draft走到done。stock_move_line是移动的明细行,一个移动可以拆成多行(比如按批次、按序列号)。stock_quant是库存快照表,quantity是账面数量,reserved_quantity是已预留数量,可用量等于两者之差。查实时库存查stock_quant,查库存变动历史查stock_move。

sale_order是销售订单头,state取值有draft、sent、sale、done、cancel,invoice_status单独表达开票进度(upselling、invoiced、to invoice、no)。sale_order_line是订单行,product_uom_qty是订购数量,qty_delivered是已发货数量,qty_invoiced是已开票数量,price_unit是单价,discount是折扣百分比。订单行与库存移动之间不是直接外键,而是通过stock_move.sale_line_id反向指回来,这个方向在字典里不显眼,但查「某订单发了多少货」时必须用它。

stock_picking是调拨单头,stock_picking_type是调拨类型(收货、发货、内部调拨),picking_type_id决定这张单走哪个作业流程。stock_location是库位,usage区分内部库位、客户、供应商、库存损失、生产等,complete_name是全路径。

3.4 配置表:字段不多但决定语义

配置类表普遍列很少,但缺了它业务表的字段就没法解释。account_journal的type决定凭证性质,code是日记账简码,凭证编号规则也挂在这张表上。account_payment_term是付款条款头,account_payment_term_line是条款行,nb_days是相对天数,delay_type区分「几天后」和「月末后几天」,算到期日必须两张表一起看。account_fiscal_position是税位映射,account_fiscal_position_tax和account_fiscal_position_account分别存税和科目的映射关系,跨国业务里同一笔交易换一个税位,税率和科目就全变了。

account_cash_rounding是现金舍入规则,rounding是舍入精度,strategy决定是加一行调整行还是直接改金额。这类表的数据量通常个位数,但它们决定了业务表的金额是怎么算出来的,跳过它们直接看account_move_line,很多数字会对不上。

4. 字段列传:ORM 字段落到 PostgreSQL 列的真实样子

字典翻到列这一层,才是真正开始抠细节的地方。Odoo 的字段类型和 PostgreSQL 类型不是一一对应,而且有一批列是框架自动加的,你在模型定义里根本看不到它们。

4.1 每个模型都有的几个自动列

任何一张业务表都会带上这几列:id(主键,整数自增)、create_uid(创建人,指向res_users)、create_date(创建时间)、write_uid(最后修改人)、write_date(最后修改时间)。这四列不在模型里定义,但字典里每张表都有。做增量同步时,write_date是最常用的水位线字段,比id可靠,因为记录被修改时id不变。

active是软删除标记,绝大多数业务表都有,默认true。Odoo 的删除操作是把active置为false,物理行还在表里。sequence是排序号,company_id是多公司隔离键,currency_id出现在所有带金额的表上。这几列在字典的注释里往往只有一句话,但它们决定了你的 SQL 要不要加过滤条件。

4.2 类型映射:从 Char 到 jsonb

常见字段类型和 PostgreSQL 列的对应关系大概是这样的:

ORM 字段类型PostgreSQL 类型说明
Char/Selectionvarchar选择项存的是键值字符串,不是显示名
Text/Htmltext无长度限制
Integerint4外键列也是这个类型
Float/Monetarynumeric或double precision以字典里标注的实际类型为准,别凭经验猜
Booleanbool只有真假两值
Datedate不带时区
Datetimetimestamp存的是 UTC,展示时按用户时区换算
Binarybytea附件内容常见
Jsonjsonb结构化数据、翻译字段常落在这里

最容易踩的是Datetime。数据库里存的是 UTC 时间,界面上看到的是用户时区换算后的时间,两者差几个小时。做跨时区报表时,要么在 SQL 里显式转换,要么在应用层换算,直接在 SQL 里按日期分组会把跨零点的单据分到错误的日期。Date和Datetime也不能混用,account_move.date是date类型,create_date是timestamp,拿两者直接比较会得到意料之外的结果。

4.3 关系字段:只有 many2one 会落列

四种关系字段在数据库里的表现完全不同。Many2one落成一列xxx_id,整数类型,通常带外键约束和索引,比如account_move_line.move_id。One2many不落任何列,它是反向查询,靠对方表的xxx_id实现,account_move.line_ids在account_move表里找不到对应列。Many2many落成中间表,就是前面说的_rel。One2one在 Odoo 里用Many2one加唯一约束模拟,表现和Many2one一样。

知道这个规律之后,查「这张表有没有指向那张表的外键」就不用一列列看,直接搜_id结尾的列名即可。反过来,如果模型上定义了一个One2many却找不到对应的_id列,说明对方表用了别的字段名,回ir_model_fields查relation_field就能定位。

4.4 翻译字段、计算字段和按公司存值的列

被翻译的Char/Text字段在库里不是单个字符串,而是jsonb,键是语言代码,值是对应语言的文本,形如{"en_US": "Chair", "zh_CN": "椅子"}。直接拿name = '椅子'去比较永远查不到,必须用->>取值。字典里这类列的注释通常写着「名称」,从注释看不出它是 jsonb,只有看类型那一栏才能发现。

计算字段分两种。compute没加store=True的,库里根本没有这一列,值在读取时实时算出来;加了store=True的才会落列,但只在依赖字段变化时重算,你直接改基础列不会触发重算。related字段同理,store=False时是穿透读取,store=True时是复制一份存下来。

还有一类列,注释写着数值(比如成本价),但类型是jsonb,值是{"1": 12.5, "2": 13.0}这种以公司 id 为键的结构。这是按公司存值的字段,多公司库里必须按当前公司 id 取键再转数值,直接求和会报类型错误。

查凭证行的一个典型写法,把上面几个坑都绕开了:

-- 按公司 + 期间统计已过账凭证的真实业务行金额 SELECT am.date, am.name AS move_name, aa.code AS account_code, SUM(aml.balance) AS net_balance FROM account_move_line aml JOIN account_move am ON am.id = aml.move_id JOIN account_account aa ON aa.id = aml.account_id WHERE am.state = 'posted' -- 只统计已过账 AND am.company_id = 1 -- 多公司必须带公司键 AND am.date >= DATE '2024-01-01' AND am.date < DATE '2024-02-01' -- 半开区间,避免边界重复 AND aml.display_type = 'product' -- 过滤小计/说明/税行 AND aml.active -- 软删除过滤 GROUP BY am.date, am.name, aa.code ORDER BY am.date, aa.code;

这段 SQL 里,state = 'posted'保证只算已过账数据,company_id保证不串公司,半开区间避免月初月末重复计入,display_type = 'product'排掉合计行和说明行,active排掉被软删除的行。五个条件少任何一个,结果都会偏。参数上唯一需要按场景改的是期间范围和公司 id,其余条件在报表场景里基本是固定搭配。

5. 易错点排查:查数据字典时最容易翻车的五处

字典本身是中立的,坑都在「照着字典想当然」的地方。下面五条是系统梳理时最容易被漏掉的,每一条都对应一种具体的翻车方式。

5.1 易漏点:把_rel中间表当成业务表

为什么漏:这类表名由两张业务表拼成,看起来像是某个业务实体,而且它确实有id列、有create_date,长得和普通表没区别。真正的问题是它只有两个外键列,没有业务字段,你按业务表去理解它,就会试图在它上面找金额、找状态、找数量,找不到又以为字典缺页。

怎么补:判断方法很直接,看列数。只有id、两个xxx_id、加自动列的表,一律归为关系表。查关联关系时不要在这张表上做聚合,而是把它当桥,两边各 JOIN 一次主表。比如查某个科目被哪些日记账使用,就从account_account_account_journal_rel分别连account_account和account_journal,而不是在这张桥表上找科目名。

5.2 易漏点:忘了active过滤,统计口径偏大

为什么漏:Odoo 界面上删除一条记录,看起来就消失了,很容易以为数据库里也没了。实际上只是active被置为false,行还躺在表里。字典里这一列的注释通常只有「有效」两个字,翻页时很容易划过去。

怎么补:所有面向业务的查询都默认加上AND active或AND active IS NOT FALSE。用后者的原因是历史数据里可能存在NULL,而active IS NOT FALSE能把NULL和true都算进来。做数据核对时,反而要专门查一次active = false的行数,两边的差额往往能解释「界面数量和库里数量对不上」的问题。

5.3 易漏点:拿=去比较翻译字段

为什么漏:字典的注释栏写的是「名称」「描述」,看起来就是个字符串。类型栏里那个jsonb太不起眼,等到写了WHERE name = '椅子'查不出数据,还以为是数据没录进去。

怎么补:先看类型,jsonb就用->>取值再比较,注意语言键的写法是zh_CN、en_US这种带下划线的形式,不是zh-CN。排序和模糊匹配也要写在取值之后,例如ORDER BY name->>'zh_CN'。如果一段 SQL 里要反复用同一个翻译字段,可以在子查询里先取出来再外层过滤,可读性会好很多。

5.4 易漏点:按公司存值的列被当成普通数字列

为什么漏:这类列的注释写得像普通数值——「成本」「价格」——但存储结构是以公司 id 为键的jsonb。单公司环境下取值恰好能对上,一旦换到多公司库,SUM(standard_price)要么报错,要么算出一堆无意义的数字。

怎么补:见到「价格」「成本」「科目编码」这类字段,先确认类型再写聚合。取值时按当前公司 id 取键,再转成数值,例如(standard_price->>'1')::numeric,其中1是company_id。做跨公司对比报表时,先把 jsonb 展开成(company_id, value)两列再聚合,不要在原始结构上直接算。

5.5 易漏点:绕过 ORM 直接改库,计算字段不同步

为什么漏:字典让你看到了表结构,很容易产生「既然知道列名,直接UPDATE最快」的想法。但 Odoo 的store=True计算字段只在依赖变化时由 ORM 触发重算,你从数据库层面改了基础列,计算列不会跟着动,缓存里的旧值也可能继续被读到。

怎么补:涉及业务数据的修改一律走 ORM,哪怕是批量操作,也写成脚本调用模型的write方法。确实需要直接改库的场景(比如数据修复),改完之后要显式触发相关字段的重算,并清理缓存,否则界面显示和库里数据会长期不一致。这条是血泪经验,一次批量改价没走 ORM,后面花了大半天才对上账。

6. 复习清单:把几百张表读成二十来张母表的三步法

复习这份字典,不建议从头再翻一遍。更有效的方式是做减法:把几百张表归并成二十来张「母表」,剩下的都是母表的行、关系或配置。我自己的三步是这样的。

第一步,默写前缀表。拿出一张白纸,写出res_、ir_、account_、sale_、purchase_、stock_、product_、mail_这八个前缀各自管什么,每个前缀下面至少说出三张表名。说不出来的回去补,这一步决定你看到陌生表时有没有方向感。

第二步,把核心表的两两关系画出来。重点就五组:account_move↔account_move_line、product_template↔product_product、sale_order↔sale_order_line、stock_move↔stock_move_line、res_partner↔res_company。每组要能说清谁是头谁是行、外键在哪张表上、多对多走的是哪张_rel。

第三步,反向查字段来源。随便挑一个业务字段(比如订单行的折扣),从ir_model_fields查到它属于哪个模型,再回到字典确认它落在哪张表的哪一列、是什么类型、有没有翻译或按公司存值。这一步练的是「从字段反推表」的能力,比正着背表名有用得多。

自查可以用下面这张表过一遍,每项能不看资料说清楚就算过关:

自查项过关标准
表名后缀看到_rel、_line、_wizard能立刻判断角色
模型与表知道ir_act_window这类历史命名怎么对应
自动列能说出create_uid、write_date、active各自的用途
关系字段清楚One2many不落列、Many2many落中间表
特殊类型见到jsonb知道要取键,见到timestamp知道是 UTC
过滤条件写查询时默认带上active、company_id、state

查字段来源可以用这段 SQL 起手,把模型名换成你要查的即可:

-- 查某个模型下所有字段的落库情况 SELECT f.name AS field_name, f.ttype AS field_type, f.store AS is_stored, -- false 表示库里没有这一列 f.relation AS rel_model, -- many2one/many2many 指向的模型 f.translate AS is_translated -- true 表示列类型是 jsonb FROM ir_model_fields f JOIN ir_model m ON m.id = f.model_id WHERE m.model = 'sale.order.line' ORDER BY f.name;

store = false的字段不要再去表里找列,translate = true的字段记得用->>取值,relation有值的字段顺着它去找关联表。三个参数一对照,字段的落库形态就全清楚了。

我现在每次接手一个不熟的模块,都会先跑一遍这段 SQL,把store和translate两列扫一遍,再去翻字典对应的表。这个习惯是被坑出来的——早年直接照着字典列名写查询,遇到计算字段和翻译字段各翻一次车,后来就再也不敢跳过这一步了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询