Coding

Huo15 Odoo19 Module Dev

Try it

Odoo 19 模块开发技能 — 从项目结构、模型定义、视图设计到业务逻辑,完整覆盖 Odoo 19 模块开发全流程

What it does

用户提到以下内容时激活本技能: Odoo19 模块开发 开发 Odoo 模块 Odoo 模型/视图/业务逻辑 Odoo manifest / Odoo 插件安装 Odoo XML/JS/CSS 资源

The skill document

SKILL.md — huo15-odoo19-module-dev

触发条件

用户提到以下内容时激活本技能:

  • Odoo19 模块开发
  • 开发 Odoo 模块
  • Odoo 模型/视图/业务逻辑
  • Odoo manifest / __manifest__.py
  • Odoo 插件安装
  • Odoo XML/JS/CSS 资源

Odoo 模块开发核心概念

模块结构(标准化)

my_module/
├── __init__.py          # 模块导入声明
├── __manifest__.py      # 元数据清单(必需)
├── models/
│   ├── __init__.py
│   └── models.py         # 所有模型定义
├── views/
│   ├── views.xml         # 视图定义
│   └── templates.xml     # Web 模板
├── security/
│   └── ir.model.access.csv
├── data/
│   └── demo.xml          # 演示数据
├── controllers/
│   ├── __init__.py
│   └── controllers.py    # HTTP 路由
├── wizards/
│   ├── __init__.py
│   └── wizard.py         # 向导/向导模型
└── static/
    ├── src/xml/          # JS/QWeb 模板
    ├── src/js/           # JavaScript
    └── src/css/          # CSS

__manifest__.py 最小模板

{
    'name': "我的模块",
    'version': '1.0.0',
    'summary': '模块一句话描述',
    'description': """
        模块详细说明(多行)
    """,
    'category': 'Hidden',  # Hidden/Productivity/Manufacturing/...
    'author': '火一五信息科技',
    'website': 'https://www.huo15.com',
    'license': 'LGPL-3',
    'depends': ['base'],
    'data': [
        'security/ir.model.access.csv',
        'views/views.xml',
    ],
    'installable': True,
    'application': False,
}

模型开发(Model)

基本模型模板

from odoo import models, fields, api
from odoo.exceptions import ValidationError

class MyModel(models.Model):
    _name = 'my.model'           # 必填,点分格式
    _description = '我的模型'    # 模型描述
    _order = 'sequence, id'      # 默认排序
    _inherit = []                # 继承,可为空列表

    name = fields.Char(
        string='名称',
        required=True,
        index=True,
        help='这是帮助文本'
    )
    
    active = fields.Boolean(
        string='活跃',
        default=True,
        index=True
    )
    
    description = fields.Text(string='描述')
    
    date = fields.Date(string='日期')
    datetime = fields.Datetime(string='时间')
    
    # 关联字段
    partner_id = fields.Many2one(
        'res.partner',
        string='客户',
        index=True,
        ondelete='cascade'  # cascade/restrict/set null
    )
    
    line_ids = fields.One2many(
        'my.model.line',
        'parent_id',
        string='明细行'
    )
    
    # 计算字段
    amount_total = fields.Float(
        string='合计',
        compute='_compute_amount',
        store=True  # 存储以便搜索
    )
    
    # 状态字段
    state = fields.Selection([
        ('draft', '草稿'),
        ('confirm', '已确认'),
        ('done', '完成'),
        ('cancel', '已取消'),
    ], string='状态', default='draft', index=True)
    
    # SQL 约束
    _sql_constraints = [
        ('name_unique', 'UNIQUE(name)', '名称不能重复!'),
    ]
    
    @api.constrains('name')
    def _check_name(self):
        for record in self:
            if not record.name:
                raise ValidationError('名称不能为空')
    
    @api.depends('line_ids.price', 'line_ids.qty')
    def _compute_amount(self):
        for rec in self:
            rec.amount_total = sum(
                line.price * line.qty 
                for line in rec.line_ids
            )

关系字段对比

字段类型语法用途
Many2onefields.Many2one('other.model', ...)多选一(外键)
One2manyfields.One2many('other.model', 'rel_field', ...)一对多
Many2manyfields.Many2many('other.model', ...)多对多
Referencefields.Reference(...)可变模型引用

常用字段参数

