← 返回 Skills 市场
zhaobod1

Huo15 Odoo19 Module Dev

作者 Job Zhao · GitHub ↗ · v1.0.0 · MIT-0
cross-platform ✓ 安全检测通过
92
总下载
0
收藏
0
当前安装
1
版本数
在 OpenClaw 中安装
/install huo15-odoo19-module-dev
功能描述
Odoo 19 模块开发技能 — 从项目结构、模型定义、视图设计到业务逻辑,完整覆盖 Odoo 19 模块开发全流程
使用说明 (SKILL.md)

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
            )

关系字段对比

字段类型 语法 用途
Many2one fields.Many2one('other.model', ...) 多选一(外键)
One2many fields.One2many('other.model', 'rel_field', ...) 一对多
Many2many fields.Many2many('other.model', ...) 多对多
Reference fields.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

\x3C?xml version="1.0" encoding="utf-8"?>
\x3Codoo>
    \x3C!-- 窗口动作 -->
    \x3Crecord id="action_my_model" model="ir.actions.act_window">
        \x3Cfield name="name">我的模型\x3C/field>
        \x3Cfield name="res_model">my.model\x3C/field>
        \x3Cfield name="view_mode">tree,form\x3C/field>
        \x3Cfield name="context">{'default_active': True}\x3C/field>
        \x3Cfield name="help" type="html">
            \x3Cp class="o_view_nocontent_smiling_face">
                创建您的第一条记录
            \x3C/p>
        \x3C/field>
    \x3C/record>

    \x3C!-- 菜单 -->
    \x3Cmenuitem id="menu_my_model"
              name="我的模块"
              action="action_my_model"
              parent='menu_root'     \x3C!-- 父菜单 -->
              sequence="10"/>
\x3C/odoo>

Tree(列表)视图

\x3Ctree>
    \x3Cfield name="sequence" widget="handle"/>  \x3C!-- 拖拽排序 -->
    \x3Cfield name="name"/>
    \x3Cfield name="partner_id"/>
    \x3Cfield name="date"/>
    \x3Cfield name="amount_total" sum="合计"/>  \x3C!-- sum/x editable 属性 -->
    \x3Cfield name="state" widget="badge" decoration-success="state=='done'"/>
    \x3Cfield name="active" invisible="1"/>
    \x3C!-- 按钮 -->
    \x3Cbutton name="action_confirm" type="object" icon="fa-check" string="确认"/>
\x3C/tree>

Form(表单)视图

\x3Cform>
    \x3Csheet>
        \x3C!-- 头部 -->
        \x3Cdiv class="oe_title">
            \x3Ch1>
                \x3Cfield name="name" placeholder="名称..."/>
            \x3C/h1>
        \x3C/div>
        
        \x3Cgroup>
            \x3Cgroup string="基本信息">
                \x3Cfield name="partner_id"/>
                \x3Cfield name="date"/>
                \x3Cfield name="state"/>
            \x3C/group>
            \x3Cgroup string="金额">
                \x3Cfield name="amount_total" readonly="1"/>
            \x3C/group>
        \x3C/group>
        
        \x3C!-- Notebook 分页 -->
        \x3Cnotebook>
            \x3Cpage string="明细行" name="lines">
                \x3Cfield name="line_ids">
                    \x3Ctree editable="bottom">  \x3C!-- bottom/top -->
                        \x3Cfield name="product_id"/>
                        \x3Cfield name="qty" sum="数量"/>
                        \x3Cfield name="price" sum="单价"/>
                        \x3Cfield name="subtotal" sum="小计"/>
                    \x3C/tree>
                \x3C/field>
            \x3C/page>
            \x3Cpage string="备注" name="notes">
                \x3Cfield name="description"/>
            \x3C/page>
        \x3C/notebook>
    \x3C/sheet>
\x3C/form>

搜索视图

\x3Csearch>
    \x3Cfield name="name" string="名称" filter_domain="[('name','ilike',self)]"/>
    \x3Cfield name="partner_id" string="客户" operator='child_of'/>
    \x3Cfield name="date" string="日期"/>
    
    \x3Cfilter string="活跃" name="active" domain="[('active','=',True)]"/>
    \x3Cfilter string="已确认" name="confirmed" domain="[('state','=','confirm')]"/>
    
    \x3Cseparator/>
    \x3Cfilter string="本月" name="this_month"
            domain="[('date','>=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
    
    \x3Cgroup expand="0" string="分组">
        \x3Cfilter string="客户" name="partner" context="{'group_by': 'partner_id'}"/>
        \x3Cfilter string="状态" name="state" context="{'group_by': 'state'}"/>
        \x3Cfilter string="日期" name="date" context="{'group_by': 'date:month'}"/>
    \x3C/group>
\x3C/search>

看板视图(Kanban)

