Odoo 開発ポータル Viewの開発運用

運用をできるだけシンプルにして、**「初期一括取り込み → 以降はポータルを正として上書き or 追加」**に固定した手順を、開発向けにまとめます。差分チェックは一切しません。


運用ポリシー(前提)

  • 単一の正はポータル
    取り込み後は、Odoo側で直接編集しない(Studio含む)。
  • 既存の変更が必要になったら、ポータルで編集 → XML生成 → Odooへ上書き
  • 新規ビューはポータルに追加定義 → XML生成 → Odooへ反映
  • 差分検知・監査は行わない(その代わり、Odoo直編集は禁止・最小権限)。

0) 事前準備(1回だけ)

  • ポータルDBに portal_* テーブル&ENUMを作成済みであること。
  • Odoo DBに接続できる(同一PGかFDWでも可)。
  • Studio編集を原則禁止(権限付与を外す or 各環境で周知)。

1) 初期取り込み(初期Odoo → ポータル)※一度だけ

1-1. 既存のポータルデータを(モデル単位で)空にする

-- 例: sale.order を取り込み直す場合
DROP TABLE IF EXISTS tmp_victims;
CREATE TEMP TABLE tmp_victims AS
SELECT id AS view_id FROM portal_view WHERE model = 'sale.order';

DELETE FROM portal_menu               WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_action             WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_search_settings    WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_calendar_settings  WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_kanban_settings    WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_list_settings      WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_form_settings      WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_view_creation      WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_view_permissions   WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_view_common        WHERE view_id IN (SELECT view_id FROM tmp_victims);
DELETE FROM portal_view               WHERE id      IN (SELECT view_id FROM tmp_victims);
DROP TABLE IF EXISTS tmp_victims;

外部キーに ON DELETE CASCADE を張っているなら
DELETE FROM portal_view WHERE model='sale.order'; だけでOK。

1-2. ir_ui_viewportal_view へ取り込み

name が text/jsonb どちらでも動作、未知 typeother に丸める)

WITH params AS (SELECT 'sale.order'::text AS model_name),
src AS (
  SELECT
    v.id AS odoo_view_id,
    v.model,
    CASE v.type
      WHEN 'tree' THEN 'list' WHEN 'form' THEN 'form'
      WHEN 'kanban' THEN 'kanban' WHEN 'calendar' THEN 'calendar'
      WHEN 'search' THEN 'search' WHEN 'graph' THEN 'graph'
      WHEN 'pivot' THEN 'pivot' WHEN 'gantt' THEN 'gantt'
      ELSE 'other' END AS view_type_norm,
    CASE
      WHEN v.name IS NULL THEN NULL
      WHEN v.name::text LIKE '{%' THEN COALESCE((v.name::jsonb)->>'ja_JP',(v.name::jsonb)->>'en_US',v.name::text)
      ELSE v.name::text END AS view_name,
    v.priority AS priority_num,
    CASE WHEN v.priority<=8 THEN 'high'
         WHEN v.priority>=25 THEN 'low'
         ELSE 'standard' END AS priority_ui_txt,
    v.arch_db, v.inherit_id, v.write_date,
    imd.module AS origin_module, (imd.module||'.'||imd.name) AS xmlid,
    (imd.module = 'web_studio') AS is_studio
  FROM ir_ui_view v
  JOIN params p ON v.model = p.model_name
  LEFT JOIN ir_model_data imd ON imd.model='ir.ui.view' AND imd.res_id=v.id
)
INSERT INTO portal_view (
  source_view_id, source_xmlid, model, view_type, view_name,
  priority_ui, priority_num, enabled, ai_purpose,
  arch_db_snapshot, is_custom, origin, origin_module
)
SELECT
  s.odoo_view_id, s.xmlid, s.model, s.view_type_norm::view_type, s.view_name,
  s.priority_ui_txt::view_priority, s.priority_num, TRUE, NULL,
  s.arch_db, s.is_studio,
  CASE WHEN s.is_studio THEN 'studio'
       WHEN s.origin_module IS NOT NULL THEN 'module'
       ELSE 'standard' END::view_origin,
  s.origin_module
FROM src s;

1-3. common settings 自動生成(表示フィールド / 既定ソート)

