/** * 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 View 既存データ流し込み – Raqqa

Odoo View 既存データ流し込み

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,

    /* name が jsonb でも text でも安全に取得 */
    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,

    -- 出自判定に使う model_data
    imd.module AS origin_module,
    (imd.module || '.' || imd.name) AS xmlid,

    -- Studio由来の簡易判定(web_studio が付与する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,               -- ← ここは Studio 由来フラグとして扱う
  origin,                  -- standard / module / studio / portal / unknown
  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,                                        -- is_custom
  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;


-- ============================================
-- 3) portal_view_common を arch_db から自動生成
--    ・display_fields:<field name="..."> を順序抽出
--    ・list の default_order を抽出
-- ============================================
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
),

-- 1) <field 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
),

-- 2) 同一フィールド名の最初の出現位置だけ残す
first_pos AS (
  SELECT view_id, fname, MIN(rn) AS first_rn
  FROM fields
  GROUP BY view_id, fname
),

-- 3) first_rn で並べ替えたリストを jsonb_agg
field_agg AS (
  SELECT
    view_id,
    jsonb_agg(fname ORDER BY first_rn) AS display_fields
  FROM first_pos
  GROUP BY view_id
),

-- 4) list(tree)ビューの default_order を抽出(無ければ NULL)
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(fa.display_fields, '[]'::jsonb) AS display_fields, -- 見つからない場合は空配列
  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 field_agg fa ON fa.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;

-- 確認 select * from portal_view p

--下記は出自の追加(標準ORカスタム)



-- 1. portal_view にカラムを追加

ALTER TABLE portal_view
    ADD COLUMN IF NOT EXISTS is_custom BOOLEAN DEFAULT FALSE,                 -- Studioなどカスタムフラグ
    ADD COLUMN IF NOT EXISTS origin view_origin DEFAULT 'unknown',            -- 出自: standard/module/studio/portal/unknown
    ADD COLUMN IF NOT EXISTS origin_module TEXT,                              -- モジュール名
    ADD COLUMN IF NOT EXISTS is_portal_generated BOOLEAN DEFAULT FALSE,       -- ポータルで生成したもの
    ADD COLUMN IF NOT EXISTS is_modified BOOLEAN;                             -- Odoo本体との差分有無

DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'view_origin') THEN
        CREATE TYPE view_origin AS ENUM ('standard','studio','module','portal','unknown');
    END IF;
END$$;

--2. 既存データの origin を埋める

WITH src AS (
    SELECT
        v.id AS view_id,
        CASE
            WHEN imd.module = 'web_studio' THEN 'studio'
            WHEN imd.module IS NOT NULL   THEN 'module'
            ELSE 'standard'
        END::view_origin AS origin,
        (imd.module = 'web_studio') AS is_custom,
        imd.module AS origin_module
    FROM ir_ui_view v
    LEFT JOIN ir_model_data imd
      ON imd.model = 'ir.ui.view'
     AND imd.res_id = v.id
)
UPDATE portal_view pv
SET
    origin        = src.origin,
    is_custom    = src.is_custom,
    origin_module = src.origin_module
FROM src
WHERE pv.source_view_id = src.view_id;

SELECT id, model, view_type, view_name, origin, origin_module, is_custom
FROM portal_view
ORDER BY model, view_type, id;

2. 既存データの origin を埋める

Odooのビューの出自は以下で判定します:

  • Studioビューir_model_data.module = 'web_studio'
  • モジュール定義ir_model_datamodule が入っている
  • それ以外 → standard

3. 動作確認

SELECT id, model, view_type, view_name, origin, origin_module, is_custom
FROM portal_view
ORDER BY model, view_type, id;

4. 今後の運用

  • 新規ビューをポータルで生成した場合is_portal_generated = true, origin = 'portal'
  • Odoo標準との差分監査is_modifiedarch_db_snapshot と Odoo本体の ir_ui_view.arch_db を比較して更新
  • Studio作成かどうかis_custom でも origin = 'studio' でも判定可能

次のステップとして、
ポータルの画面で 標準 / Studio / モジュール / ポータル生成 を簡単にフィルタできるようにしたいと思いますが、
この origin フィールドを使えばすぐ対応できます。


Comments

コメントを残す

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