# 通用参数
string='标签'           # UI 显示名
required=True          # 必填
index=True             # 数据库索引
help='提示文本'         # 鼠标悬停提示
default=...             # 默认值
copy=True/False         # 复制时是否带值
readonly=True          # 只读
store=True             # 存储到数据库(计算字段)
tracking=True           # 变更追踪(邮件通知)

# Char
size=200               # 最大长度(旧版,新版已废弃)
translate=True          # 允许翻译

# Numeric
digits=(16, 2)         # 总位数,小数位

# Date/Datetime
calendar='gregorian'   # 日历类型

# Selection
selection=[('a','A'),('b','B')]  # 选项列表

核心 API 方法

# 创建
record = self.env['my.model'].create({'name': 'value'})

# 搜索
records = self.env['my.model'].search([
    ('active', '=', True),
    ('name', 'like', 'test%'),
])

# 读取
record.read(['name', 'active'])
record.mapped('partner_id.name')  # 提取字段

# 写入
record.write({'name': 'new name'})

# 删除
record.unlink()

# 复制
record.copy()

# 过滤
filtered = records.filtered(lambda r: r.active)

# 排序
sorted_records = records.sorted(key=lambda r: r.create_date)

模型方法覆盖

class SaleOrder(models.Model):
    _inherit = 'sale.order'
    
    # 覆盖 create 方法
    @api.model
    def create(self, vals):
        # 添加默认值或预处理
        vals['client_order_ref'] = vals.get('name', '')
        return super().create(vals)
    
    # 覆盖 write 方法
    def write(self, vals):
        # 业务逻辑
        if 'state' in vals and vals['state'] == 'sale':
            self.mapped('order_line').write({'state': 'sale'})
        return super().write(vals)
    
    # 按钮方法
    def action_confirm(self):
        for order in self:
            if not order.partner_id:
                raise ValidationError('请先选择客户')
            order.write({'state': 'sale'})
        return True
    
    # 动作返回
    def action_view_invoice(self):
        # 返回视图
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'account.move',
            'view_mode': 'tree,form',
            'domain': [('id', 'in', self.invoice_ids.ids)],
        }

视图开发(Views)

Window Action + Menu

<?xml version="1.0" encoding="utf-8"?>

    <!-- 窗口动作 -->
    
        我的模型
        my.model
        tree,form
        {'default_active': True}
        
            
                创建您的第一条记录
            
        
    

    <!-- 菜单 -->
    
              sequence="10"/>

Tree(列表)视图


      <!-- 拖拽排序 -->
    
    
    
      <!-- sum/x editable 属性 -->
    
    
    <!-- 按钮 -->
    

Form(表单)视图


    
        <!-- 头部 -->
        
            
                
            
        
        
        
            
                
                
                
            
            
                
            
        
        
        <!-- Notebook 分页 -->
        
            
                
                      <!-- bottom/top -->
                        
                        
                        
                        
                    
                
            
            
                
            
        
    

搜索视图


    
    
    
    
    
    
    
    
    =', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
    
    
        
        
        
    

看板视图(Kanban)


    
        
            
                
                    
                
                
                
                
                    
                
            
        
    


权限控制(ACL)

ir.model.access.csv

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_my_model_user,my.model.user,model_my_model,base.group_user,1,1,1,1
access_my_model_manager,my.model.manager,model_my_model,,1,1,1,1

组ID选项:

  • base.group_user — 普通用户
  • base.group_portal — 门户用户
  • base.group_public — 匿名访客
  • 空 = 所有用户

权限位: perm_read,perm_write,perm_create,perm_unlink — 1=允许,0=拒绝


业务逻辑(Business Logic)

按钮与动作

class MyModel(models.Model):
    _name = 'my.model'
    
    def action_draft(self):
        """重置为草稿"""
        self.write({'state': 'draft'})
        return True
    
    def action_confirm(self):
        """确认"""
        for rec in self:
            if rec.state != 'draft':
                raise UserError('只能确认草稿状态的记录')
            rec.write({'state': 'confirm'})
        return True
    
    def action_done(self):
        """完成"""
        self.write({'state': 'done'})
        # 触发其他逻辑
        self.mapped('line_ids').write({'done': True})
        return True
    
    def action_cancel(self):
        """取消"""
        for rec in self:
            if rec.invoice_count > 0:
                raise UserError('有关联发票,不能取消')
        self.write({'state': 'cancel'})
        return True
    
    def unlink(self):
        """删除前检查"""
        for rec in self:
            if rec.state == 'done':
                raise UserError('不能删除已完成的记录')
        return super().unlink()

