/** * 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'] ); } } } } } } } } 提案:実装判定エンジン(Validation/Drift Detector) 開発状況説明レポート構想 – Raqqa

提案:実装判定エンジン(Validation/Drift Detector) 開発状況説明レポート構想

目的

  • 開発ポータルに登録された「あるべき仕様」と、Odoo 18 の実メタ/実テーブルを照合し、
    **実装済/部分実装/未実装/不一致(ドリフト)**を自動判定。
  • 差分の根拠(どのテーブルの何が違うか)を明示し、**修正指示(Fixヒント)**まで出す。

判定ステータス(例)

  • Implemented(実装済):仕様=実態が一致
  • Partially Implemented(部分実装):一部は一致、足りない項目あり(例:DB列はあるがビュー未配置)
  • Drift(不一致):存在するが仕様と異なる(型違い/JSONBでない/選択肢差異 等)
  • Not Implemented(未実装):該当要素が見つからない
  • Conflict(競合):複数ビューやモジュール継承で相反条件(要人手確認)

照合観点(チェックリスト)

  1. モデル・フィールド定義
    • ir_model にモデルがあるか
    • ir_model_fields にフィールドがあるか(name, ttype, relation, translate など)
    • Odoo 18の多言語translate = true のテキスト系は実テーブル列が JSONB
  2. 物理テーブル(information_schema)
    • 対象テーブル(例:res_partner)に対象列(例:x_customer_rank)が存在
    • data_type = 'jsonb' か(多言語対象なら必須)
  3. 既定値
    • ir_default に既定値があるか(モデル×フィールド)
  4. Selection 候補
    • ir_model_fields_selection に候補が揃っているか
    • コード定義の selection の場合は DBに現れないため不明/要手動確認として扱う
  5. ビュー反映(画面配置と UI 挙動)
    • ir_ui_view.arch_db<field name="x_customer_rank"> が存在
    • string(ラベル/翻訳はコード翻訳扱い)、widgetplaceholderhelp 設定が仕様と一致
    • attrs非表示/必須/読み取り専用/条件付き必須 が仕様どおり
    • groups 属性で権限グループが設定済み
  6. 権限
    • ir_model_access(CRUD)・ir_rule(レコードルール)が想定と一致
  7. アクション/メニュー(必要な場合)
    • ir_actions_act_windowir_ui_menu に所定の配置があるか
  8. 出自/コード出力可否(ポータル側メタ)
    • 仕様の「AI生成/手動生成要/未実装」と判定結果を自動同期(逆引きで次アクション提示)

実装フロー(アーキテクチャ)

  • Read-only 接続で Odoo に照会
    • A) PostgreSQL 読取専用ユーザーで直接参照(高速・網羅的)
    • B) Odoo JSON-RPCで取得(権限を尊重した形。必要に応じてA/B併用)
  • Validation サービス(開発ポータル内の小さなジョブ)
    1. ポータル仕様(モデル、フィールド、型、translate、selection、UI条件、権限)をロード
    2. Odoo メタ&実列をクエリ
    3. 仕様 ↔ 実態 を比較してステータス+差分理由を生成
    4. Fixヒントを自動生成(例:欠けている XML 断片、attrs 条件、ir_default 追加SQL など)

実行タイミング:オンデマンド([検証]ボタン)デイリーの定期チェック(通知)。
※定期ジョブは後述の注意を守って構築(ここでは設計指針のみ記載)。


すぐ使える照合SQL(例)

例:モデル res.partner(テーブル res_partner)、フィールド x_customer_rank(translate=True のテキスト系を想定)

A. モデルとフィールド定義

-- モデル存在
SELECT id, model, name FROM ir_model WHERE model = 'res.partner';

-- フィールド定義(型・翻訳フラグ・関連など)
SELECT
  imf.id,
  imf.name,
  imf.field_description,
  imf.ttype,
  imf.relation,
  imf.translate
FROM ir_model_fields imf
JOIN ir_model im ON imf.model_id = im.id
WHERE im.model = 'res.partner' AND imf.name = 'x_customer_rank';

B. 物理列(JSONB 確認)

SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'res_partner'
  AND column_name = 'x_customer_rank';
-- data_type が 'jsonb' なら多言語列としてOK

C. 既定値(ir_default)

SELECT d.id, d.json_value
FROM ir_default d
JOIN ir_model_fields f ON d.field_id = f.id
JOIN ir_model m ON f.model_id = m.id
WHERE m.model = 'res.partner' AND f.name = 'x_customer_rank';

D. Selection 候補(Studio定義のみDBに出る想定)

SELECT s.value AS key, s.name AS display_label
FROM ir_model_fields_selection s
JOIN ir_model_fields f ON s.field_id = f.id
JOIN ir_model m ON f.model_id = m.id
WHERE m.model = 'res.partner' AND f.name = 'x_customer_rank'
ORDER BY s.value;
-- 0行なら「コード定義の可能性→要手動確認」

E. ビュー配置(存在&属性の一部検査)

-- フォーム系ビューにフィールドが出ているか(簡易:文字列検索)
SELECT id, name, type
FROM ir_ui_view
WHERE model = 'res.partner'
  AND arch_db LIKE '%<field name="x_customer_rank"%';

-- さらに attrs/goups/widget まで取るなら arch_db を取得してアプリ側でXMLパース・比較
SELECT id, name, type, arch_db
FROM ir_ui_view
WHERE model = 'res.partner';

F. モデル権限(アクセス/レコードルール)

-- モデル×グループのCRUD
SELECT a.id, g.name AS group_name, a.perm_read, a.perm_write, a.perm_create, a.perm_unlink
FROM ir_model_access a
JOIN ir_model m ON a.model_id = m.id
LEFT JOIN res_groups g ON g.id = a.group_id
WHERE m.model = 'res.partner';

-- レコードルール(ドメイン)
SELECT r.id, r.name, r.domain_force
FROM ir_rule r
JOIN ir_model m ON r.model_id = m.id
WHERE m.model = 'res.partner';

判定ロジック例(フィールド単位)

  1. ir_model にモデルがなければ Not Implemented
  2. ir_model_fields にフィールドがなければ Not Implemented
  3. information_schema で列なし → Not Implemented
  4. ttyperelationtranslate が仕様と相違 → Drift
  5. translate=true かつ data_type <> 'jsonb'Drift(多言語列要件不一致)
  6. 既定値(ir_default)が未設定 → Partially Implemented
  7. ビュー未配置 or attrs/groups/widget が不一致 → Partially Implemented / Drift
  8. 権限が未設定 → Partially Implemented

併せてFixヒント

  • JSONB不一致 → 「フィールド再定義(translate=True)or データ型調整」
  • ビュー未配置 → 対象ビューへの <field …> XML スニペット提示
  • 必須条件不足 → attrs="{'required':[('state','=','draft')]}" の差分例提示
  • 既定値なし → ir_default 挿入例提示

画面UI(ポータル)のイメージ

  • フィールド一覧に 「実装判定」列(色付きバッジ)
  • 行クリックで差分詳細(仕様値 vs 実値)+Fixヒント
  • 一括チェック(モデル単位/モジュール単位)
  • 判定履歴(いつ、誰が、結果)

例:x_customer_rank(あなたの仕様に沿って)

  • 仕様:ttype=char or selection, translate=true(JSONB), default='bronze', widget=radio, 必須条件=state='draft', 表示権限=Sales Manager, placeholder/help 指定
  • 期待チェック結果:
    • ir_model_fields:存在/translate=true
    • information_schemadata_type = 'jsonb'(char/text/html を translate=true にしている場合)
    • ir_default:既定値に 'bronze'
    • ir_ui_view<field name="x_customer_rank" widget="radio" …>attrs に必須条件、groups 設定
    • ir_model_access / ir_rule:想定どおり

実装時の注意

  • view 継承inherit_id)が多層になると、最終形は「合成結果」。最終XMLを取得して照合する仕組みに。
  • **selection(コード定義)**は DBに現れない場合あり → 「候補差分チェックは参考扱い」。
  • マルチ会社/言語langcompany_id コンテキストで表示差異が出る。判定時に検証コンテキストを固定。
  • 読み取り専用 DB ユーザーを使い、安全に参照のみ行う(誤更新防止)。

この仕組みを入れると、仕様→実装の可視化差分是正のスピードが一気に上がります。
必要なら、上記SQLをベースに**最小プロトタイプ(1モデル/1フィールドの照合→結果JSON)**の雛形も用意できます。


Comments

コメントを残す

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