/** * 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 portal メニュー定義  – Raqqa

Odoo portal メニュー定義 

標準からの抽出

-- =========================================================
-- portal_menu: Create-from-scratch DDL (i18n + notes_i18n)
-- =========================================================
-- 必要なら明示的に落とす(本番は慎重に)
-- DROP TABLE IF EXISTS public.portal_menu CASCADE;

-- テーブル作成
CREATE TABLE IF NOT EXISTS public.portal_menu (
  id                   BIGSERIAL PRIMARY KEY,

  -- Odoo 参照(任意)
  menu_id              INTEGER,                 -- ir_ui_menu.id(同居してなくても値は保持可)
  menu_xml_id          TEXT,                    -- <module>.<name>

  -- 表示
  menu_path_i18n       JSONB NOT NULL DEFAULT '{}'::jsonb,  -- {"ja_JP":"販売/受注/...","en_US":""}
  sequence             INTEGER,

  -- アクション
  action_type          TEXT NOT NULL DEFAULT 'other' CHECK (
                         action_type IN ('act_window','report','client','server_action','url','other')
                       ),
  action_xml_id        TEXT,
  action_label_i18n    JSONB NOT NULL DEFAULT '{}'::jsonb,  -- {"ja_JP":"見積","en_US":""}

  -- 遷移先
  target_model         TEXT,                    -- 例: "sale.order"
  view_mode            TEXT,                    -- 例: "tree,form"

  -- 初期フィルタ
  initial_domain_expr  TEXT,                    -- 技術文字列(例: "[('state','=','draft')]")
  initial_domain_i18n  JSONB NOT NULL DEFAULT '{}'::jsonb,  -- {"ja_JP":"下書き","en_US":"Draft"}

  -- 由来
  origin               TEXT NOT NULL DEFAULT 'portal' CHECK (
                         origin IN ('standard','module','studio','portal','unknown')
                       ),
  origin_module        TEXT,                    -- 例: "sale"

  -- コード連携
  code_present         BOOLEAN NOT NULL DEFAULT FALSE,
  code_github_url      TEXT,

  -- 備考(プレーンテキスト+多言語)
  notes                TEXT,
  notes_i18n           JSONB NOT NULL DEFAULT '{}'::jsonb,  -- {"ja_JP":"...", "en_US":"..."}

  -- タイムスタンプ
  created_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at           TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 部分ユニーク(NULL を許容したまま重複抑止)
CREATE UNIQUE INDEX IF NOT EXISTS uq_portal_menu_menu_id
  ON public.portal_menu (menu_id) WHERE menu_id IS NOT NULL;

CREATE UNIQUE INDEX IF NOT EXISTS uq_portal_menu_menu_xml_id
  ON public.portal_menu (menu_xml_id) WHERE menu_xml_id IS NOT NULL;

-- よく使う検索キー
CREATE INDEX IF NOT EXISTS idx_portal_menu_target_model ON public.portal_menu (target_model);
CREATE INDEX IF NOT EXISTS idx_portal_menu_action_type  ON public.portal_menu (action_type);
CREATE INDEX IF NOT EXISTS idx_portal_menu_origin       ON public.portal_menu (origin);
CREATE INDEX IF NOT EXISTS idx_portal_menu_sequence     ON public.portal_menu (sequence);

-- JSONB の GIN インデックス(i18n 検索用)
CREATE INDEX IF NOT EXISTS idx_portal_menu_path_gin
  ON public.portal_menu USING GIN (menu_path_i18n);
CREATE INDEX IF NOT EXISTS idx_portal_menu_action_label_gin
  ON public.portal_menu USING GIN (action_label_i18n);
CREATE INDEX IF NOT EXISTS idx_portal_menu_domain_i18n_gin
  ON public.portal_menu USING GIN (initial_domain_i18n);
CREATE INDEX IF NOT EXISTS idx_portal_menu_notes_i18n_gin
  ON public.portal_menu USING GIN (notes_i18n);

-- updated_at 自動更新トリガ
CREATE OR REPLACE FUNCTION public.portal_menu_touch() RETURNS trigger AS $$
BEGIN
  NEW.updated_at := now();
  RETURN NEW;
END
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS trg_portal_menu_touch ON public.portal_menu;
CREATE TRIGGER trg_portal_menu_touch
BEFORE UPDATE ON public.portal_menu
FOR EACH ROW EXECUTE FUNCTION public.portal_menu_touch();

-- ==========================================
-- 動作確認用サンプル(必要な場合のみ実行)
-- ==========================================
-- 日本語だけ埋めて英語欠損 → 翻訳エンドポイントの dry-run で補完候補に出ます
-- INSERT INTO public.portal_menu (
--   menu_xml_id, menu_path_i18n, sequence,
--   action_type, action_xml_id, action_label_i18n,
--   target_model, view_mode, origin,
--   notes, notes_i18n
-- ) VALUES (
--   'sale.menu_sale_quotations',
--   '{"ja_JP":"販売/見積","en_US":""}',
--   10,
--   'act_window',
--   'sale.action_quotations',
--   '{"ja_JP":"見積","en_US":""}',
--   'sale.order',
--   'tree,form',
--   'portal',
--   '営業チームが使う見積リスト',
--   '{}'::jsonb
-- );

-- SELECT * FROM public.portal_menu ORDER BY id DESC LIMIT 5;
追加:必要なフィールドのみ表示させるため
ALTER TABLE portal_fields
  ADD COLUMN visibility TEXT NOT NULL DEFAULT 'visible',
  ADD CONSTRAINT ck_visibility CHECK (visibility IN ('visible','invisible'));
ポータル項目Odooメタの出所同じ表現で可能別表現で可能(導出/解決方法)備考
メニューパスmenu_path_i18nir_ui_menu.name(翻訳:ir_translation)+親チェーン部分的(各階層の名前は同一表現):親を再帰で辿って name/ 連結してフルパスを生成i18nは各ir_ui_menu名の翻訳で再現。フルパス自体は計算結果(別表現)。
インデントindent親子関係(ir_ui_menu.parent_id:親の段数を再帰CTEでカウント(depth=0がルート)画面表示用の補助値。
アクション(論理参照:action_xml_idir_model_data(対象:アクションレコード)ir_ui_menu.action → 汎用ir.actions.actions → 各種ir_actions_*へ解決 → そのidに対応するXMLIDを取得。
アクション名action_label_i18nアクションの name(翻訳:ir_translationir.actions.act_window.name 等をそのままi18nで取得。
アクション種別action_typeir.actions.actions.type(マッピング)代表:ir.actions.act_windowact_windowir.actions.serverserver_actionir.actions.reportreportir.actions.clientclientir.actions.act_urlurl
遷移先モデルtarget_modelir_actions_act_window.res_model / ir_actions_report.model / ir_actions_server.model_id(act_window, report)(server_actionはmodel_idir_model.modelへ解決)url/clientなどモデルが無い種別は空(手入力不要)。
ビュー種view_modeir_actions_act_window.view_mode補完可ir_actions_act_window_viewの並びからview_mode連結を再構築通常はaction.view_modeで十分。
初期ドメイン(式)initial_domain_exprir_actions_act_window.domainテクニカル表現のまま保存。
初期ドメイン(表示名)initial_domain_i18n不可(手入力)「下書き/送信済み」等の人が読むラベルはメタに無いので手入力。
出自originir_model_data.module(対象:メニュー/アクション)(判定)モジュールがweb_studiostudio、値あり→module、無し→standard
origin_moduleir_model_data.module例:salestock
コード有無code_presentorigin の判定結果から推測可(推定)module/standardtruestudio/portalfalse が目安(運用方針で調整可)。
コード参照(GITHUB)code_github_url不可(手入力)リポジトリやXMLID→URLのマッピングは環境依存。別テーブルで管理すれば半自動化可。
menu_id / menu_xml_idir_ui_menu.id / ir_model_data参照キーとして保存推奨。
sequenceir_ui_menu.sequence同階層の並び順。
notes不可(手入力)運用メモ。

画面項目

English column日本語名備考
idID主キー
menu_idメニューIDir_ui_menu.id
menu_xml_idメニューXMLID<module>.<name>
menu_path_i18nメニューパス(i18n)例:{"ja_JP":"販売/受注/見積","en_US":"Sales/Orders/Quotations"}
sequence並び順(インデント代用)同階層の順序。表示上のインデント計算に利用可
action_typeアクション種別act_window/report/client/server_action/url/other
action_xml_idアクションXMLID例:sale.action_quotations(URL系はNULL可)
action_label_i18nアクション名(i18n)例:{"ja_JP":"見積","en_US":"Quotations"}
target_model遷移先モデル例:sale.order(URL/Client等はNULL可)
view_modeビュー種例:tree,form
initial_domain_expr初期ドメイン式例:[('state','=','draft')]
initial_domain_i18n初期ドメイン(表示名 i18n)例:{"ja_JP":"下書き","en_US":"Draft"}
origin出自standard/module/studio/portal/unknown
origin_module出自モジュール例:sale
code_presentコード有無true/false
code_github_urlコード参照(GitHub)パーマリンク推奨
notes備考運用メモ
created_at作成日時自動設定
updated_at更新日時自動更新推奨


Comments

コメントを残す

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