计算字段

from odoo import api

class MyModel(models.Model):
    _name = 'my.model'
    
    price = fields.Float(string='单价')
    qty = fields.Float(string='数量')
    tax = fields.Float(string='税率')
    
    subtotal = fields.Float(
        compute='_compute_subtotal',
        store=True,
        string='小计'
    )
    
    total = fields.Float(
        compute='_compute_total',
        store=True,
        string='含税合计'
    )
    
    @api.depends('price', 'qty')
    def _compute_subtotal(self):
        for rec in self:
            rec.subtotal = rec.price * rec.qty
    
    @api.depends('subtotal', 'tax')
    def _compute_total(self):
        for rec in self:
            rec.total = rec.subtotal * (1 + rec.tax / 100)
    
    @api.onchange('price', 'qty')
    def _onchange_price_qty(self):
        """当单价或数量变化时触发"""
        if self.price and self.qty:
            self.subtotal = self.price * self.qty
        return {
            'warning': {
                'title': '提示',
                'message': '小计已自动更新'
            }
        }

约束验证

from odoo.exceptions import ValidationError, UserError

class MyModel(models.Model):
    _name = 'my.model'
    
    date_start = fields.Date(string='开始日期')
    date_end = fields.Date(string='结束日期')
    
    @api.constrains('date_start', 'date_end')
    def _check_dates(self):
        for rec in self:
            if rec.date_end and rec.date_start > rec.date_end:
                raise ValidationError(
                    '开始日期不能晚于结束日期!'
                )
    
    @api.constrains('amount')
    def _check_amount(self):
        for rec in self:
            if rec.amount < 0:
                raise ValidationError('金额不能为负数')

服务端动作(Server Action)


    批量确认
    
    code
    
records = env['my.model'].browse(context.get('active_ids'))
records.action_confirm()
    


    批量取消
    
    code
    
records = env['my.model'].browse(context.get('active_ids'))
for rec in records.filtered(lambda r: r.state == 'draft'):
    rec.action_cancel()
    


自动动作(Automated Actions / Cron)

class MyModel(models.Model):
    _name = 'my.model'
    
    @api.model
    def _cron_check_overdue(self):
        """定时任务:检查逾期"""
        overdue = self.search([
            ('state', 'in', ['draft', 'confirm']),
            ('date_end', '<', fields.Date.today())
        ])
        for rec in overdue:
            rec.write({'state': 'overdue'})
            # 发送通知邮件
            rec._send_overdue_notification()
    
    def _send_overdue_notification(self):
        """发送逾期通知"""
        self.ensure_one()
        template = self.env.ref('my_module.email_template_overdue')
        if template:
            template.send_mail(self.id, force_send=True)

权限记录规则(Record Rule)

<!-- security/ir.model.access.csv 同级 security/record_rules.xml -->

    只能查看自己的记录
    
    
    
    <!-- perm_read/perm_write/perm_create/perm_unlink -->


开发工作流

1. 创建模块骨架

# 进入 addons 目录
cd ~/.openclaw/workspace/huo15-odoo19-docker/custom_src/odoo/addons

# 创建模块目录
mkdir -p my_module/{models,views,security,data,controllers,wizards,static/src/js,static/src/xml}

# 创建 __init__.py
echo 'from . import models' > my_module/__init__.py
echo 'from . import controllers' >> my_module/__init__.py
echo 'from . import wizards' >> my_module/__init__.py

# 创建 models/__init__.py
echo 'from . import models' > my_module/models/__init__.py

# 创建其他 __init__.py
touch my_module/controllers/__init__.py
touch my_module/wizards/__init__.py

2. 编写模块清单

# my_module/__manifest__.py
{
    'name': '我的模块',
    'version': '1.0.0',
    'author': '火一五信息科技',
    'category': 'Productivity',
    'depends': ['base', 'mail'],
    'data': [
        'security/ir.model.access.csv',
        'views/views.xml',
        'data/demo.xml',
    ],
    'installable': True,
}

3. 安装与调试

