بنقرة واحدة
huo15-odoo19-module-dev
Odoo 19 模块开发技能 — 从项目结构、模型定义、视图设计到业务逻辑,完整覆盖 Odoo 19 模块开发全流程
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Odoo 19 模块开发技能 — 从项目结构、模型定义、视图设计到业务逻辑,完整覆盖 Odoo 19 模块开发全流程
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
【青岛火一五信息科技有限公司】企业级 Word 文档生成技能,支持两种模式:规则模式(默认)和模板模式。触发词:写word,写文档、生成word、生成文档、创建文档、.docx、Word文档、写合同、写方案、写报告、写会议纪要、按模板生成、导出PDF、Word转PDF、生成PDF。
辉火 Token 优化器 — 在不降低 OpenClaw 性能的前提下,系统化扫描、清理、监控工作区 token 消耗。安全模式:默认 dry-run,备份先行,AGENTS 永不自动替换。
用宝塔(BT-Panel)HTTP API 操控火一五的服务器——查系统状态/内存/磁盘/负载、管网站 (停站/启站/重载)、数据库、SSL 证书、计划任务、Docker,以及调用任意宝塔接口。 当用户说"看下 xx 服务器状态/负载/磁盘""重启/停掉某网站""宝塔面板""续证书" "列计划任务""管一下服务器""bt panel"等运维意图时使用。支持多面板。 凭据在 ~/.huo15/baota.json,脚本 scripts/bt_api.py。
Use this skill whenever the user wants to manage their work in the 火一五/辉火云 company Odoo system (www.huo15.com, db=huo15) — to-dos / tasks ("记个待办" "加任务" "我的待办"), projects, timesheets ("记工时"), CRM leads & opportunities ("新建商机" "跟进客户" "销售管道"), follow-up activities & reminders ("提醒我" "3天后回访"), calendar events & meetings ("安排会议" "我这周日程" "提前30分钟提醒"), knowledge-base articles ("查/写知识库" "产品手册"), or document files ("找文档" "上传文件"). Also a one-stop "我今天/本周要做什么" briefing. Triggers on odoo, 辉火云企业套件, 公司系统, /odoo/to-do /project /timesheets /crm /calendar /knowledge /documents /sale /purchase /inventory, or "登录公司系统/保存 odoo 账号". Treat bare work-context mentions of 待办/任务/项目/工时/客户/商机/会议/日程/提醒/知识库/文档/销售/采购/库存/订单/发货/收货 as this skill. First run: login.py init (地址/数据库/账号/密码 → ~/.huo15/tools.md). Pure standard library, zero dependencies.
火一五多智能体协同 - 基于 OpenClaw sessions_spawn 的多 Agent 并行工作系统。支持协调者模式、任务分配、结果汇总。触发词:多智能体协同、多 Agent、并行任务、协调者模式。
Use this skill when the user says "你好世界" / "你好世界初始化" / "初始化龙虾" / "龙虾初始化" / "初始化" / "bootstrap" / "hello world" / "onboarding" / "重新认识一下" / "重置我的偏好" / "更新画像", OR when the current working directory contains a BOOTSTRAP.md file. This skill guides users through a 4-step onboarding process to initialize their OpenClaw workspace by creating IDENTITY.md, USER.md, SOUL.md, TOOLS.md, and AGENTS.md files.
| name | huo15-odoo19-module-dev |
| version | 1.0.0 |
| slug | odoo19-module-dev |
| description | Odoo 19 模块开发技能 — 从项目结构、模型定义、视图设计到业务逻辑,完整覆盖 Odoo 19 模块开发全流程 |
| aliases | ["Odoo19模块开发","Odoo19开发","Odoo模块","odoo module","odoo19"] |
用户提到以下内容时激活本技能:
__manifest__.pymy_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,
}
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')] # 选项列表
# 创建
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)],
}
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 窗口动作 -->
<record id="action_my_model" model="ir.actions.act_window">
<field name="name">我的模型</field>
<field name="res_model">my.model</field>
<field name="view_mode">tree,form</field>
<field name="context">{'default_active': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
创建您的第一条记录
</p>
</field>
</record>
<!-- 菜单 -->
<menuitem id="menu_my_model"
name="我的模块"
action="action_my_model"
parent='menu_root' <!-- 父菜单 -->
sequence="10"/>
</odoo>
<tree>
<field name="sequence" widget="handle"/> <!-- 拖拽排序 -->
<field name="name"/>
<field name="partner_id"/>
<field name="date"/>
<field name="amount_total" sum="合计"/> <!-- sum/x editable 属性 -->
<field name="state" widget="badge" decoration-success="state=='done'"/>
<field name="active" invisible="1"/>
<!-- 按钮 -->
<button name="action_confirm" type="object" icon="fa-check" string="确认"/>
</tree>
<form>
<sheet>
<!-- 头部 -->
<div class="oe_title">
<h1>
<field name="name" placeholder="名称..."/>
</h1>
</div>
<group>
<group string="基本信息">
<field name="partner_id"/>
<field name="date"/>
<field name="state"/>
</group>
<group string="金额">
<field name="amount_total" readonly="1"/>
</group>
</group>
<!-- Notebook 分页 -->
<notebook>
<page string="明细行" name="lines">
<field name="line_ids">
<tree editable="bottom"> <!-- bottom/top -->
<field name="product_id"/>
<field name="qty" sum="数量"/>
<field name="price" sum="单价"/>
<field name="subtotal" sum="小计"/>
</tree>
</field>
</page>
<page string="备注" name="notes">
<field name="description"/>
</page>
</notebook>
</sheet>
</form>
<search>
<field name="name" string="名称" filter_domain="[('name','ilike',self)]"/>
<field name="partner_id" string="客户" operator='child_of'/>
<field name="date" string="日期"/>
<filter string="活跃" name="active" domain="[('active','=',True)]"/>
<filter string="已确认" name="confirmed" domain="[('state','=','confirm')]"/>
<separator/>
<filter string="本月" name="this_month"
domain="[('date','>=', (context_today() + relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
<group expand="0" string="分组">
<filter string="客户" name="partner" context="{'group_by': 'partner_id'}"/>
<filter string="状态" name="state" context="{'group_by': 'state'}"/>
<filter string="日期" name="date" context="{'group_by': 'date:month'}"/>
</group>
</search>
<kanban class="oe_kanban_small_column"
default_group_by="state"
records_draggable="1">
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card">
<div class="oe_kanban_card_header">
<strong><field name="name"/></strong>
</div>
<field name="partner_id"/>
<field name="amount_total"/>
<div class="oe_kanban_footer">
<field name="state" widget="badge"/>
</div>
</div>
</t>
</templates>
</kanban>
ir.model.access.csvid,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=拒绝
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('金额不能为负数')
<record id="action_mass_confirm" model="ir.actions.server">
<field name="name">批量确认</field>
<field name="model_id" ref="model_my_model"/>
<field name="state">code</field>
<field name="code">
records = env['my.model'].browse(context.get('active_ids'))
records.action_confirm()
</field>
</record>
<record id="action_mass_cancel" model="ir.actions.server">
<field name="name">批量取消</field>
<field name="model_id" ref="model_my_model"/>
<field name="state">code</field>
<field name="code">
records = env['my.model'].browse(context.get('active_ids'))
for rec in records.filtered(lambda r: r.state == 'draft'):
rec.action_cancel()
</field>
</record>
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)
<!-- security/ir.model.access.csv 同级 security/record_rules.xml -->
<record id="my_model_rule" model="ir.rule">
<field name="name">只能查看自己的记录</field>
<field name="model_id" ref="model_my_model"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field eval="[('create_uid', '=', user.id)]" name="domain_force"/>
<!-- perm_read/perm_write/perm_create/perm_unlink -->
</record>
# 进入 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
# 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,
}
# 升级模块(在 Odoo 界面或命令行)
# 界面:应用 → 搜索模块 → 升级
# 或通过命令行
docker exec -it odoo19 odoo-bin -u my_module -d huo15 --stop-after-init
# 查看日志
docker logs -f odoo19
| 问题 | 排查方法 |
|---|---|
| 模块不显示 | 检查 __manifest__.py 语法;确认在 addons 路径 |
| 视图报错 | 检查 XML 语法;查看 Odoo 日志定位标签 |
| 权限不足 | 检查 ir.model.access.csv 配置 |
| 计算字段不更新 | 确认 store=True;检查 @api.depends 依赖 |
| 按钮无响应 | 检查 type="object";确认方法签名正确 |
models/sale_order.pySTATE = [('draft','草稿'),...]_action_* 私有方法| 元素 | 规范 | 示例 |
|---|---|---|
| 模块目录 | 下划线 | my_module |
| 模型名 | x_<name>(避免冲突) | x_my_model |
| 字段名 | 下划线 | partner_id |
| 方法名 | 下划线 | _compute_amount |
| 视图ID | model_name_view_type | my_model_tree |
raise UserError 而不是 raise Exceptionunlink() 前检查状态Decimal,避免浮点精度问题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 开发规范,适配辉火云企业套件环境。