WITH params AS (SELECT 'sale.order'::text AS model_name),
pv AS (
  SELECT id, arch_db_snapshot, view_type
  FROM portal_view JOIN params p ON portal_view.model=p.model_name
),
fields AS (
  SELECT pv.id AS view_id,
         (regexp_matches(pv.arch_db_snapshot,'<field\s+name="([a-zA-Z0-9_\.]+)"','g'))[1] AS fname,
         row_number() OVER (PARTITION BY pv.id ORDER BY ordinality) AS rn
  FROM pv, regexp_matches(pv.arch_db_snapshot,'<field\s+name="([a-zA-Z0-9_\.]+)"','g') WITH ORDINALITY
),
first_pos AS (
  SELECT view_id, fname, MIN(rn) AS first_rn
  FROM fields GROUP BY view_id, fname
),
agg AS (
  SELECT view_id, jsonb_agg(fname ORDER BY first_rn) AS display_fields
  FROM first_pos GROUP BY view_id
),
order_raw AS (
  SELECT pv.id AS view_id,
         (regexp_matches(pv.arch_db_snapshot,'default_order="([a-zA-Z0-9_]+)\s+(asc|desc)"'))[1] AS sort_field,
         (regexp_matches(pv.arch_db_snapshot,'default_order="([a-zA-Z0-9_]+)\s+(asc|desc)"'))[2] AS sort_dir_txt
  FROM pv WHERE pv.view_type='list'
),
order_norm AS (
  SELECT view_id, sort_field,
         CASE WHEN lower(sort_dir_txt)='asc' THEN 'asc'::sort_order ELSE 'desc'::sort_order END AS sort_dir
  FROM order_raw
)
INSERT INTO portal_view_common (view_id, display_fields, sort_field, sort_dir, default_group_by, default_filters)
SELECT pv.id, COALESCE(agg.display_fields,'[]'::jsonb), onm.sort_field, onm.sort_dir, NULL, NULL
FROM (SELECT id FROM portal_view pv JOIN params p ON pv.model=p.model_name) pv
LEFT JOIN agg ON agg.view_id=pv.id
LEFT JOIN order_norm onm ON onm.view_id=pv.id
ON CONFLICT (view_id) DO UPDATE
  SET display_fields=EXCLUDED.display_fields, sort_field=EXCLUDED.sort_field, sort_dir=EXCLUDED.sort_dir;

必要に応じ、List/Kanban/Calendar/Search の補助抽出(インライン編集/quick_create/開始・終了/検索優先度)も実行可。省略可。


2) 以降の運用(超シンプル)

2-1. 既存ビューを変更したいとき

  1. ポータルで編集(display_fields、並び、group_by、検索フィルタ等)
  2. XMLを生成(ポータルのテンプレで arch_db を作る)
  3. Odooへ上書き(対象 ir_ui_view.id = portal_view.source_view_id に対して)
-- :view_id は portal_view.id、:xml は生成したXML
UPDATE ir_ui_view
SET arch_db = :xml, write_date = now()
WHERE id = (SELECT source_view_id FROM portal_view WHERE id=:view_id);
  1. ポータル側スナップショットも更新(“正”がポータルなので、生成XMLで保持)
UPDATE portal_view SET arch_db_snapshot = :xml WHERE id = :view_id;

差分確認はしないポリシーのため、常にポータル→Odooへ上書き

2-2. 新規ビューを追加したいとき

  1. ポータルで新規行を作成portal_view と各設定テーブル)
    • 命名規則・優先度(High/Standard/Low)を入力
  2. XML生成Odooに挿入
    • 新規ビューは ir_model_data のXMLID を付けないなら、単純INSERTでも動きます
-- 新規挿入(arch_dbのみ記載。name/model/type/priority もセット)
-- 例: listビュー
INSERT INTO ir_ui_view (name, model, type, priority, arch_db, create_date, write_date)
VALUES ('Portal Generated: Sales Orders List', 'sale.order', 'tree', 16, :xml, now(), now())
RETURNING id;
  1. 得られた ir_ui_view.idportal_view.source_view_id に保存(ひも付け)
UPDATE portal_view SET source_view_id = :new_view_id WHERE id = :portal_view_id;
  1. スナップショット保存
UPDATE portal_view SET arch_db_snapshot = :xml WHERE id = :portal_view_id;

XMLIDが必要(モジュール配布や移行を意識)な場合は、モジュール側のXMLとして出力し、ir_model_data 連携まで行う方式に切替。


3) 環境フロー(推奨)

  • Dev:ポータルで編集→XML生成→Dev Odooへ反映
  • Staging:Devで確認後、同じXMLで反映
  • Prod:Staging確認後に反映(SQL一式をOpsが実行)
  • すべて ポータルを唯一の編集窓口にする(Odoo/Studio直編集は行わない)

4) 失敗時のリカバリ(最低限)

  • 直前のXMLを ポータル側に保存arch_db_snapshot が常に最新=ロールバック用)。
  • 誤配備時は そのXMLで再上書きすれば戻せます。

5) 開発者向けチェックリスト

  • 編集は必ずポータルで(Odoo/Studio直編集禁止)
  • 既存変更:ポータル編集→XML生成→ir_ui_view.arch_db 上書き→arch_db_snapshot更新
  • 新規追加:ポータル追加→XML生成→ir_ui_view INSERT→source_view_id 保存→arch_db_snapshot更新
  • 優先度(High/Standard/Low)と view_type を正しく設定
  • (任意)アクション/メニューが必要なら、別途 ir_actions_act_windowir_ui_menu を作成

これで、差分チェックなしでも、
「初期取り込み後は常にポータルが正」「既存は上書き/新規は追加」の運用が回ります。


Comments

コメントを残す

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