フィールドを起点にして、CICDでバージョンを跨いだモジュールテスト。
Odooのバージョンアップに追随しないモジュールをテストであぶり出す。
---
config:
layout: fixed
---
flowchart TD
subgraph S0["入力"]
PF["portal_fields<br>(対象フィールド一覧)"]
REPO["コードリポジトリ<br>(addons/*)"]
DB["PostgreSQL<br>Odooメタ (ir_ui_view 等)"]
LOGS["任意: ランタイムログ"]
end
subgraph S1["静的解析(コード)"]
RG@{ label: "ripgrep 粗取り<br>self.field / write({'field': ...})" }
SG["Semgrep / AST 精度向上<br>py_read / py_write / py_depends / py_domain"]
N1["結果正規化<br>module / file / line / snippet 分類"]
end
subgraph S2["DBメタ解析(XML / QWeb / サーバーアクション)"]
XPATH["XPath 抽出<br>//form//field | //tree//field"]
ATTRS["attrs 抽出<br>readonly / invisible / required ドメイン"]
QWEB["QWeb 参照<br>t-field / t-esc 等"]
SAS["サーバーアクション<br>ir_actions_server.code 検索"]
N2["結果正規化<br>usage_kind = xml_field / xml_attrs / qweb / server_action"]
end
subgraph S3["任意:動的計測"]
RT1["write/create フック<br>vals のフィールド記録"]
RT2["@api.depends / onchange ログ"]
RT3["ドメイン評価ログ"]
N3["結果正規化<br>usage_kind = runtime_*"]
end
subgraph S4["統合 & UPSERT"]
MERGE["統合・重複排除<br>(model, field, usage_kind, file, line)"]
UPSERT["UPSERT -> portal_field_usage"]
end
subgraph S5["可視化 / 運用"]
UI["ポータル画面<br>Where-Used 一覧 / リンク"]
Q1["影響範囲集計<br>usage_kind 別件数"]
Q2["削除可否判定<br>使用 0 件 -> 候補"]
TASK["改善 / 移植タスク化<br>コード生成 / リファクタ"]
end
PF --> ORCH["ジョブ実行器<br>(CI/手動: 対象モデル/フィールドを取得)"]
REPO --> ORCH
DB --> ORCH
LOGS --> ORCH
ORCH --> RG & XPATH & QWEB & SAS & RT1 & RT2 & RT3
RG --> SG
SG --> N1
XPATH --> ATTRS
ATTRS --> N2
QWEB --> N2
SAS --> N2
RT1 --> N3
RT2 --> N3
RT3 --> N3
N1 --> MERGE
N2 --> MERGE
N3 --> MERGE
MERGE --> UPSERT
UPSERT --> UI & Q1
Q1 --> Q2
Q2 --> TASK
SCHED["スケジュール<br>(CI: push / 夜間; 手動トリガ)"] --> ORCH
RG@{ shape: rect}
- Pythonで書いたカスタマイズ(モデル・メソッド・計算ロジック等)は基本「コードはコードとして」モジュールのファイルシステム上(Git等)で管理されます。
- DB側の「メタ情報」は、**その結果としての構造や参照(モデル/フィールドの宣言、ビューXML、アクション、メニュー、権限、外部IDなど)**を保持します。
- 例外的に、サーバーアクション等の“実行時Python断片”はDBに保存されます(=コード断片がメタに入るパターン)。
以下、要素別に「どこに何が入るか」を整理します。
どこに保存される?(ざっくり対応表)
| 要素 | 典型的な保存場所 | 備考 |
|---|---|---|
Pythonモデル/メソッド(models.Model サブクラス、create/write/onchange/constraints、ビジネスロジック、ウィザード) | ファイルシステム:addons/<module>/models/*.py | DBにはコード本体は保存されません。起動時にレジストリへロードされ、DBのir_model/ir_model_fieldsへ宣言結果が同期されます(stateがbase)。 |
| フィールド定義(型・relation・必須・翻訳可・selection等) | DBのメタ:ir_model / ir_model_fields / ir_model_fields_selection | コード定義はstate='base'、Studio作成はstate='manual'で区別されるのが一般的。計算フィールドの関数本体はDBに持ちません(Studioの“式”を除く)。 |
| ビュー(フォーム/ツリー/検索/QWeb) | DB:ir_ui_view.arch_db(XML、近年は多言語JSONBの場合あり) | コード側XML(views/*.xml)もインストール時にDBへ取り込まれ、以後はDBが一次情報になります。 |
| アクション/メニュー | DB:ir_actions_* / ir_ui_menu | Pythonから定義しても、インストールでDBにレコード化。 |
| 権限/レコードルール | DB:ir_model_access / ir_rule | モジュール配布XML(security/*.xml)→インストールでDB化。 |
| 外部ID(XMLID) | DB:ir_model_data | 「コード⇔DB」を結び、アップグレード時の差分適用のカナメ。 |
| サーバーアクション(Pythonコード) | DB:ir_actions_server.code | ここだけ実コードがDBに保存され、safe_evalで実行。自動化(ir_cron)から呼ぶことも多い。 |
| 自動化(Automated Actions) | DB:base.automation | ドメイン/トリガ/(場合により)Python式がDBに保存。 |
| Studioカスタム(フィールド/ビュー/自動化) | DB(上記メタ各所) | 「Studio生成モジュール」にエクスポートも可能だが、基本はDBメタ。 |
「データ処理」はどこに?
- **通常のデータ処理(ビジネスロジック)**は Pythonコード側:
create()/write()/unlink()のオーバーライド- 計算フィールド:
compute/inverse/@api.depends @api.constrains/@api.onchange- トランザクション内の手続き(在庫転送、請求生成など)
- DBメタには関数本体は入りません。入るのは“そのフィールドが計算か否か/storeか否か”等の記述的属性のみ(版によって
ir_model_fieldsにcompute名やrelated文字列を持つことはありますが、実行コードはDBに保存されないのが原則)。
「コードがメタ情報に格納される」例外
- サーバーアクション(Python):
ir_actions_server.codeにテキストとして保存。- 手軽ですが、テスト性/レビュー/移植の観点で大規模ロジックには不向き。
- モジュール化してPythonに移すのがベター。
- Studioの計算フィールド/自動化:StudioのUIで書いた式(Python/ドメイン)はDBに式として保存されます。
- こちらも成長したらモジュールのPythonへ移植推奨。
運用ベストプラクティス(実務指針)
- ビジネスロジックはGit管理のPythonモジュールに集約
- レビュー、テスト、デプロイ手順を標準化できます。
- DBメタは“結果のスナップショットと参照”
ir_model_*/ir_ui_view/ir_actions_*/ir_model_dataは、“何が定義されたか”の台帳。
- Studioやサーバーアクションはプロトタイプ/軽量用途
- 長期運用するならPythonコード化してGit管理へ。
- 移植性の確保
- 重要なビュー/アクション/権限はモジュールXMLで定義し、DBは“適用先”にする。
- XMLID(
ir_model_data)を必ず付与してアップグレード耐性を確保。
参考:コード由来かStudio由来かを見分けるクエリ例
実行環境に応じて列名差異はありますが、ざっくりの見方です。
-- フィールドの由来(コード: base / Studio: manual)
SELECT model, name AS field_name, ttype, state
FROM ir_model_fields
WHERE model = 'sale.order'
ORDER BY name;
-- サーバーアクションに埋め込まれたPythonコード
SELECT id, name, model_id, state, code
FROM ir_actions_server
WHERE state = 'code';
-- ビューXML(Studio含む)は ir_ui_view.arch_db に保存
SELECT id, model, type, key, arch_db
FROM ir_ui_view
WHERE model = 'sale.order';
まとめ
- PythonカスタマイズのコードはDBの“メタ情報”には基本入りません。
- DBは定義結果(モデル/フィールド/ビュー/アクション等)を記録する台帳、コードはモジュールでGit管理、という二層構造がOdooの標準的なアーキテクチャです。
- 例外として、サーバーアクションやStudioの式のような“小さなコード片”だけがDBメタに入ります。
あなた:
追加されたフィールドがどのコードで使われたかを記録する良い方法はありますか。
ChatGPT:
思考時間: 33s
結論:「コードはコードで管理」が基本なので、
「追加フィールドがどのコードで使われたか」は (1)静的解析+(2)DBメタ解析+(3)任意で動的計測 の三段構えにすると実務で回ります。
下に “すぐ導入できる最小構成” と “発展構成” を両方、テーブル箱(portal_fields)連携前提でまとめます。
1) 結果を書き留める箱(推奨DDL)
-- どこで使われたかの索引
CREATE TABLE IF NOT EXISTS portal_field_usage (
id BIGSERIAL PRIMARY KEY,
model TEXT NOT NULL,
field_name TEXT NOT NULL,
usage_kind TEXT NOT NULL, -- 'py_read','py_write','py_depends','py_domain','xml_field','xml_attrs','xml_domain','qweb','report','server_action'
module TEXT, -- 推定モジュール(パスから)
file_path TEXT, -- リポジトリ相対パス
line_start INT,
line_end INT,
snippet TEXT,
commit_sha TEXT, -- CIで埋めると便利
detected_at TIMESTAMP NOT NULL DEFAULT now(),
UNIQUE (model, field_name, usage_kind, file_path, line_start, commit_sha)
);
CREATE INDEX IF NOT EXISTS idx_pfu_field ON portal_field_usage (model, field_name);
CREATE INDEX IF NOT EXISTS idx_pfu_kind ON portal_field_usage (usage_kind);
この箱に、「どのファイル・何行目で・どう使われたか」を貯めます。
portal_fields(あなたの“箱”)のmodel + field_nameと一致させる運用です。
2) 静的解析(コード側)
2.1 まずは ripgrep で粗取り(CIでもローカルでもOK)
新規フィールド群(portal_fields から抽出)に対して、以下の典型パターンを拾います。
- 参照(読み):
self.field,record.field,mapped('field'),filtered(lambda r: r.field ...) - 更新(書き):
write({'field': ...}),create({'field': ...}),vals.get('field') - 依存:
@api.depends('field')/@api.onchange('field') - ドメイン:
[('field', '=', ...)](Pythonのドメイン構築部) - XML/QWeb は後述(DBメタ解析)
例(Bash; 変数 $FIELD を回す想定):
# 例: addons 直下の Python を対象
RG_PATHS="addons/**/{models,controllers,report,wizard}/**/*.py"
# 読み(attributeアクセス)
rg -n --line-number -g "$RG_PATHS" -e "\b(self|record|rec|line)s?\.$FIELD\b"
# 書き(ORM vals/書き込み)
rg -n --line-number -g "$RG_PATHS" -e "\b(write|create)\s*\(\s*{[^}]*'$FIELD'\s*:"
# 依存デコレータ
rg -n --line-number -g "$RG_PATHS" -e "@api\.(depends|onchange)\([^)]*'$FIELD'"
# ドメイン(Pythonリテラル)
rg -n --line-number -g "$RG_PATHS" -e "\(\s*'$FIELD'\s*,\s*'[=!<>ilke~]+'"
粗取り結果は 一度 TSV/CSV に落とし、あとで整形(usage_kind の振り分け・
module推定)します。
誤検知を下げるには、後述の AST 解析 or Semgrep を追加。
2.2 (任意)Semgrep/AST で精度を上げる
- Semgrep なら Odoo 向けルールを数個書くだけで、「
writeの vals に含まれるfield」等を綺麗に取れます。 - Python AST なら
ast.walk()でAttribute(Name('self'),'field')やCall(Name('write'), kwargs...)を機械的に分類。
ここで得たヒットを
portal_field_usage(usage_kind='py_*')に UPSERT していきます。
3) DBメタ解析(XML/QWeb/サーバーアクション)
OdooはビューXMLがDBに格納されるため、PostgreSQLだけでビュー参照を洗い出せます。
すでに作られた XPath 抽出の延長で、**「どのビューで <field name=’…’> が使われたか」**をレコード化します。
3.1 フォーム/ツリーでのフィールド参照
WITH views AS (
SELECT
v.id AS view_id,
v.model,
CASE
WHEN pg_typeof(v.arch_db)::text='jsonb' THEN
COALESCE(v.arch_db->>'ja_JP', v.arch_db->>'ja', v.arch_db->>'en_US',
(SELECT value FROM jsonb_each_text(v.arch_db) LIMIT 1))
ELSE v.arch_db::text
END AS arch_text
FROM ir_ui_view v
WHERE v.model IS NOT NULL
),
xml AS (
SELECT view_id, model,
CASE WHEN arch_text LIKE '%<%' THEN xmlparse(document regexp_replace(arch_text, '^[\uFEFF\s]*', '')) END AS x
FROM views
WHERE arch_text IS NOT NULL
),
nodes AS (
SELECT
x.model,
x.view_id,
(xpath('string(./@name)', n))[1]::text AS field_name,
-- attrs にドメイン制御があれば拾う
NULLIF((xpath('string(./@attrs)', n))[1]::text,'') AS attrs_raw
FROM xml x
CROSS JOIN LATERAL unnest(xpath('//form//field[@name] | //tree//field[@name]', x.x)) AS n
),
attrs AS (
SELECT
model, view_id, field_name,
CASE WHEN attrs_raw ~* '(?is)\binvisible\s*:\s*\['
THEN regexp_replace(attrs_raw, '(?is).*?\binvisible\s*:\s*(\[[^\]]*\]).*', '\1') END AS inv_domain,
CASE WHEN attrs_raw ~* '(?is)\breadonly\s*:\s*\['
THEN regexp_replace(attrs_raw, '(?is).*?\breadonly\s*:\s*(\[[^\]]*\]).*', '\1') END AS ro_domain,
CASE WHEN attrs_raw ~* '(?is)\brequired\s*:\s*\['
THEN regexp_replace(attrs_raw, '(?is).*?\brequired\s*:\s*(\[[^\]]*\]).*', '\1') END AS req_domain
FROM nodes
)
SELECT
n.model, n.field_name,
'xml_field'::text AS usage_kind,
n.view_id::text AS file_path, -- 便宜的に view_id を格納(後でXMLID解決してもOK)
NULL::int AS line_start,
NULL::int AS line_end,
NULL::text AS snippet
FROM nodes n
UNION ALL
SELECT
a.model, a.field_name,
'xml_attrs'::text AS usage_kind,
a.view_id::text, NULL::int, NULL::int,
NULLIF(a.inv_domain,'') || NULLIF(a.ro_domain,'') || NULLIF(a.req_domain,'') AS snippet
FROM attrs a
WHERE a.inv_domain IS NOT NULL OR a.ro_domain IS NOT NULL OR a.req_domain IS NOT NULL;
これを
INSERT INTO portal_field_usage (...) SELECT ...にすればビューでの参照が蓄積されます。
XMLID を取りたい場合は、ir_model_dataでmodel='ir.ui.view' AND res_id=view_idを JOIN してください。
3.2 QWeb テンプレート(帳票/ポータル)
QWeb は ir.ui.view の type='qweb' か、key に module.template_name が付いたものです。
同様に XML を xpath('//t[@t-field] | //@t-esc | //@t-raw …') 等で拾い、'.' + field_name を含むものを usage_kind='qweb' として保存します。
3.3 サーバーアクション(DBに生コード)
SELECT
imd.module,
'server_action'::text AS usage_kind,
'ir_actions_server:' || s.id AS file_path,
NULL::int AS line_start,
NULL::int AS line_end,
s.code AS snippet
FROM ir_actions_server s
LEFT JOIN ir_model_data imd
ON imd.model='ir.actions.server' AND imd.res_id=s.id
WHERE s.state='code' AND s.code ILIKE '%.<YOUR_FIELD>%';
ここは DB内にコード文字列があるので、簡易
ILIKEで十分粗取りできます。
4) (任意)動的計測(本番/検証環境)
- テスト・ステージングで 実際のアクセス を記録したい場合、次のいずれか:
BaseModel.write/createの オーバーライドでvals中のフィールド名をログに吐く@api.dependsログ出力(self._name,field,stack)- ドメイン評価の入口(
odoo.osv.expression)でフィールド名を拾ってログ
- いずれも性能とリスクがあるので、限定環境で使い、出力を
portal_field_usage(usage_kind='runtime_*')に集約するのが現実的です。
5) 運用フロー(最小でOKな回し方)
- フィールド一覧:
portal_fieldsがソース。 - 静的解析(rg/semgrep/AST)→ CSV に吐き出し →
portal_field_usage(py_*)にUPSERT。 - DBメタ解析(上のSQL)→
portal_field_usage(xml_*)にUPSERT。 - (任意)動的解析→
portal_field_usage(runtime_*)にUPSERT。 - ポータル表示:
SELECT usage_kind, count(*) FROM portal_field_usage WHERE model='sale.order' AND field_name='...' GROUP BY 1- 「どのファイルで使われるか」のリンクリストを画面に。
参考:portal_field_usage への投入(例:ビュー参照)
INSERT INTO portal_field_usage (model, field_name, usage_kind, module, file_path, line_start, line_end, snippet)
SELECT
n.model,
n.field_name,
'xml_field',
imd.module,
COALESCE(imd.module,'') || ':' || COALESCE(imd.name,'view_'||n.view_id)::text AS file_path,
NULL, NULL, NULL
FROM (
-- さきほどの nodes CTE をここへ貼る
) n
LEFT JOIN ir_model_data imd
ON imd.model='ir.ui.view' AND imd.res_id = n.view_id
ON CONFLICT DO NOTHING;
まとめ
- 答え:コードはコードで管理されるため、**「どこで使われたか」**は
① 静的解析(Python)+ ② DBメタ解析(XML/QWeb/サーバーアクション) で 定期収集→portal_field_usageへ蓄積するのが堅実です。 - これにより、フィールドごとの“Where Used” をポータルから即参照でき、削除可否判断や影響範囲の見積りが一気に楽になります。
- さらに必要なら 限定環境で動的ログ を追加し、漏れを埋める二段構えにすると盤石です。
、OCA(Odoo Community Association)のモジュールは「次バージョンで必ず追随される」とは限りません。なので、**“使うなら自分で守る”**前提で、技術・運用の二層でリスクを下げるのが現実解です。下記の対策をセットで回すのがおすすめです。
OCAモジュール保全 方針サマリ
- 依存縮小:OCAに過度依存しない設計(ラッパー層・最小実装化)
- フォーク&バージョン固定:自社フォークで保守、リリースをピン止め
- 自動テストで事前検証:複数OdooバージョンのCIマトリクスで毎回回す
- 移行手順の標準化:OpenUpgrade+自作マイグレーションスクリプト
- 段階リリース:ステージング→パイロット→本番の順でローリング
- ロールバック準備:DBバックアップ&Compose/K8sでの迅速巻き戻し
1. 設計で守る(依存縮小)
- アダプタ/ラッパー層を自作:OCA APIを直接あちこちで呼ばず、
services/oca_<module>_adapter.pyのような薄い抽象化に集約。将来OCAが変わっても、修正はこの層に閉じ込められます。 - “必要最小限だけ使う”:OCAモジュールの中機能を全部採用せず、要件に必要な機能だけ有効化。代替可能な箇所は自前モジュールで小さく置き換える。
- ハードフォーク回避:根本を書き換えるより、
inheritで拡張。独自コードは別アドオンに隔離。
2. ソース管理(フォーク&ピン止め)
- OCAリポジトリを自社GitHubにフォークし、
requirements.txt/manifestのversionを固定。manifest['version'] = '16.0.1.2.0'のように追随可否を明示。external_dependencies(python)やpip-toolsで依存の上限下限をレンジ固定。
- 上流へPR:自社修正は可能な限りOCAへ還元(採用されれば将来の差分が減る)。
3. 自動テスト(マトリクスCI)
- テスト戦略:
- 契約テスト(外部I/Fが満たすべき振る舞いの固定化)
- 回帰テスト(主要シナリオ:受注→出荷→請求 など)
- データ移行テスト(旧→新スキーマで損失/破壊がないか)
- 実行環境:Dockerで Odoo
16.0/17.0/18.0(予定)のマトリクスを回す。 - 最小のCI例(概念):
# .github/workflows/odoo-oca-matrix.yml(概要) name: Odoo OCA Matrix on: [push, pull_request] jobs: test: strategy: matrix: odoo: [16.0, 17.0] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build & Test run: | docker compose -f docker-compose.${{ matrix.odoo }}.yml up -d db # モジュールインストール+テスト docker compose -f docker-compose.${{ matrix.odoo }}.yml run --rm odoo \ odoo-bin -d testdb -i your_module,oca_module --test-enable --stop-after-init実際はDB初期化、フィクスチャ投入、レポート生成なども含めます。
4. バージョンアップ/移行の定石
- OpenUpgrade を活用:差分分析・マイグレーションの当たりをつける。
- 自前のmigrations/(
pre-migrate.py/post-migrate.py)で- フィールドリネーム・型変更・データ分割などをコード化。
- 影響範囲チェック表:モデル/ビュー/セキュリティ/RPC/外部I/Fごとに変更有無を一覧化。
- スキーマ差分ツール(
pg_dump --schema-only比較など)でDB変化を可視化。
5. 展開プロセス(段階リリース)
- ローカル:Docker Composeでモジュール単体→連結テスト。
- ステージング:本番同等データのサブセット(匿名化)でリハーサル。
- パイロット:限定ユーザーで先行利用。
- 本番:時間帯を選び段階反映(Blue/GreenやK8sのローリング更新)。
6. ロールバックと監視
- バックアップ:
pg_dumpとS3保管、スナップショットの自動化。 - 即時Rollback手順書:Compose/K8sで前タグへ戻す手順をプレイブック化。
- 監視とアラート:Odooログ、Postgresメトリクス(接続数・ロック・遅延クエリ)、エラーレートで閾値アラート。
7. モジュール選定の見極めポイント
- メンテ状況:直近コミット頻度、CIグリーン、メンテナの人数
- 利用規模:Issue/PRの活発さ、Runbotの通過状況
- バージョンライン:直近メジャー(16→17)での移行実績
- 依存の浅さ:他OCAへの連鎖依存が深いほどリスク増
8. “結局、事前に自分でテストするしかない?”への回答
- はい。ただし“人手テスト”ではなく“自動テストと再現性のある環境”で毎回やる、が正解です。
- フォーク&ピン+CIマトリクス+移行スクリプト+段階デプロイ+ロールバック
→ この組み合わせで「壊れてもすぐ戻せる/すぐ直せる」体制にします。
- フォーク&ピン+CIマトリクス+移行スクリプト+段階デプロイ+ロールバック
すぐ着手できる最小セット(推奨)
- OCAモジュールをフォークして
version固定 - Docker Composeで
16/17の起動定義を分ける - CIマトリクス(16/17)で
--test-enableを回す - migrations/ の雛形を用意(pre/post)
- リリース手順書(バックアップ→デプロイ→検証→ロールバック)を1枚に
コメントを残す