/** * 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'] ); } } } } } } } } 仕様書ドラフト モデル – Raqqa

仕様書ドラフト モデル

https://www.figma.com/make/PBonN5azF9LPA0hJFtUEOw/Odoo%E9%96%8B%E7%99%BA%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB-TOP%E7%94%BB%E9%9D%A2?p=f&t=BsJIe2vYrWUfEBev-0

画面:モデル一覧(Top / Models Page)— UI説明書兼 仕様

(前提:portal_modelportal_view_common1:1


0. このページでやりたいこと(結論)

このページは “現在Dev-Portalで管理対象としているモデル” を一覧表示するページである。
ユーザーはここでモデルを管理し、次の2種類の追加ができる:

  1. 新規作成(New):Dev-Portalに新しいモデルを追加する(Odooに存在するかは問わない)
  2. 既存追加(Import existing):Odoo側に既に存在するモデルを、srcテーブルから取り込んでDev-Portalに登録する

このページのベーステーブルは portal_modelportal_fields(集計)で構成する。


1. スコープ(今回やること / やらないこと)

1.1 やること(Must)

  • モデル一覧表示(ベース:portal_model
  • カスタムフィールド数表示(ベース:portal_fields 集計)
  • ボタン:
    • 「新規作成」=モデルを新規作成して portal_model に1行追加
    • 「既存追加」= portal_fields_src / portal_view_src から対象モデルのデータを抽出し、portal_model + portal_fields + portal_view_common(必要なら portal_view)へ格納
  • 追加後、一覧に即反映(再読み込み or optimistic update)

1.2 やらないこと(Not required / 今回対象外)

  • スマートボタン取り込み(portal_smart_button不要
  • メニュー取り込み(portal_menu不要
  • Odoo画面遷移 不要
  • 「適用済み/承認済み」などのOdoo整合ステータス判定 不要(暫定表示/非表示でOK)

2. データモデル(DB)— 最低限の前提

2.1 管理テーブル(Portal側:UIが表示・更新する)

  • public.portal_model
    • 1モデル = 1行(一覧の本体)
  • public.portal_fields
    • フィールド定義(一覧のカスタムフィールド数集計に使用)
  • public.portal_view_common
    • モデル共通(model-centric):1モデルにつき1行(= portal_model1:1
  • public.portal_view
    • view_type別の詳細:portal_view_common 1行に対して複数行(= 1:N

2.2 取り込み元(src:Odooメタを保持)

  • public.portal_fields_src(Odoo RPCで最新が入る前提)
  • public.portal_view_src(Odoo RPCで最新が入る前提)

注意:本仕様では「srcは常に最新である」前提で、*Import時に src → portal_ にコピー/正規化して保存**する。


3. ER(簡易:1:1 に統一)

portal_model (1) ──1:1── portal_view_common (1) ──< portal_view (N)
portal_model (1) ───────< portal_fields (N)

3.1 仕様上の意味

  • 新規作成portal_model のみ作る(fields/viewsは作らない)
  • 既存追加portal_model + portal_fields + portal_view_common(1行)+ portal_view(view_type分の複数行)を src から作る

4. 画面仕様(UI)

4.1 一覧表示(カード/リスト)

表示対象:portal_model の全件(検索/フィルタは後回しでOK)

カードに最低限表示する(例):

  • 表示名(label)
  • モデル技術名(例:sale.order
  • テーブル名(例:sale_order
  • カスタムフィールド数(portal_fields 集計)
  • メモ(任意)
  • 更新日(任意)

4.2 ボタン

  • 「+ 新規作成」
  • 「既存追加」

「既存追加」は “Odooにあるモデルを取り込む” 操作であることをUI上に明記する。


5. 機能仕様:新規作成(New Model)

5.1 目的

Dev-Portalに新しいモデル(=管理対象)を登録する。Odoo側の存在は問わない。

5.2 UI入力(最低限)

  • モデル技術名:model(例:x_custom.order
  • テーブル名:model_table(例:x_custom_order
  • 表示名:label or label_i18n.ja_JP(例:受注(カスタム)
  • メモ(任意)

5.3 DB反映(重要)

  • portal_model1行追加
  • fields/views/view_common は作らない(ゼロ件が正しい)
    • portal_fields:0件
    • portal_view_common:0件
    • portal_view:0件

つまり「新規作成したモデル」は、**フィールドもビューも未定義の“空モデル”**として登録される。

5.4 期待結果(成功時)

  • 一覧にそのモデルが表示される
  • カスタムフィールド数は 0(または未表示)

6. 機能仕様:既存追加(Import existing model)— API連携に変更(差し替え)

6.1 目的

Odoo側に既に存在するモデルを、srcテーブルにある最新メタから Portal管理テーブルに自動取り込みする。
この取り込みは 専用APIが実行し、フロントはそのAPIを呼ぶ。

6.2 UI入力(最低限)

  • 取り込み対象のモデル技術名(例:sale.order

6.3 フロントの責務(必須)

フロントは以下を実施する:

  1. ユーザーがモデル技術名を指定して「既存追加」を押す
  2. 既存モデル取り込みAPIを呼ぶ
  3. 成功時:一覧を再取得して表示更新(または返却結果で即時反映)
  4. 失敗時:エラーを表示(再実行できることを案内)

取り込みロジック(srcから抽出してportal_*へ格納)は すべてAPI側の責務

6.4 APIの期待仕様(オフショア向け・実装要件)

※エンドポイント名は実装に合わせて読み替え可。ただし “APIがある/フロントはそれを叩く” を仕様として固定する。

  • HTTPPOST /portal/models/import(例)
  • HeaderIdempotency-Key: <uuid>(POSTは必須運用とする)
  • Body(例) { "model": "sale.order" }
  • API内部でやること(確定)
    • portal_fields_src / portal_view_src から model = 対象モデル を抽出
    • portal_model をUPSERT(なければ作成)
    • portal_fields をUPSERT
    • portal_view_commonmodelにつき1行UPSERT(= 1:1)
    • portal_viewview_typeごとにUPSERT(= 1:N)
    • スマートボタン/メニューは取り込まない
  • Response(例:同期実行想定) { "status": "ok", "model": "sale.order", "upserted": { "portal_model": 1, "portal_fields": 128, "portal_view_common": 1, "portal_view": 5 } }

もし非同期(ジョブ)で実装されている場合でも、フロント視点では

  • 受付(accepted)→ 完了確認(再取得 or status API)
    が必要になるので、APIの返却に job_id を含める等の設計にする。

6.5 冪等性(必須)

  • 同じモデルでAPIを繰り返し叩いても、重複登録せず UPSERTで更新されること
  • portal_view_commonmodelで1行に維持されること
  • portal_view(common_id, view_type) で維持されること


7. 実装上の制約(DB一意性の前提)

実装者が迷わないよう、以下の一意性を前提とする。

  • portal_view_commonmodelで一意(推奨)
    • 例:UNIQUE (model)
  • portal_view:あなたのDDL通り
    • UNIQUE (common_id, view_type)

※もし既存DDLが action_xmlid UNIQUE のみで model UNIQUE がない場合は、実装側で「modelで1行にする」UPSERTキーを固定する(推奨はDDLに UNIQUE(model) を追加)。


8. 失敗時と再実行(運用)

  • Import中に失敗しても、再実行すれば整合すること(UPSERT)を担保する
  • 可能なら「1モデルのImport」はトランザクションでまとめてcommit(望ましい)

9. 受け入れ条件(DoD)

最低限、以下ができれば「大枠ができた」とみなす:

  1. Models page lists all records in portal_model
  2. New creates exactly one row in portal_model and creates no fields/views
  3. Import Existing calls an API, and the API imports from:
  4. portal_fields_srcportal_fields
  5. portal_view_srcportal_view_common (1 row per model) and portal_view (per view_type)
  6. Custom field count updates correctly after import
  7. Smart buttons and menus are not imported (not implemented)


Comments

コメントを残す

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