sale.order限定で、抽出→参照解決→重複排除→portal_smart_buttonへ投入まで、上から順にコピペ実行で再現できます。
前提:
portal_smart_button.view_idはir_ui_view(id)を参照する想定にしています(再現性重視)。
もしportal_view(id)を参照したい場合は、先に対応するportal_viewレコードを用意してください。
0) 事前:FKを ir_ui_view に向ける(必要な場合のみ)
ALTER TABLE portal_smart_button
DROP CONSTRAINT IF EXISTS portal_smart_button_view_id_fkey;
ALTER TABLE portal_smart_button
ADD CONSTRAINT portal_smart_button_view_id_fkey
FOREIGN KEY (view_id) REFERENCES ir_ui_view(id) ON DELETE CASCADE;
1) 必須ENUM型(無ければ作成)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname='portal_origin') THEN
CREATE TYPE portal_origin AS ENUM ('code','studio','portal','unknown','module','standard');
ELSE
-- 既存ENUMに不足があれば追加
IF NOT EXISTS (SELECT 1 FROM pg_type t JOIN pg_enum e ON e.enumtypid=t.oid
WHERE t.typname='portal_origin' AND e.enumlabel='module') THEN
ALTER TYPE portal_origin ADD VALUE 'module';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type t JOIN pg_enum e ON e.enumtypid=t.oid
WHERE t.typname='portal_origin' AND e.enumlabel='standard') THEN
ALTER TYPE portal_origin ADD VALUE 'standard';
END IF;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname='portal_button_action_type') THEN
CREATE TYPE portal_button_action_type AS ENUM ('act_window','server_action','object_method','url','other');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname='portal_button_target') THEN
CREATE TYPE portal_button_target AS ENUM ('current','new','inline','other');
END IF;
END$$;
2) portal_smart_button(未作成なら)
CREATE TABLE IF NOT EXISTS portal_smart_button (
id BIGSERIAL PRIMARY KEY,
view_id BIGINT NOT NULL REFERENCES ir_ui_view(id) ON DELETE CASCADE,
label_i18n JSONB NOT NULL DEFAULT '{}'::jsonb,
button_key TEXT NOT NULL, -- (view_id,button_key)で一意
target_model TEXT NOT NULL,
origin portal_origin NOT NULL DEFAULT 'standard',
show_count BOOLEAN NOT NULL DEFAULT FALSE,
dest_view_url TEXT,
is_codegen_target BOOLEAN NOT NULL DEFAULT FALSE,
show_in_inspection BOOLEAN NOT NULL DEFAULT TRUE,
notes TEXT,
action_type portal_button_action_type DEFAULT 'other',
action_ref TEXT,
target portal_button_target DEFAULT 'current',
domain JSONB,
context JSONB,
sequence INTEGER NOT NULL DEFAULT 10,
groups TEXT[] DEFAULT '{}',
badge_count_expr TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (view_id, button_key)
);
CREATE OR REPLACE FUNCTION set_timestamp_updated_at() RETURNS trigger AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_portal_smart_button_updated_at ON portal_smart_button;
CREATE TRIGGER trg_portal_smart_button_updated_at
BEFORE UPDATE ON portal_smart_button
FOR EACH ROW EXECUTE FUNCTION set_timestamp_updated_at();
3) ステージング:スキーマ、関数、互換ビュー
CREATE SCHEMA IF NOT EXISTS portal_stage;
-- '%(module.xmlid)d' → 'module.xmlid'
CREATE OR REPLACE FUNCTION portal_stage.extract_xmlid(p_name TEXT)
RETURNS TEXT LANGUAGE SQL IMMUTABLE AS $$
SELECT CASE WHEN p_name ~ '^\%\([^)]+\)d$'
THEN regexp_replace(p_name, '^\%\(([^)]+)\)d$', '\1')
ELSE NULL END;
$$;
-- 属性抽出の堅牢版(xpath→属性ノード→正規表現の三段フォールバック)
CREATE OR REPLACE FUNCTION portal_stage.attr_val_hard(node XML, attr TEXT)
RETURNS TEXT LANGUAGE SQL IMMUTABLE AS $$
WITH x AS (
SELECT
(xpath('string(./@' || attr || ')', node))[1]::text AS v1,
((xpath('./@' || attr, node))[1])::text AS v2,
xmlserialize(CONTENT node AS text) AS s
)
SELECT NULLIF(COALESCE(NULLIF(v1,''), NULLIF(regexp_replace(v2,'^[^"]*"(.*)".*$','\1'), ''),
CASE WHEN s ~ ('(?s)'||attr||'="') THEN regexp_replace(s,'(?s).*'||attr||'="([^"]*)".*','\1') END
), '');
$$;
-- act_window 互換ビュー
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
WHERE n.nspname='portal_stage' AND c.relname='v_ir_act_window' AND c.relkind IN ('v','m')
) THEN
EXECUTE 'DROP VIEW portal_stage.v_ir_act_window';
END IF;
IF EXISTS (SELECT 1 FROM pg_class WHERE relkind='r' AND relname='ir_act_window') THEN
EXECUTE 'CREATE VIEW portal_stage.v_ir_act_window AS SELECT * FROM ir_act_window';
ELSIF EXISTS (SELECT 1 FROM pg_class WHERE relkind='r' AND relname='ir_actions_act_window') THEN
EXECUTE 'CREATE VIEW portal_stage.v_ir_act_window AS SELECT * FROM ir_actions_act_window';
ELSE
RAISE EXCEPTION 'act_window テーブルが見つかりません(ir_act_window / ir_actions_act_window なし)';
END IF;
END$$;
-- ステージテーブル
DROP TABLE IF EXISTS portal_stage.sb_stage_views CASCADE;
CREATE TABLE portal_stage.sb_stage_views (
view_id BIGINT PRIMARY KEY,
view_model TEXT,
view_module TEXT,
view_xml XML
);
DROP TABLE IF EXISTS portal_stage.sb_stage_buttons CASCADE;
CREATE TABLE portal_stage.sb_stage_buttons (
view_id BIGINT,
view_model TEXT,
btn_idx INTEGER,
btn_node XML,
btn_type TEXT,
btn_name TEXT,
btn_label TEXT,
btn_class TEXT
);
DROP TABLE IF EXISTS portal_stage.sb_stage_resolved CASCADE;
CREATE TABLE portal_stage.sb_stage_resolved (
view_id BIGINT,
view_model TEXT,
btn_idx INTEGER,
btn_type TEXT,
btn_name TEXT,
btn_label TEXT,
btn_class TEXT,
action_xmlid TEXT,
action_module TEXT,
action_name TEXT,
action_target TEXT,
action_view_mode TEXT,
action_views TEXT,
action_model TEXT,
resolved_target_model TEXT,
origin portal_origin,
button_key TEXT
);
4) 取り込み(sale.order のビューだけ)
-- 4.1 ir_ui_view → ステージ(言語JSONB対応)
WITH base AS (
SELECT v.id AS view_id, v.model AS view_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.type='form' AND v.model='sale.order'
),
xml_ok AS (
SELECT b.view_id, b.view_model,
xmlparse(document regexp_replace(b.arch_text,'^[\uFEFF\s]*','')) AS view_xml
FROM base b
WHERE b.arch_text IS NOT NULL AND position('<' in b.arch_text) > 0
),
view_xmlid AS (
SELECT d.res_id AS view_id, d.module AS view_module
FROM ir_model_data d
WHERE d.model='ir.ui.view'
)
INSERT INTO portal_stage.sb_stage_views(view_id, view_model, view_module, view_xml)
SELECT x.view_id, x.view_model, vx.view_module, x.view_xml
FROM xml_ok x LEFT JOIN view_xmlid vx ON vx.view_id=x.view_id
ON CONFLICT (view_id) DO UPDATE
SET view_model=EXCLUDED.view_model, view_module=EXCLUDED.view_module, view_xml=EXCLUDED.view_xml;
-- 4.2 スマートボタン抽出(button_box / header / 継承)
TRUNCATE portal_stage.sb_stage_buttons;
WITH btns_form AS (
SELECT v.view_id, v.view_model,
unnest(xpath('
//form//div[@name="button_box"]//button
| //form/header//button
| //form//button[ contains(concat(" ", normalize-space(@class), " "), " oe_stat_button ")
or contains(concat(" ", normalize-space(@class), " "), " o_stat_button ")
or contains(concat(" ", normalize-space(@class), " "), " stat_button ") ]
| //form//a[ contains(concat(" ", normalize-space(@class), " "), " oe_stat_button ")
or contains(concat(" ", normalize-space(@class), " "), " o_stat_button ")
or contains(concat(" ", normalize-space(@class), " "), " stat_button ") ]
', v.view_xml)) AS btn_node
FROM portal_stage.sb_stage_views v
),
btns_inherit AS (
SELECT v.view_id, v.view_model,
unnest(xpath('
//data//*[local-name()="xpath"][contains(@expr,"button_box") or contains(@expr,"header")]//button
| //data//*[local-name()="xpath"]//button[
contains(concat(" ", normalize-space(@class), " "), " oe_stat_button ")
or contains(concat(" ", normalize-space(@class), " "), " o_stat_button ")
or contains(concat(" ", normalize-space(@class), " "), " stat_button ")
]
', v.view_xml)) AS btn_node
FROM portal_stage.sb_stage_views v
),
all_btns AS (SELECT * FROM btns_form UNION ALL SELECT * FROM btns_inherit),
proj AS (
SELECT
a.view_id, a.view_model,
row_number() OVER (PARTITION BY a.view_id ORDER BY xpath('string(.)', a.btn_node)::text) AS btn_idx,
a.btn_node,
portal_stage.attr_val_hard(a.btn_node,'type') AS btn_type,
portal_stage.attr_val_hard(a.btn_node,'name') AS btn_name,
portal_stage.attr_val_hard(a.btn_node,'class') AS btn_class,
COALESCE(
NULLIF((xpath('string(./@string)', a.btn_node))[1]::text,''),
NULLIF((xpath('string(.//field[@widget="statinfo"]/@string)', a.btn_node))[1]::text,''),
NULLIF((xpath('string(.//span/@title)', a.btn_node))[1]::text,''),
NULLIF((xpath('normalize-space(string(.))', a.btn_node))[1]::text,'')
) AS btn_label
FROM all_btns a
)
INSERT INTO portal_stage.sb_stage_buttons(view_id, view_model, btn_idx, btn_node, btn_type, btn_name, btn_label, btn_class)
SELECT view_id, view_model, btn_idx, btn_node, btn_type, btn_name, btn_label, btn_class
FROM proj;
5) 参照解決(action)+ object の補完
-- 5.1 action(xmlid) → act_window 解決
TRUNCATE portal_stage.sb_stage_resolved;
WITH cand AS (
SELECT b.view_id, b.view_model, b.btn_idx,
b.btn_type, b.btn_name, b.btn_label, b.btn_class,
portal_stage.extract_xmlid(b.btn_name) AS action_xmlid_like
FROM portal_stage.sb_stage_buttons b
WHERE b.btn_type='action'
),
split AS (
SELECT c.*, split_part(c.action_xmlid_like,'.',1) AS xml_module,
split_part(c.action_xmlid_like,'.',2) AS xml_name
FROM cand c
WHERE c.action_xmlid_like IS NOT NULL AND c.action_xmlid_like <> ''
),
dmap AS (
SELECT s.view_id, s.view_model, s.btn_idx, s.btn_type, s.btn_name, s.btn_label, s.btn_class,
s.action_xmlid_like AS action_xmlid,
d.module AS action_module, d.name AS action_name, d.model AS action_model, d.res_id AS action_res_id
FROM split s
JOIN ir_model_data d ON d.module=s.xml_module AND d.name=s.xml_name
WHERE d.model LIKE 'ir.actions.%'
),
aw AS (
SELECT m.*, a.res_model, a.target, a.view_mode
FROM dmap m
LEFT JOIN portal_stage.v_ir_act_window a
ON a.id = m.action_res_id
AND m.action_model='ir.actions.act_window'
)
INSERT INTO portal_stage.sb_stage_resolved (
view_id, view_model, btn_idx, btn_type, btn_name, btn_label, btn_class,
action_xmlid, action_module, action_name, action_target, action_view_mode, action_views, action_model,
resolved_target_model, origin, button_key
)
SELECT
a.view_id, a.view_model, a.btn_idx, a.btn_type, a.btn_name, a.btn_label, a.btn_class,
a.action_xmlid, a.action_module, a.action_name,
COALESCE(a.target,'current') AS action_target,
a.view_mode, NULL::text AS action_views, a.action_model,
a.res_model AS resolved_target_model,
CASE
WHEN v.view_module='web_studio' THEN 'studio'::portal_origin
WHEN v.view_module IS NOT NULL THEN 'module'::portal_origin
WHEN a.action_module='web_studio' THEN 'studio'::portal_origin
WHEN a.action_module IS NOT NULL THEN 'module'::portal_origin
ELSE 'standard'::portal_origin
END AS origin,
a.action_xmlid AS button_key
FROM aw a
LEFT JOIN portal_stage.sb_stage_views v ON v.view_id=a.view_id;
-- 5.2 object(メソッド呼び出し→ターゲットはビューのmodel)
INSERT INTO portal_stage.sb_stage_resolved (
view_id, view_model, btn_idx, btn_type, btn_name, btn_label, btn_class,
action_xmlid, action_module, action_name, action_target, action_view_mode, action_views, action_model,
resolved_target_model, origin, button_key
)
SELECT
b.view_id, b.view_model, b.btn_idx, b.btn_type, b.btn_name, b.btn_label, b.btn_class,
NULL, NULL, NULL, 'current', NULL, NULL, NULL,
b.view_model AS resolved_target_model,
CASE
WHEN v.view_module='web_studio' THEN 'studio'::portal_origin
WHEN v.view_module IS NOT NULL THEN 'module'::portal_origin
ELSE 'standard'::portal_origin
END AS origin,
'object:'||b.btn_name AS button_key
FROM portal_stage.sb_stage_buttons b
LEFT JOIN portal_stage.sb_stage_views v ON v.view_id=b.view_id
WHERE b.btn_type='object';
6) portal_smart_button へ UPSERT(重複排除つき)
WITH ranked AS (
SELECT r.*, (row_number() OVER (PARTITION BY r.view_id ORDER BY r.btn_idx))*10 AS seq
FROM portal_stage.sb_stage_resolved r
WHERE r.view_model='sale.order'
),
payload AS (
SELECT
view_id, button_key,
jsonb_strip_nulls(jsonb_build_object('ja', NULLIF(btn_label,'')))::jsonb AS label_i18n,
resolved_target_model AS target_model,
origin,
(position('stat' in coalesce(btn_class,'')) > 0) AS show_count,
NULL::TEXT AS dest_view_url,
FALSE AS is_codegen_target,
TRUE AS show_in_inspection,
NULL::TEXT AS notes,
CASE
WHEN btn_type='action' AND action_model='ir.actions.act_window' THEN 'act_window'
WHEN btn_type='object' THEN 'object_method'
ELSE 'other'
END::portal_button_action_type AS action_type,
COALESCE(action_xmlid, btn_name) AS action_ref,
COALESCE(NULLIF(action_target,''), 'current')::portal_button_target AS target,
seq,
btn_type, action_model, btn_label, btn_class, btn_idx
FROM ranked
),
scored AS (
SELECT p.*,
(CASE WHEN btn_type='action' THEN 100 ELSE 0 END) +
(CASE WHEN action_model='ir.actions.act_window' THEN 10 ELSE 0 END) +
(CASE origin WHEN 'studio' THEN 4 WHEN 'module' THEN 3 WHEN 'standard' THEN 2 ELSE 1 END) +
(CASE WHEN coalesce(btn_label,'')<>'' THEN 1 ELSE 0 END) AS score
FROM payload p
),
dedup AS (
SELECT *, row_number() OVER (
PARTITION BY view_id, button_key
ORDER BY score DESC, seq ASC, btn_idx ASC
) AS rn
FROM scored
)
INSERT INTO portal_smart_button (
view_id, button_key, label_i18n, target_model, origin,
show_count, dest_view_url, is_codegen_target, show_in_inspection, notes,
action_type, action_ref, target, sequence
)
SELECT
view_id, button_key, label_i18n, target_model, origin,
show_count, dest_view_url, is_codegen_target, show_in_inspection, notes,
action_type, action_ref, target, seq AS sequence
FROM dedup
WHERE rn=1
ON CONFLICT (view_id, button_key) DO UPDATE
SET label_i18n = EXCLUDED.label_i18n,
target_model = EXCLUDED.target_model,
origin = EXCLUDED.origin,
show_count = EXCLUDED.show_count,
dest_view_url = EXCLUDED.dest_view_url,
show_in_inspection = EXCLUDED.show_in_inspection,
notes = EXCLUDED.notes,
action_type = EXCLUDED.action_type,
action_ref = EXCLUDED.action_ref,
target = EXCLUDED.target,
sequence = EXCLUDED.sequence;
7) 動作確認
SELECT
psb.view_id,
(psb.label_i18n->>'ja') AS button_name_ja,
psb.button_key,
psb.target_model,
psb.origin,
psb.action_type,
psb.action_ref,
psb.show_count
FROM portal_smart_button psb
ORDER BY psb.view_id, psb.sequence;
現場一覧の主要カラム × Studio対応可否(要点だけ)
| 項目 | Studioで設定できる? | 補足 |
|---|---|---|
| ボタン名(label_i18n) | 〇 | ラベル(表示名)はStudioで編集可能。多言語は翻訳機能側で対応。 |
| 遷移先モデル(target_model) | 〇/△ | act_windowで既存アクションに紐づけて実質的に指定は可能。object_methodの新規メソッド作成はStudio単独では不可(自動処理=サーバーアクションで近似は可)。 |
| 出自(origin) | (自動) | Studioで作れば自動的に「Studio由来」。手で設定する項目ではない。 |
| 件数表示(show_count) | △ | 「スマートボタン(stat)」+数値フィールドで表示可。単純な計算/関連ならStudioで計算フィールドやオートメーションで作れるが、複雑集計や高パフォーマンス集計はコード推奨。 |
| 遷移ビューURL(dest_view_url) | △/× | Studioはアクション(ビュー/コンテキスト/ドメイン)を設定するのが基本。URL直遷移は ir.actions.url を使えば一部可能だが、StudioUIだけでの細かい制御は限定的。 |
| コード生成対象(is_codegen_target) | × | これはポータル側メタ。Studioの範囲外。 |
| 備考(notes) | × | これも運用メタ。Studioの範囲外。 |
Studioで“さらに”設定できる主なこと
- 権限制御(groups):特定グループにのみボタン表示/利用を制限。
- 可視条件(invisible / ドメイン相当):状態やフィールド値に応じて表示/非表示。
- 配置・見た目:
button_box(ヘッダの統計ボタン枠)に配置、アイコン設定、順序(並び)調整。 - アクション紐付け:既存の Window Action を割り当て、コンテキストや(可能な範囲の)デフォルトフィルタを付与。
- 簡易カウントの実現:Studioの計算フィールドや**オートメーション(サーバーアクション)**で件数用の数値を用意し、スマートボタンに表示。
ポータル「スマートボタン」入力ガイド(コンサル向け・自動取得なし)
このレコードが表すもの
- 対象:フォームヘッダの統計/遷移ボタン(Smart Button)
- 目的:ポータル上で 表示文言・遷移先・件数バッジ を宣言的に管理し、必要に応じて XML生成依頼(コード生成対象)に載せる
入力項目と記入ルール(手入力)
1) ボタン名
- 記入内容:画面に表示するボタンテキスト
- 形式:自由テキスト(日本語推奨、英語が必要なら備考に併記可)
- 命名ヒント:業務用語そのまま・2〜6文字程度を目安(例:配送/請求書/入金)
2) 遷移先モデル
- 記入内容:押下後に扱うモデルのテクニカル名
- 形式:
stock.pickingのようなドット区切り - 決め方の目安:ボタンで開きたい一覧/明細の主モデルを1つ指定
3) 遷移ビューURL
- 記入内容:押下時に開く画面のURL(テンプレ可)
- 形式(代表例):
/web#action=<action_id>&model=<res_model>&view_type=list
外部遷移なら絶対URLを記入 - 補足:
action_idが環境差異を持つ場合は XMLID解決プロセスを別途運用し、ここはテンプレ/メモでも可
4) 出自
- 記入内容:このボタン定義の由来
- 選択肢:
standard/module/studio/portal/unknown - 運用ルール:
studioは将来のXML生成と衝突しやすいので、採用/置換方針を備考に明記
5) 件数表示(トグル)
- 記入内容:ボタン上にバッジ(件数)を出すか
- 選択:ON / OFF
- 注意:重い集計や複雑な権限条件が絡む場合は OFF を推奨(理由は備考へ)
6) コード生成対象(トグル)
- 記入内容:このボタンをXML自動生成の対象に含めるか
- 選択:ON / OFF
- 基準:要件が固い・参照先が安定→ON/プロトタイピング段階→OFF
7) 備考
- 記入内容:要件・設計意図・権限前提・関連チケット・表示条件など
- 形式:自由テキスト(箇条書き推奨)
- 例:
在庫ユーザのみ表示。0件でも遷移可。承認後は読み取り専用。
入力チェック(クイックルール)
- 必須:ボタン名/遷移先モデル/(可能なら)遷移ビューURL
- 整合:
- 遷移先モデルとURL内の
model=は一致させる - 件数表示ON時は、集計範囲とフィルタ条件を備考に明記(例:
order_id=active_id)
- 遷移先モデルとURL内の
- 運用:
studio出自は将来置換方針を備考に、module出自は参照モジュール名も備考に
画面項目 ↔ DB カラム対応(実装メモ)
| 画面項目 | 推奨カラム(例:portal_smart_button) | 備考 |
|---|---|---|
| ボタン名 | label_i18n | 単一言語入力でもDBは {"ja_JP":"…","en_US":"…"} を推奨 |
| 遷移先モデル | target_model | stock.picking など |
| 遷移ビューURL | view_url | /web#action=...&model=... 等 |
| 出自 | origin(+ module) | standard/module/studio/portal/unknown |
| 件数表示(トグル) | show_badge | 併用:badge_count_expr(必要なら式やドメインを文字列で管理) |
| コード生成対象(トグル) | is_codegen_target | 生成依頼の対象フラグ |
| 備考 | notes | 権限・条件・運用メモ |
既に詳細項目(
action_type,action_ref,domain/context,groupsなど)を保持する場合は、上記は現場入力のサマリとして使用。
入力例(sale.order の「配送」ボタン)
- ボタン名:配送
- 遷移先モデル:
stock.picking - 遷移ビューURL:
/web#action=stock.action_picking_tree_all&model=stock.picking - 出自:
module(stock) - 件数表示:ON
- コード生成対象:ON
- 備考:
在庫ユーザのみ表示。0件でも遷移可。一覧→フォームの順で利用を想定。
コメントを残す