06. 模型和字段
在上章节中,我们已经学习并创建了一个新的 Odoo 模块。但它仍是一个空壳,没有菜单也没有任何操作,也不能存储任何数据。接下来将学习如何在 estate 模块中,实现将房产相关的信息(名称、描述、价格、居住面积等)并存储到数据库中。
在继续本小节实践之前,请确保 estate 模块已安装,即在“应用程序”列表中, estate 模块的状态应显示为“已安装”(“启用”按钮已隐藏)。

对象关系映射
首先,创建名称为 estate_property 的数据库表:
$ psql -d odoodb
odoodb=# SELECT COUNT(*) FROM estate_property;
count
-------
0
(1 row)Odoo 的一个关键组件是 ORM 层,ORM 层避免了手动编写大部分 SQL 的需求,并提供了可扩展性和安全服务。
业务对象标准的做法是继承 odoo.models.Model 类,从而将业务对象集成到自动持久化系统中。
可以通过在模型定义中设置属性来配置模型。一个非常重要的属性是 _name,该属性为必填项,用于定义模型在 Odoo 系统中的名称。以下是一个模型的最简定义:
from odoo import models
class Property(models.Model):
_name = "estate_property"
_description = ""以上定义让 ORM 自动生成一个名称为 estate_property 的数据库表。按照惯例,所有模型都位于模块目录下的 models 目录中,且每个模型都在各自的 Python 文件中定义。
请看 crm_recurring_plan 表是如何定义的,以及相应的 Python 文件是如何被导入的:
该模型在文件 crm/models/crm_recurring_plan.py 中定义(参见此处)
文件 crm_recurring_plan.py 在 crm/models/__init__.py 中被导入(参见此处)
文件夹 models 在 crm/__init__.py 中被导入(参见[此处](odoo/addons/crm/init.py at e80911aaead031e7523173789e946ac1fd27c7dc · odoo/odoo · GitHub))
定义房地产属性模型
参照 CRM 模块中的示例,为 estate_property 表创建相应的文件和文件夹。
from odoo import models
class Property(models.Model):
_name = "estate.property"
_description = "房地产模型"ORM 根据上面定义自动生成一个名称为 estate_property 的数据库表,可以看到数据库表名与模型中 _name 属性的值相关联,Odoo 系统把 estate.property 替换成为 estate_property 来命名数据库中表的名称。按照惯例,所有模型都位于模块目录下的 models 目录中,且每个模型都在各自的 Python 文件中定义。
可参考 crm_recurring_plan 表是如何定义的,以及相应的 Python 文件是如何导入:
- 模型在文件
crm/models/crm_recurring_plan.py中定义,参考链接 。 - 在
crm/models/__init__.py中导入文件crm_recurring_plan.py定义的模型,参考链接 。 - 在
crm/__init__.py中导入文件夹models中定义的模型,参考链接 。
注:请勿使用可变的全局变量。 一个 Odoo 实例可以在同一个 Python 进程中并行运行多个数据库。由于每个数据库可能安装了不同的模块,因此我们不能依赖那些会根据已安装模块而发生变化的全局变量。
定义房地产属性模型
实践一: 参照上面 CRM 模块中的示例,创建 estate.property 模型相应的文件和文件夹。具体如下所示:
创建模型文件:estate/models/property.py
from odoo import models
class Property(models.Model):
_name = "estate.property"在文件:estate/__init.py 中导入文件模型包:
from . import models在文件:estate/models/__init.py 中导入文件模型包:
from . import property文件创建完成后,使用以下脚本重启服务器:
.\venv\Scripts\python.exe odoo-bin.py `
--db_host 127.0.0.1 --db_port=5432 -r userodoo `
-w 123456 -d odoodb --addons-path=addons -u estate `
--limit-time-real=120000或者( bat 脚本):
.\venv\Scripts\python.exe odoo-bin.py ^
--db_host 127.0.0.1 --db_port=5432 -r userodoo ^
-w 123456 -d odoodb --addons-path=addons -u estate ^
--limit-time-real=120000-u estate 表示要升级 estate 模块,即 ORM 将应用数据库模式变更,它会创建模型中对应的新表。-d odoodb 表示在 odoodb 这个数据库上执行升级。-u 必须始终与 -d 配合使用。
注:对 Python 文件进行任何修改都需要重启 Odoo 服务器,才能生效。
启动过程中控制台输出的日志记录,可以会看到类似以下警告内容(WARNING):
2026-05-03 08:03:15,217 8956 INFO doodb odoo.modules.loading: Loading module estate (2/15)
2026-05-03 08:03:15,278 8956 WARNING doodb odoo.registry: The model estate.property has no _description
2026-05-03 08:03:15,323 8956 INFO doodb odoo.registry: module estate: creating or updating database tables
2026-05-03 08:03:15,777 8956 INFO doodb odoo.addons.base.models.ir_module: module estate: no translation for language zh_CN
2026-05-03 08:03:15,811 8956 WARNING doodb odoo.modules.loading: The models ['estate.property'] have no access rules in module estate, consider adding some, like:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,0,0,0
2026-05-03 08:03:15,857 8956 INFO doodb odoo.modules.loading: Module estate loaded in 0.64s, 37 queries (+37 other)
2026-05-03 08:03:15,864 8956 WARNING doodb odoo.registry: The model estate.property has no _description
2026-05-03 08:03:15,937 8956 WARNING doodb odoo.registry: The model estate.property has no _description
2026-05-03 08:03:16,242 8956 WARNING doodb odoo.registry: The model estate.property has no _description
2026-05-03 08:03:16,256 8956 INFO doodb odoo.modules.loading: 15 modules loaded in 1.04s, 37 queries (+37 extra)从上面输出结果中可以看到,已经正常启动!为了保险起见可使用 psql 查看创建的表信息再次确认。数据库表中缺少了一个字段,需要在模型中添加一个 _description 字段,即可消除其中一条关于_description的警告信息。
到了这个阶段,数据库中已经创建了 estate_property,表的结构如下:
CREATE TABLE "public"."estate_property" (
"id" int4 NOT NULL DEFAULT nextval('estate_property_id_seq'::regclass),
"create_uid" int4,
"write_uid" int4,
"create_date" timestamp(6),
"write_date" timestamp(6),
CONSTRAINT "estate_property_pkey" PRIMARY KEY ("id"),
CONSTRAINT "estate_property_create_uid_fkey" FOREIGN KEY ("create_uid") REFERENCES "public"."res_users" ("id") ON DELETE SET NULL ON UPDATE NO ACTION,
CONSTRAINT "estate_property_write_uid_fkey" FOREIGN KEY ("write_uid") REFERENCES "public"."res_users" ("id") ON DELETE SET NULL ON UPDATE NO ACTION
)
;
ALTER TABLE "public"."estate_property"
OWNER TO "odoo_user";
COMMENT ON COLUMN "public"."estate_property"."create_uid" IS 'Created by';
COMMENT ON COLUMN "public"."estate_property"."write_uid" IS 'Last Updated by';
COMMENT ON COLUMN "public"."estate_property"."create_date" IS 'Created on';
COMMENT ON COLUMN "public"."estate_property"."write_date" IS 'Last Updated on';
COMMENT ON TABLE "public"."estate_property" IS 'estate.property';模型属性
属性(也叫字段,一般 Python 类中叫属性,数据表中叫字段)用于定义模型可以存储哪些数据以及这些数据存储的位置。在模型类中可以定义各种属性。如下一个简单的示例:
from odoo import fields, models
class Property(models.Model):
_name = "estate_property"
_description = "房地产模型"
name = fields.Char()name 字段是 Property 类的一个字符型属性,系统表示为 Python 的 Unicode 字符串,在数据库中存储为 SQL 的 VARCHAR 类型。
实践二: 在 estate.property 模型中添加一些属性,这些属性最终会被创建并存储在数据库表中。属性大致可分为两类:
- “简单”属性,即直接存储在模型表中的原子值;
- “关系”属性,用于链接(同一模型或不同模型的)记录。
在 estate.property 模型中添加基本属性。在对应的 estate_property 表中添加以下基本字段:
| 字段 | 类型 | 备注 |
|---|---|---|
| name | Char | 名称 |
| description | Text | 描述 |
| postcode | Char | 邮编 |
| date_availability | Date | 有效期 |
| expected_price | Float | 期望价 |
| selling_price | Float | 实际售价 |
| bedrooms | Integer | 房间数 |
| living_area | Integer | 居住面积 |
| has_garage | Boolean | 是否有车库 |
| has_garden | Boolean | 是否有庭院 |
| garden_area | Integer | 庭院面积 |
| garden_orientation | Selection | 朝向 |
garden_orientation 字段包含 4 个可能的取值:“朝北”、“朝南”、“朝东”和“朝西”。下拉列表定义为元组列表,示例请参见 - 此处 。
继续打开文件:models/property.py,在 Property 类中添加以下属性:
from odoo import models, fields, api, exceptions
from odoo.tools.float_utils import float_compare, float_is_zero
from odoo import models
from datetime import datetime
class Property(models.Model):
_name = "estate.property"
_description = "房地产模型"
name = fields.Char("标题/名称")
description = fields.Text("描述", )
postcode = fields.Char("邮编")
date_availability = fields.Date(string="有效期")
expected_price = fields.Float("期望价格")
selling_price = fields.Float("实际售价")
bedrooms = fields.Integer("房间数量")
living_area = fields.Integer("室内面积(M²)")
has_garage = fields.Boolean("是否有车库")
has_garden = fields.Boolean("是否有花园")
garden_area = fields.Integer("花园面积(M²)")
garden_orientation = fields.Selection(
string='朝向',
selection=[('north', '朝北'), ('south', '朝南'), ('east', '朝东'), ('west', '朝西')],
help="“朝向”可用于区分潜在客户和商机")Odoo 会根据上面模型的定义自动创建相应的数据库表:estate_property。数据库表字段的数据类型有:布尔型、浮点型、字符型、文本型、日期型和选择型。
将字段添加到模型后,然后使用 -u estate 参数重启服务器。
$ ./odoo-bin --addons-path=addons,dev-addons/ -d odoodb -u estate连接到 pgsql 数据库并查看 estate_property 表的结构,就会发现新增了几个字段,如下所示:
CREATE TABLE "public"."estate_property" (
"id" int4 NOT NULL DEFAULT nextval('estate_property_id_seq'::regclass),
"create_uid" int4,
"write_uid" int4,
"create_date" timestamp(6),
"write_date" timestamp(6),
"name" varchar COLLATE "pg_catalog"."default",
"bedrooms" int4,
"living_area" int4,
"garden_area" int4,
"postcode" varchar COLLATE "pg_catalog"."default",
"garden_orientation" varchar COLLATE "pg_catalog"."default",
"date_availability" date,
"description" text COLLATE "pg_catalog"."default",
"has_garage" bool,
"has_garden" bool,
"expected_price" float8,
"selling_price" float8,
CONSTRAINT "estate_property_pkey" PRIMARY KEY ("id"),
CONSTRAINT "estate_property_create_uid_fkey" FOREIGN KEY ("create_uid") REFERENCES "public"."res_users" ("id") ON DELETE SET NULL ON UPDATE NO ACTION,
CONSTRAINT "estate_property_write_uid_fkey" FOREIGN KEY ("write_uid") REFERENCES "public"."res_users" ("id") ON DELETE SET NULL ON UPDATE NO ACTION
)
;
ALTER TABLE "public"."estate_property"
OWNER TO "odoo_user";
COMMENT ON COLUMN "public"."estate_property"."create_uid" IS 'Created by';
COMMENT ON COLUMN "public"."estate_property"."write_uid" IS 'Last Updated by';
COMMENT ON COLUMN "public"."estate_property"."create_date" IS 'Created on';
COMMENT ON COLUMN "public"."estate_property"."write_date" IS 'Last Updated on';
COMMENT ON COLUMN "public"."estate_property"."name" IS 'Name';
COMMENT ON COLUMN "public"."estate_property"."bedrooms" IS '房间数量';
COMMENT ON COLUMN "public"."estate_property"."living_area" IS '室内面积(M²)';
COMMENT ON COLUMN "public"."estate_property"."garden_area" IS '花园面积(M²)';
COMMENT ON COLUMN "public"."estate_property"."postcode" IS '邮编';
COMMENT ON COLUMN "public"."estate_property"."garden_orientation" IS '朝向';
COMMENT ON COLUMN "public"."estate_property"."date_availability" IS '有效期';
COMMENT ON COLUMN "public"."estate_property"."description" IS '描述';
COMMENT ON COLUMN "public"."estate_property"."has_garage" IS '是否有车库';
COMMENT ON COLUMN "public"."estate_property"."has_garden" IS '是否有花园';
COMMENT ON COLUMN "public"."estate_property"."expected_price" IS '期望价格';
COMMENT ON COLUMN "public"."estate_property"."selling_price" IS '实际售价';
COMMENT ON TABLE "public"."estate_property" IS 'estate.property';常见属性
目标在本节结束时,表 estate_property 中的 name 和 expected_price 列应设置为不可为空:
dev-demo=# \d estate_property;
Table "public.estate_property"
Column | Type | Collation | Nullable | Default
--------------------+-----------------------------+-----------+----------+---------------------------------------------
...
name | character varying | | not null |
...
expected_price | double precision | | not null |
...与模型本身类似,字段可以通过将配置属性作为参数传递来进行配置:
name = fields.Char(required=True)某些属性适用于所有字段,以下是最常见的属性:
string(str, 默认值:字段名称) - 字段在用户界面中的标签(用户可见)。required(bool, 默认值:False) -若为True,则该字段不能为空。它必须具有默认值,或者在创建记录时必须始终提供值。help(str, 默认值:"") - 在 UI 中为用户提供长篇帮助提示。index(bool, 默认值:False) - 要求 Odoo 在该列上创建数据库索引。
为现有字段设置属性。添加以下属性(required=True):
| 字段 | 属性 | 备注 |
|---|---|---|
| name | required | |
| expected_price | required |
重启服务器后,这两个字段都应设置为不可为空。
自动字段
可能已经注意到,模型中存在一些您从未定义过的字段。Odoo会在所有模型中创建几个字段。这些字段由系统管理,无法进行写入操作,但在有用或必要时可以进行读取:
id (Id)- 该模型记录的唯一标识符。create_date (Datetime)- 记录的创建日期时间。create_uid (Many2one)- 创建该记录的用户。write_date (Datetime)- 记录的最后修改日期时间。write_uid (Many2one)- 最后修改该记录的用户。
虽然可以编写原始 SQL 查询,但需谨慎操作,因为这会绕过 Odoo 的所有身份验证和安全机制。