/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } Odoo python カスタマイズとそのメタデータ – Raqqa

Odoo python カスタマイズとそのメタデータ

フィールドを起点にして、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 件 -&gt; 候補"]
        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/*.pyDBにはコード本体は保存されません。起動時にレジストリへロードされ、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)DBir_ui_view.arch_db(XML、近年は多言語JSONBの場合あり)コード側XML(views/*.xml)もインストール時にDBへ取り込まれ、以後はDBが一次情報になります。
アクション/メニューDBir_actions_* / ir_ui_menuPythonから定義しても、インストールでDBにレコード化。
権限/レコードルールDBir_model_access / ir_ruleモジュール配布XML(security/*.xml)→インストールでDB化。
外部ID(XMLID)DBir_model_data「コード⇔DB」を結び、アップグレード時の差分適用のカナメ。
サーバーアクション(Pythonコード)DBir_actions_server.codeここだけ実コードがDBに保存され、safe_evalで実行。自動化(ir_cron)から呼ぶことも多い。
自動化(Automated Actions)DBbase.automationドメイン/トリガ/(場合により)Python式がDBに保存。
Studioカスタム(フィールド/ビュー/自動化)DB(上記メタ各所)「Studio生成モジュール」にエクスポートも可能だが、基本はDBメタ。

「データ処理」はどこに?

  • **通常のデータ処理(ビジネスロジック)**は Pythonコード側
    • create()/write()/unlink() のオーバーライド
    • 計算フィールド:compute / inverse / @api.depends
    • @api.constrains / @api.onchange
    • トランザクション内の手続き(在庫転送、請求生成など)
  • DBメタには関数本体は入りません。入るのは“そのフィールドが計算か否か/storeか否か”等の記述的属性のみ(版によってir_model_fieldscompute名やrelated文字列を持つことはありますが、実行コードはDBに保存されないのが原則)。

「コードがメタ情報に格納される」例外

  • サーバーアクション(Python)ir_actions_server.code にテキストとして保存。
    • 手軽ですが、テスト性/レビュー/移植の観点で大規模ロジックには不向き
    • モジュール化してPythonに移すのがベター。
  • Studioの計算フィールド/自動化:StudioのUIで書いた式(Python/ドメイン)はDBに式として保存されます。
    • こちらも成長したらモジュールのPythonへ移植推奨。

運用ベストプラクティス(実務指針)

  1. ビジネスロジックはGit管理のPythonモジュールに集約
    • レビュー、テスト、デプロイ手順を標準化できます。
  2. DBメタは“結果のスナップショットと参照”
    • ir_model_*/ir_ui_view/ir_actions_*/ir_model_data は、“何が定義されたか”の台帳。
  3. Studioやサーバーアクションはプロトタイプ/軽量用途
    • 長期運用するならPythonコード化してGit管理へ。
  4. 移植性の確保
    • 重要なビュー/アクション/権限はモジュール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_datamodel='ir.ui.view' AND res_id=view_id を JOIN してください。

3.2 QWeb テンプレート(帳票/ポータル)

QWeb は ir.ui.viewtype='qweb' か、keymodule.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な回し方)

  1. フィールド一覧portal_fields がソース。
  2. 静的解析(rg/semgrep/AST)→ CSV に吐き出し → portal_field_usage(py_*) にUPSERT。
  3. DBメタ解析(上のSQL)→ portal_field_usage(xml_*) にUPSERT。
  4. (任意)動的解析portal_field_usage(runtime_*) にUPSERT。
  5. ポータル表示
    • 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モジュール保全 方針サマリ

  1. 依存縮小:OCAに過度依存しない設計(ラッパー層・最小実装化)
  2. フォーク&バージョン固定:自社フォークで保守、リリースをピン止め
  3. 自動テストで事前検証:複数OdooバージョンのCIマトリクスで毎回回す
  4. 移行手順の標準化:OpenUpgrade+自作マイグレーションスクリプト
  5. 段階リリース:ステージング→パイロット→本番の順でローリング
  6. ロールバック準備: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. 展開プロセス(段階リリース)

  1. ローカル:Docker Composeでモジュール単体→連結テスト。
  2. ステージング:本番同等データのサブセット(匿名化)でリハーサル。
  3. パイロット:限定ユーザーで先行利用。
  4. 本番:時間帯を選び段階反映(Blue/GreenやK8sのローリング更新)。

6. ロールバックと監視

  • バックアップpg_dump とS3保管、スナップショットの自動化
  • 即時Rollback手順書:Compose/K8sで前タグへ戻す手順をプレイブック化。
  • 監視とアラート:Odooログ、Postgresメトリクス(接続数・ロック・遅延クエリ)、エラーレートで閾値アラート

7. モジュール選定の見極めポイント

  • メンテ状況:直近コミット頻度、CIグリーン、メンテナの人数
  • 利用規模:Issue/PRの活発さ、Runbotの通過状況
  • バージョンライン:直近メジャー(16→17)での移行実績
  • 依存の浅さ:他OCAへの連鎖依存が深いほどリスク増

8. “結局、事前に自分でテストするしかない?”への回答

  • はい。ただし“人手テスト”ではなく“自動テストと再現性のある環境”で毎回やる、が正解です。
    • フォーク&ピン+CIマトリクス+移行スクリプト+段階デプロイ+ロールバック
      → この組み合わせで「壊れてもすぐ戻せる/すぐ直せる」体制にします。

すぐ着手できる最小セット(推奨)

  1. OCAモジュールをフォークしてversion固定
  2. Docker Compose16/17 の起動定義を分ける
  3. CIマトリクス(16/17)で --test-enable を回す
  4. migrations/ の雛形を用意(pre/post)
  5. リリース手順書(バックアップ→デプロイ→検証→ロールバック)を1枚に

Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です