\x3Ckanban class="oe_kanban_small_column" 
        default_group_by="state"
        records_draggable="1">
    \x3Ctemplates>
        \x3Ct t-name="kanban-box">
            \x3Cdiv class="oe_kanban_card">
                \x3Cdiv class="oe_kanban_card_header">
                    \x3Cstrong>\x3Cfield name="name"/>\x3C/strong>
                \x3C/div>
                \x3Cfield name="partner_id"/>
                \x3Cfield name="amount_total"/>
                \x3Cdiv class="oe_kanban_footer">
                    \x3Cfield name="state" widget="badge"/>
                \x3C/div>
            \x3C/div>
        \x3C/t>
    \x3C/templates>
\x3C/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 \x3C 0:
                raise ValidationError('金额不能为负数')

服务端动作(Server Action)

\x3Crecord id="action_mass_confirm" model="ir.actions.server">
    \x3Cfield name="name">批量确认\x3C/field>
    \x3Cfield name="model_id" ref="model_my_model"/>
    \x3Cfield name="state">code\x3C/field>
    \x3Cfield name="code">
records = env['my.model'].browse(context.get('active_ids'))
records.action_confirm()
    \x3C/field>
\x3C/record>

\x3Crecord id="action_mass_cancel" model="ir.actions.server">
    \x3Cfield name="name">批量取消\x3C/field>
    \x3Cfield name="model_id" ref="model_my_model"/>
    \x3Cfield name="state">code\x3C/field>
    \x3Cfield name="code">
records = env['my.model'].browse(context.get('active_ids'))
for rec in records.filtered(lambda r: r.state == 'draft'):
    rec.action_cancel()
    \x3C/field>
\x3C/record>

自动动作(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', '\x3C', 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)

\x3C!-- security/ir.model.access.csv 同级 security/record_rules.xml -->
\x3Crecord id="my_model_rule" model="ir.rule">
    \x3Cfield name="name">只能查看自己的记录\x3C/field>
    \x3Cfield name="model_id" ref="model_my_model"/>
    \x3Cfield name="groups" eval="[(4, ref('base.group_user'))]"/>
    \x3Cfield eval="[('create_uid', '=', user.id)]" name="domain_force"/>
    \x3C!-- perm_read/perm_write/perm_create/perm_unlink -->
\x3C/record>

开发工作流

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_\x3Cname>(避免冲突) x_my_model
字段名 下划线 partner_id
方法名 下划线 _compute_amount
视图ID model_name_view_type my_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 开发规范,适配辉火云企业套件环境。

安全使用建议
Treat this as an incomplete low-confidence review: the clean VirusTotal result is reassuring, but the skill should be re-reviewed once metadata.json and artifact contents can be inspected.
能力评估
Purpose & Capability
Unable to verify the skill purpose or capabilities from metadata.json or artifact files because local shell inspection failed with a sandbox bwrap loopback error.
Instruction Scope
Unable to inspect SKILL.md instructions, so no instruction-scope concern is artifact-backed in this review.
Install Mechanism
Unable to inspect install specs or manifest files, so install behavior could not be assessed from artifacts.
Credentials
No environment access concern can be supported without artifact contents; VirusTotal telemetry alone is clean and not a basis for escalation.
Persistence & Privilege
Unable to verify persistence or privilege behavior from artifacts; no evidence-backed persistence concern was available.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install huo15-odoo19-module-dev
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /huo15-odoo19-module-dev 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v1.0.0
Initial release—this skill provides a comprehensive guide to Odoo 19 module development. - Covers standard project structure, including models, views, security, controllers, and static resources. - Includes example templates for `__manifest__.py` and basic model definitions with common field types and API usage. - Provides guidelines for view design with XML snippets for menus, actions, tree, and form views. - Details essential development concepts such as constraints, computed fields, SQL constraints, and model method overrides. - Contains reference tables for relationship fields and commonly used field parameters.
元数据
Slug huo15-odoo19-module-dev
版本 1.0.0
许可证 MIT-0
累计安装 0
当前安装数 0
历史版本数 1
常见问题

Huo15 Odoo19 Module Dev 是什么?

Odoo 19 模块开发技能 — 从项目结构、模型定义、视图设计到业务逻辑,完整覆盖 Odoo 19 模块开发全流程. 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 92 次。

如何安装 Huo15 Odoo19 Module Dev?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install huo15-odoo19-module-dev」即可一键安装,无需额外配置。

Huo15 Odoo19 Module Dev 是免费的吗?

是的,Huo15 Odoo19 Module Dev 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。

Huo15 Odoo19 Module Dev 支持哪些平台?

Huo15 Odoo19 Module Dev 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。

谁开发了 Huo15 Odoo19 Module Dev?

由 Job Zhao(@zhaobod1)开发并维护,当前版本 v1.0.0。

💬 留言讨论