# 升级模块(在 Odoo 界面或命令行)
# 界面:应用 → 搜索模块 → 升级

# 或通过命令行
docker exec -it odoo19 odoo-bin -u my_module -d huo15 --stop-after-init

# 查看日志
docker logs -f odoo19

4. 常见问题排查

问题排查方法
模块不显示检查 __manifest__.py 语法;确认在 addons 路径
视图报错检查 XML 语法;查看 Odoo 日志定位标签
权限不足检查 ir.model.access.csv 配置
计算字段不更新确认 store=True;检查 @api.depends 依赖
按钮无响应检查 type="object";确认方法签名正确

最佳实践

代码组织

  • 一个模型一个文件models/sale_order.py
  • 常量为类属性STATE = [('draft','草稿'),...]
  • 按钮方法简洁:委托给 _action_* 私有方法

命名规范

元素规范示例
模块目录下划线my_module
模型名x_(避免冲突)x_my_model
字段名下划线partner_id
方法名下划线_compute_amount
视图IDmodel_name_view_typemy_model_tree

安全建议

  1. 始终使用 raise UserError 而不是 raise Exception
  2. unlink() 前检查状态
  3. 金额字段用 Decimal,避免浮点精度问题
  4. sudo() 只在必要时用,记录原因

快速参考

模型三要素

class MyModel(models.Model):
    _name = 'x.my.model'      # 点分格式,唯一
    _description = '我的模型'  # 用户可见名称
    _inherit = []              # 继承 ['base.model']

常用字段速查

fields.Char(size=XX)          # 文本(size已废弃用 Char)
fields.Text()                  # 多行文本
fields.Html()                  # 富文本
fields.Integer()               # 整数
fields.Float(digits=(16,2))   # 小数
fields.Boolean()               # 布尔
fields.Date()                  # 日期
fields.Datetime()              # 日期时间
fields.Binary()                # 二进制/文件
fields.Selection([(...)])     # 单选
fields.Many2one('res.partner') # 多选一
fields.One2many('sale.line','order_id')  # 一对多
fields.Many2many('res.users')  # 多对多

常用装饰器

@api.model         # 不依赖记录,用 cls
@api.depends()     # 计算字段依赖
@api.onchange()    # UI 自动触发
@api.constrains()  # 约束验证
@api.returns()     # 返回模型链
@api.multi         # 方法处理多条记录(默认)

本技能基于 Odoo 19 开发规范,适配辉火云企业套件环境。

Related skills

Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.

by nssa.io1.0k installs47 stars

Read and write Excel workbooks, worksheets, ranges, tables, and charts in OneDrive through Microsoft Graph with managed OAuth.

by byungkyu800 installs42 stars

Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.

by johnpatternai21 installs8 stars

Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.

by Iván854 installs69 stars

Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.

by Iván555 installs18 stars

More from zhaobod1

Browse all skills

Generate enterprise Word and native PDF documents across 39 format presets, with 7 contract subtypes.

by zhaobod149 installs

Bootstrap a fresh OpenClaw workspace with a guided 4-step setup that writes five identity and preference files.

by zhaobod130 installs

Adds Claude Code-style agent experience to OpenClaw without modifying the host.

by zhaobod144 installs

麻省理工学院48小时学习法技能(青岛火一五信息科技有限公司)。完整还原 Ihtesham Ali 原始三问框架 + 反馈循环 + 完整 48h 三阶段时间线,叠加网上最佳实践(synthesis / contradictions / gaps / Feynman teach-back / weakness ana...

by zhaobod133 installs2 stars

通过火山方舟Ark API调用Seedance 2.0生成第一人称带货短视频,v2 新增剧本驱动的配音(edge-tts/火山TTS)、背景音乐自动混音、字幕烧录,内置 8 套人设模板(传统女、时尚主播、老中医、厨房主妇、美妆博主、健身教练、户外探店、数码博主)。触发词:生成视频、带货视频、产品视频、拍视频、剧本...

by zhaobod126 installs1 stars

规范 + 时尚的思维导图生成。输入 Markdown 大纲 / JSON / OPML / XMind,输出 XMind 2021+ (.xmind)、OPML、FreeMind (.mm)、Markdown、PNG、PDF、SVG;内置 13 种风格(modern / classic / dark / xiao...

by zhaobod136 installs