关于 odoo-workflow
odoo-workflow 是一项指导开发者在修改前追踪 Odoo 代码与引用的技能。它使用按版本工作的辅助命令和基于引用的上下文摘要,帮助处理 Python、XML、JavaScript 与数据变更并减少错误。
主要功能
- 01发现版本与运行环境
- 02追踪模型、字段和方法
- 03搜索 XML、JavaScript 与引用
- 04编写基于引用的上下文摘要
- 05测试与迁移操作指引
适用场景
- 01添加变更前核实字段是否存在
- 02查找正确的方法重载位置
- 03检查继承视图的锚点
- 04迁移前追踪模块依赖
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ unclecatvn/ agent-skills --skill odoo-workflow安装后在 Claude Code 中输入/odoo-workflow确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/definition-of-done.mdscripts/odoo_trace.pytemplates/CLAUDE.md详细介绍
Odoo 工作流——先追踪,再编码
每次修改 Odoo 都要遵循此流程。代价高昂的故障总是那些老问题:不存在的字段或方法、从未存在过的 xpath 锚点、挂接到错误方法的重写、重命名存储字段却未迁移、无人能审计的 sudo(),以及运行后一个测试都没执行的测试套件。解决办法很简单:没有引用依据,就不写代码。
适用范围
- 完整流程——任何最终涉及 Odoo Python、XML、JS 或数据的工作:新增字段、重写、继承视图、OWL 补丁、向导、定时任务、控制器、报表、安全设置、迁移、错误修复、重构、“优化”、“清理”。粘贴的 traceback 也从这里开始:工作区模块中最内层的栈帧就是第一条引用依据。
- 简要流程——关于 Odoo 符号的问题(“X 定义在哪里”、“
sale.order有没有 F”、“19 中哪个模型会获得此字段”):执行步骤 0–2,然后用简报作答,不写代码。 - 快速流程——修改不涉及所编辑模块之外的符号,也不新增字段、重写、xmlid、xpath、组或
depends:例如模块自身代码中的标签、帮助文本或错别字,清单版本号更新,仅修改测试。简报只需一行,引用被编辑的代码行;跳过步骤 1、3 和 6;步骤 5 仍适用(字符串变更时要处理 i18n)。有任何疑问时,走完整流程。 - 仅涉及文档或 git 的任务可跳过(使用
odoo-commit)。 - 头脑风暴和规划类技能应在步骤 2 之后使用,且仅限业务意图仍不明确的情况——它们以上下文简报为输入。
步骤 0——运行环境、版本、参考包
内置辅助脚本 scripts/odoo_trace.py(Python 3.8+ 标准库,只读)用于解析运行环境并回答 Python 侧的追踪问题。每次调用时都要写出技能的完整基础目录——Shell 变量不会在工具调用之间保留。下文中的 odoo_trace 表示 python3 <skill_dir>/scripts/odoo_trace.py。
odoo_trace env
按以下顺序查找,命中即停止:命令行标志、.claude/odoo.json、.claude/launch.json 中 addons 覆盖当前工作目录的 odoo-bin 配置(这两个文件位于当前工作目录或最近的父目录中),然后是 addons_path 覆盖当前工作目录的唯一 *.conf 文件;从父目录向上搜索(每一级及其子目录)。脚本会输出 version(来自 odoo/release.py)、odoo_root、conf、python、dev_db,以及 Odoo 使用顺序中的 addons 根目录(第一个包含模块名称的根目录优先)。.claude/odoo.json 中的 odoo_version 或 .odoo-version 文件中的版本必须与 release.py 一致。
- 退出码为 2 时,会输出
SETUP ERROR:和原因。如果未找到运行环境:询问一次核心代码库的检出目录和 conf,然后在每次调用时(包括roots)将它们作为odoo_trace --odoo-root <core> --conf <conf> <command>传入,并提议写入被 git 忽略的.claude/odoo.json,包含odoo_version、odoo_root、conf、python、dev_db(本机路径)。版本来源不一致,或有多个 conf 覆盖当前工作目录:询问哪个才是正确的。绝不能只针对自定义根目录进行追踪。 - 用户或项目的
CLAUDE.md/AGENTS.md中的明确说明优先于辅助脚本。若两者不一致,停止并询问。不要默默假定版本。 - 使用 skills CLI 加载对应主版本的 Odoo 参考包
odoo-<major>;在 Claude Code 插件中则加载<plugin>:odoo-<major>-0(例如agent-skills:odoo-18-0)。阅读其中的references/api-highlights.md(此版本中已移除或重命名的 API),并且只阅读任务所需的指南章节(执行grep -n '^## ' <guide>,然后按范围读取)。如果缺少参考包,说明这一点并继续——真实源码才是权威,不能依赖记忆。
步骤 1 - 追踪调用链
Python 符号:辅助工具。 它使用 ast 解析类,应用该版本的 _name
规则,追踪 _inherit 父类(如 mail.thread、portal.mixin 等混入类)、_inherits
委托、base 扩展和核心 BaseModel,标记模块从未导入的文件中的类(UNIMPORTED),
并打印绝对路径 file:line。
| 问题 | 命令 |
|---|---|
定义或扩展 MODEL 的类、其混入类、类型、_auto、_log_access | odoo_trace model MODEL |
MODEL 上是否存在 FIELD(自身、父类、_inherits、魔法字段),每个定义及 comodel;会逐层检查点分隔的路径 | odoo_trace field MODEL FIELD[.FIELD...] |
METHOD 的每个定义,按 super() 顺序排列(#1 最先运行)、super() 调用、钩子、AMBIGUOUS 配对 | odoo_trace method MODEL METHOD --module MODULE |
MODULE 按加载顺序排列的传递依赖 depends | odoo_trace depends MODULE |
| xmlid 是否存在(已加载的 XML 记录或 CSV 行,或安装时生成) | odoo_trace xmlid module.name |
MODULE 中从未加载的文件(不在 data/assets 中,也未被导入) | odoo_trace unlisted MODULE |
最后一行是 VERDICT。退出码 0 = 找到,1 = NOT FOUND(结论会列出最接近的真实符号),2 = 设置错误或答案未知。只有退出码 1 才能支持 NOT FOUND 单元格。对于枢纽模型(sale.order、mail.thread)的列表会被截断;在 model、method 或 unlisted 后加上 --all 可打印完整列表。
XML、JS、控制器、使用情况:在相同根目录中 grep。 下文的 ROOTS 表示完整展开后的
$(odoo_trace roots):在 zsh 和 bash 中,命令替换会拆分成每个根目录一个参数;普通的 $ROOTS 字符串在 zsh 中不会拆分。--include glob 外的引号要保留。若 grep 警告 No such file or directory 或退出码为 2,说明命令有误,不能判定为 NOT FOUND。在 odoo_trace env 退出码为 2 时运行 grep 也一样有误(空的 $(odoo_trace roots) 会使 grep -r 只搜索当前工作目录);在路径包含空格的根目录中运行 grep 也是如此:路径会被拆分,macOS grep 会静默跳过这些片段,因此要对该根目录单独运行 grep,并将路径加引号。
# V1. Views on MODEL
grep -rlE --include='*.xml' "<field name=\"model\">MODEL</field>" ROOTS
# V2. Every view and QWeb template inheriting VIEW; -A1 also catches inherit_id split over two lines
grep -rnE -A1 --include='*.xml' "name=['\"]inherit_id['\"]|inherit_id=['\"]" ROOTS | grep -E ":[0-9]+:.*(ref|inherit_id)=['\"](MODULE\.)?VIEW['\"]|-[0-9]+-\s*ref=['\"](MODULE\.)?VIEW['\"]"
# V3. ANCHOR nodes inside one view record, absolute lines. A = the id line, B = the next </record>: grep -nE 'id="VIEW"|</record>' VIEW_FILE
awk -v a=A -v b=B 'NR>=a && NR<=b && /name="ANCHOR"/ {print FILENAME":"NR": "$0}' VIEW_FILE
# W1. OWL / QWeb client template: definition and every t-inherit (extension or primary)
grep -rnE --include='*.xml' "t-name=['\"]TEMPLATE['\"]|t-inherit=['\"]TEMPLATE['\"]" ROOTS
# W2. JS class or component: its definition (= import path) and every patch of it
grep -rnE --include='*.js' "^export (default )?(class|function|const) NAME[^A-Za-z0-9_]|patch\(\s*NAME[^A-Za-z0-9_]" ROOTS
# C1. Route (the second pattern finds a decorator split over lines), then every subclass of
# its controller class (an `import CLASS as ALIAS` hit: run the class grep again with ALIAS)
grep -rnE --include='*.py' "@(http\.)?route\(.*['\"]URL['\"]|^\s*(route=)?\[?\s*['\"]URL['\"]" ROOTS
grep -rnE --include='*.py' "^class \w+\(([^)]*[ ,])?([A-Za-z_.]+\.)?CLASS[,)]|import.*[ ,]CLASS as " ROOTS
# S1. ACL rows and record rules on MODEL (dots as underscores)
grep -rn --include='ir.model.access.csv' "model_MODEL_UNDERSCORED," ROOTS
grep -rlE --include='*.xml' "model_MODEL_UNDERSCORED['\"]" ROOTS | grep security
# U1. Every use of NAME before renaming, removing, or changing the signature of a field or method
grep -rnwE --include='*.py' --include='*.xml' --include='*.js' --include='*.csv' --include='*.scss' "NAME" ROOTS
# T1. Tests to mirror
ls BASE_MODULE/tests/; grep -rnE "def test_.*METHOD" MODULE/tests
对于 U1,保留命中 MODEL 的结果并逐一列出:视图、QWeb、@api.depends、related=、
域、记录规则、定时任务和服务器操作 code、<function>、按钮 name=、JS
orm.call 字符串、测试、迁移。对于通用名称(name、state、partner_id),将列表限制为 odoo_trace model MODEL 打印的模块以及 V1/V2 中的视图;如果仍超过 50 个命中,按模块显示数量,并在继续前询问。
只有当命中文件会被加载(odoo_trace unlisted MODULE 会列出从未加载的文件),且在 Python 中位于 MODEL 自身的类内、而非同一文件的其他类中时,grep 命中才计数。
阅读预算——这里会消耗 token:
- 阅读代码范围,而非整个文件:使用 offset/limit 阅读,或使用
awk 'NR>=A && NR<=B'——切勿使用sed -n 'A,Bp' | grep -n,因为其行号会从 1 重新开始,无法引用。每一跳最多 120 行。 - 行内最多 3 跳。更深层或跨模块的调用流程交给
odoo-code-tracerAgent,并提供odoo_version、pack_dir(已加载包的目录)、roots和helper(辅助工具的完整路径,以及env所需的任何标志),前提是该 Agent 已安装;否则继续使用上述命令。 - 切勿重新阅读已在 Brief 中引用过的文件。
步骤 2 - 上下文摘要(无引用、无代码)
在任何代码之前编写摘要;每个受更改影响的符号占一行(以 Odoo 18 为例):
| 层 | 符号 | 来源(文件:行号) | 基础行为 | 变更 |
|---|---|---|---|---|
| 模型 | purchase.order | addons/purchase/models/purchase_order.py:19 | _name,继承 mail.thread | 继承 |
| 方法+super | button_approve | purchase_order.py:528;purchase_stock/models/purchase_order.py:117 中的重写会调用 _create_picking | 仅当安装了 purchase_stock 时才会创建拣货单 | 依赖 purchase_stock,调用 super,然后执行 X |
| 路径 | stock.picking partner_id.commercial_partner_id.vat | stock/models/stock_picking.py:630 -> base/models/res_partner.py:304 -> :247 | Many2one、Many2one、Char | 相关字段 |
| 视图+xpath | purchase.purchase_order_form | purchase/views/purchase_views.xml:130,锚点 partner_ref :185,表单中只有一个节点 | - | 在锚点后添加字段 |
层:模型 / 字段 / 方法+super / 路径 / 视图+xpath / 客户端 / 控制器 / 安全 / 公司 / 数据 / 迁移 / i18n / 测试。
来源单元格必须恰好符合以下一种情况:
file:line- 所引行本身能证明该声明(定义、记录、锚点节点)。NOT FOUND- 辅助程序以状态码 1 退出,或对所有 ROOTS 在 MODEL 上进行干净的 grep 后未找到任何匹配项。 若存在最接近的实际符号,请注明。UNCERTAIN: <缺少的内容>- 指明未读取的文件、运行时构建的域,或提供该锚点的其他模块视图。对于上方命令能够回答的问题,绝不使用此项。AMBIGUOUS: <modules>- 覆盖项位于彼此不依赖的模块中。
凡是不符合 file:line 的单元格,均表示该行没有可用代码:删除该行、添加缺少的 depends,或提问。超过 15 行意味着任务应拆分;绝不能为了凑数而删除一行。
规则:
- 重新定义的字段。
odoo_trace field会按加载顺序列出每个定义;请为变更所依赖属性的每个定义提供引用。未重复定义的属性保留较早的值(selection_add会扩展列表),除非较新的类没有继承较早的类:此时较早的属性会被丢弃,助手会打印final class。 - 路径(
related=、@api.depends、depends=、domain=、domain_force)。对点分路径上的每一跳调用一次field,每一跳提供一个引用。某一跳不存在,或路径末尾之前出现非关系字段,则该跳为 NOT FOUND。 - 依赖项。 仅在
odoo_trace depends MODULE外部模块中定义的符号,即使存在于当前数据库中,也无法保证已安装。请添加依赖项或删除该行。 - 重载顺序。 请通过
odoo_trace method ... --module MODULE获取,切勿凭记忆或 grep 顺序判断:最后加载的类先运行,其super()会调用较早的类。请将变更所依赖其重载的每个模块声明为depends。标记为 AMBIGUOUS 的模块对仍为 AMBIGUOUS。 - Xmlid。 以
odoo_trace xmlid的结果为准:在清单加载的 XML 或 CSV 文件中的记录,或安装时生成的记录(model_*、field_*__*、selection__*、base.module_*、图表模板中的account.<company id>_<name>),均归属于助手指定的模块。仅存在于清单从未加载的文件中的记录为 NOT FOUND(若初始化钩子可能会加载,则为 UNKNOWN)。切勿臆造 xmlid,也绝不要将生成的 xmlid 报告为缺失。视图groups=中缺失的组不会阻止安装:Odoo 只会记录 WARNING,并隐藏该节点,因为没有用户属于不存在的组。 - 魔法字段。 切勿想当然;以
odoo_trace field的结果为准。只有_log_access为 true 时,才存在create_uid、create_date、write_uid、write_date;其默认值取决于_auto,因此,报表和 SQL 视图模型(_auto = False)没有这些字段。 - Xpath 锚点。 锚点必须位于基视图中,或位于
depends中某个模块的继承视图中;否则为 UNCERTAIN。定位器会匹配第一个节点,因此请说明组合视图(V2 + V3)中有多少个节点带有该名称。 - 公司。 如果 MODEL 或某个关联模型有
company_id,请提供引用,并说明该模型是否设置了_check_company_auto。指向受公司约束的关联模型的新关系字段应设置check_company=True。对于带有company_id的新模型,应像核心代码那样添加记录规则:[('company_id', 'in', company_ids)];如果公司为可选,则使用[('company_id', 'in', company_ids + [False])]。 - 架构变更。 重命名、删除或更改存储字段类型,更改或删除 selection 键,添加
required,向已有数据的表添加存储型计算字段,或重命名模型,都需要一行migration:填写新的清单版本,以及迁移数据的migrations/<version>/pre-*.py/post-*.py脚本(def migrate(cr, version));仅当脚本版本高于已安装版本且不高于清单版本时,脚本才会运行。没有迁移脚本时,重命名的存储字段会在-u时丢失其列和数据,且不会发出提示。只有用户确认该模块从未安装在重要数据库中时,才可跳过。相关方案见该工具包的迁移指南。 - sudo。 每个新增的
sudo()或with_user(SUPERUSER_ID)都需要一行security:说明其绕过了哪项 ACL 或记录规则(S1),以及调用前进行的检查(18+ 版本上的check_access/has_access、访问令牌或组检查)。没有该行,就不要使用 sudo。 - 优先复用,避免臆造:添加字段、辅助方法或计算字段之前,先查找是否已有可用项。
- “代码整洁”/“遵循基础实现”意味着:遵循基础实现的模式,只做最小改动,绝不重写
super()已经完成的工作。“优化”若指速度,则意味着:先测量(针对明确用例记录查询次数或耗时),修改后再次测量。
第 3 步 - 最简计划
最多 10 条:要修改的文件、明确不修改的文件,以及延期事项。除非 Brief 证明改动涉及两个模块,否则只涉及一个模块;每个涉及的 git 仓库各创建一个提交和 PR。遵循 YAGNI:不会变化的值不添加配置,只使用一次的内容不编写辅助函数,只有一种实现时不添加抽象。
第 4 步 - 实施
- 遵循该 pack 的
Coding Conventions和此版本的api-highlights.md列表;现有的 stable-addon 风格优先于个人偏好。 - 在改动需要时,重写实际运行的最窄钩子(钩子
odoo_trace method会打印),而不是包装它的公共方法。调用super()并只添加最小改动;绝不复制基类方法体。 - 使用
_inherit = 'model'(字符串)扩展模型;新模型需显式设置_name。不带_name的列表形式_inherit会创建一个以类名命名的新模型:在 19 中始终如此(并会警告),在 18 中则在列表包含两项时如此。 - 每个新增的 XML 或 CSV 文件都要按加载顺序加入清单的
data(groups、ir.model.access.csv、视图、菜单);每个新增的静态文件都必须对应正确assets包中的条目。未列入清单的文件能够正常安装,但不会生效。 sudo()命名:将通过sudo()获取的任何记录集绑定到一个以_sudo结尾的名称(partner_sudo = self.partner_id.sudo(),orders_sudo = self.env['sale.order'].sudo().search(domain))。单次调用(record.sudo().write(vals))无需变量。绝不从公共方法返回_sudo记录集,也不将其保留在self上。- Odoo 代码中不得出现
ponytail:或其他“暂且简化”标记——代码要么正确,要么第 3 步的计划中要说明延期事项。 - 新增的面向用户的字符串必须通过
_()/_t()/string=/help=编写,以便提取。
第 5 步 - 完成标准
报告完成前,请阅读 references/definition-of-done.md:其中列出了各版本的准确命令。简而言之:
- 整个检查清单只使用一个临时数据库
scratch_<module>:使用该模块的测试进行安装,测试摘要中计数 N > 0(没有摘要或显示of 0 tests均不算通过),不得有ERROR/CRITICAL,并说明每条与该模块相关的WARNING。除非用户要求,否则绝不对命名数据库(conf的db_name、staging、dev)执行-i/-u。 - 如果 Brief 中有
migration行,则在带有先前版本的数据库副本上测试升级。 -
odoo_trace unlisted <module>报告所有文件均已加载。 - 每个新增模型都要有 ACL 行;适用 Company rule 时要有公司规则;每个
sudo()都要有对应的security行。 - 字符串有变更时重新生成 i18n;随项目发布的
.po文件保持原文件名。 - 如果项目有自己的 linter,则该 linter 对已修改文件的检查必须通过。
- 所有涉及的 git 仓库均通过 Hygiene 检查。
- 删除临时数据库及其 filestore。通过
odoo-commit提交([TAG] module: description,不注明 AI 署名);每个仓库各创建一个提交和 PR。
第 6 步 - 交接
如果已安装,运行 odoo-code-review agent,并传入 odoo_version、pack_dir、roots 和 helper。如果未安装,则在报告完成前自行检查 references/definition-of-done.md 中的 Security 和 Hygiene 列表。