/** * 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 ポータルタブ定義 – Raqqa

Odoo ポータルタブ定義

1) テーブル定義(ポータル画面の項目にジャスト)

— 補助スキーマ/関数(未作成なら一度だけ)
CREATE SCHEMA IF NOT EXISTS portal_stage;

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’)
ELSE NULL END
),

)
FROM x;
$$;

DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname=’portal_origin’) THEN
CREATE TYPE portal_origin AS ENUM (‘code’,’module’,’studio’,’portal’,’standard’,’unknown’);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname=’portal_subview_policy’) THEN
CREATE TYPE portal_subview_policy AS ENUM (‘reference’,’auto’,’none’);
END IF;
END$$;

— 画面項目に合わせた最小テーブル
CREATE TABLE IF NOT EXISTS portal_tab_portal (
id BIGSERIAL PRIMARY KEY,
view_id BIGINT NOT NULL REFERENCES ir_ui_view(id) ON DELETE CASCADE,
tab_key TEXT NOT NULL, — name:<@name> or page:
page_idx INTEGER NOT NULL, — 簡易順序(継承は+1000など)
tab_label_ja TEXT, — タブ名(日本語)
tab_label_en TEXT, — タブ名(英語)

— 子モデル
child_model TEXT,
child_link_field TEXT,

— 出自
origin portal_origin NOT NULL DEFAULT ‘standard’,
module TEXT,

— コード生成/備考/GitHubリンク
is_codegen_target BOOLEAN NOT NULL DEFAULT FALSE,
notes TEXT,
github_url TEXT,

— サブビュー設定(tree/formの参照 or 生成)
view_mode TEXT, — ‘tree’,’form’,’tree,form’
tree_view_xmlid TEXT,
form_view_xmlid TEXT,
subview_policy_tree portal_subview_policy,
subview_policy_form portal_subview_policy,

— 表示制御(要否+値)
use_domain BOOLEAN,
domain_raw TEXT,
use_context BOOLEAN,
context_raw TEXT,

— 編集動作(インライン/行追加可否)
inline_edit BOOLEAN,
allow_create_rows BOOLEAN,
allow_delete_rows BOOLEAN,
options_raw TEXT, — options=”{‘no_create’: False, …}” などの原文

created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

UNIQUE(view_id, tab_key)
);

— updated_at 自動更新
CREATE OR REPLACE FUNCTION portal_stage.touch_updated_at() RETURNS trigger AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS trg_portal_tab_portal_touch ON portal_tab_portal;
CREATE TRIGGER trg_portal_tab_portal_touch
BEFORE UPDATE ON portal_tab_portal
FOR EACH ROW EXECUTE FUNCTION portal_stage.touch_updated_at();

CREATE INDEX IF NOT EXISTS idx_ptp_view ON portal_tab_portal(view_id, page_idx);
CREATE INDEX IF NOT EXISTS idx_ptp_origin ON portal_tab_portal(origin);

② 既存のタブ情報(sale.order)→ portal_tab_portal へ投入

WITH
param AS (
SELECT ‘sale.order’::text AS model
),
views AS (
SELECT
v.id AS view_id,
v.model AS view_model,
— 既定XML(優先: ja_JP → ja → en_US → 先頭)
COALESCE(
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,
— 日本語XML(あれば)
CASE WHEN pg_typeof(v.arch_db)::text=’jsonb’ THEN v.arch_db->>’ja_JP’ ELSE NULL END AS arch_text_ja,
— 英語XML(あれば)
CASE WHEN pg_typeof(v.arch_db)::text=’jsonb’ THEN v.arch_db->>’en_US’ ELSE NULL END AS arch_text_en
FROM ir_ui_view v
JOIN param p ON p.model = v.model
WHERE v.type=’form’
),
xml_main AS (
SELECT
view_id, view_model,
xmlparse(document regexp_replace(arch_text, ‘^[\uFEFF\s]‘, ”)) AS x_main, CASE WHEN arch_text_ja IS NOT NULL AND arch_text_ja LIKE ‘%<%’ THEN xmlparse(document regexp_replace(arch_text_ja, ‘^[\uFEFF\s]‘, ”)) END AS x_ja,
CASE WHEN arch_text_en IS NOT NULL AND arch_text_en LIKE ‘%<%’
THEN xmlparse(document regexp_replace(arch_text_en, ‘^[\uFEFF\s]*’, ”)) END AS x_en
FROM views
WHERE arch_text LIKE ‘%<%’
),
view_origin AS (
SELECT
d.res_id AS view_id,
CASE WHEN d.module=’web_studio’ THEN ‘studio’
WHEN d.module IS NOT NULL THEN ‘module’
ELSE ‘standard’ END AS origin,
d.module AS module
FROM ir_model_data d
WHERE d.model=’ir.ui.view’
),

— (直書き)
pages_form AS (
SELECT
x.view_id,
s.arr[g.idx] AS page_node,
g.idx::int AS page_idx
FROM xml_main x
CROSS JOIN LATERAL (SELECT xpath(‘//form//notebook/page’, x.x_main) AS arr) s
CROSS JOIN LATERAL generate_subscripts(s.arr, 1) AS g(idx)
),

— (継承:概算で+1000)
pages_inherit AS (
SELECT
x.view_id,
s.arr[g.idx] AS page_node,
(g.idx + 1000)::int AS page_idx
FROM xml_main x
CROSS JOIN LATERAL (SELECT xpath(‘//data//*[local-name()=”xpath”]//page’, x.x_main) AS arr) s
CROSS JOIN LATERAL generate_subscripts(s.arr, 1) AS g(idx)
),

pages_all AS (
SELECT view_id, page_node, page_idx FROM pages_form
UNION ALL
SELECT view_id, page_node, page_idx FROM pages_inherit
),

— ページの基本情報
pages_info AS (
SELECT
p.view_id,
p.page_idx,
COALESCE(
NULLIF((xpath(‘string(./@string)’, p.page_node))[1]::text,”),
NULLIF((xpath(‘string(./@name)’, p.page_node))[1]::text,”)
) AS tab_label_default,
(xpath(‘string(./@name)’, p.page_node))[1]::text AS page_name_attr,
(COALESCE(array_length(xpath(‘.//tree’, p.page_node), 1), 0) > 0) AS has_inline_tree
FROM pages_all p
),

— ★ 修正点:多言語ラベルの取得を「動的XPath文字列の組み立て」で行う(変数バインドは不可)
labels_ja AS (
SELECT
m.view_id,
pf.page_idx,
COALESCE(
CASE WHEN m.x_ja IS NOT NULL AND NULLIF(pf.page_name_attr,”) IS NOT NULL THEN
(xpath(
‘string((//form//notebook/page[@name=’
|| quote_literal(pf.page_name_attr) || ‘]/@string)[1])’,
m.x_ja
))[1]::text
END,
pf.tab_label_default
) AS tab_label_ja
FROM xml_main m
JOIN pages_info pf ON pf.view_id=m.view_id
),
labels_en AS (
SELECT
m.view_id,
pf.page_idx,
COALESCE(
CASE WHEN m.x_en IS NOT NULL AND NULLIF(pf.page_name_attr,”) IS NOT NULL THEN
(xpath(
‘string((//form//notebook/page[@name=’
|| quote_literal(pf.page_name_attr) || ‘]/@string)[1])’,
m.x_en
))[1]::text
END,
pf.tab_label_default
) AS tab_label_en
FROM xml_main m
JOIN pages_info pf ON pf.view_id=m.view_id
),

— sale.order の one2many 定義
o2m_defs AS (
SELECT
mf.name AS field_name,
mf.relation AS child_model,
mf.relation_field AS child_link_field
FROM ir_model_fields mf
JOIN param p ON p.model = mf.model
WHERE mf.ttype = ‘one2many’
),

— ページ内の field 群
page_fields AS (
SELECT
p.view_id,
p.page_idx,
f.field_node,
portal_stage.attr_val_hard(f.field_node, ‘name’) AS field_name
FROM pages_all p
CROSS JOIN LATERAL unnest(xpath(‘.//field[@name]’, p.page_node)) AS f(field_node)
),

— one2many に該当する field のみ注釈(代表=先頭)
page_o2m AS (
SELECT
pf.view_id, pf.page_idx, pf.field_node, pf.field_name,
d.child_model, d.child_link_field,
— 編集動作/オプション系
NULLIF((xpath(‘string(.//tree/@editable)’, pf.field_node))[1]::text,”) AS tree_editable,
NULLIF((xpath(‘string(.//tree/@create)’, pf.field_node))[1]::text,”) AS tree_create,
NULLIF((xpath(‘string(.//tree/@delete)’, pf.field_node))[1]::text,”) AS tree_delete,
NULLIF((xpath(‘string(./@options)’, pf.field_node))[1]::text,”) AS options_raw,
— view_mode / domain / context
NULLIF((xpath(‘string(./@mode)’, pf.field_node))[1]::text,”) AS view_mode,
NULLIF((xpath(‘string(./@domain)’, pf.field_node))[1]::text,”) AS domain_raw,
NULLIF((xpath(‘string(./@context)’, pf.field_node))[1]::text,”) AS context_raw,
— 参照ビュー(context 内の _view_ref) CASE WHEN (xpath(‘string(./@context)’, pf.field_node))[1]::text ~ ‘tree_view_ref’ THEN COALESCE( regexp_replace((xpath(‘string(./@context)’, pf.field_node))[1]::text, ‘(?s).tree_view_ref\s:\s”([^”]+)”.‘, ‘\1’), regexp_replace((xpath(‘string(./@context)’, pf.field_node))[1]::text, ‘(?s).tree_view_ref\s:\s“([^”]+)”.‘, ‘\1’) ) END AS tree_view_xmlid, CASE WHEN (xpath(‘string(./@context)’, pf.field_node))[1]::text ~ ‘form_view_ref’ THEN COALESCE( regexp_replace((xpath(‘string(./@context)’, pf.field_node))[1]::text, ‘(?s).form_view_ref\s:\s”([^”]+)”.‘, ‘\1’), regexp_replace((xpath(‘string(./@context)’, pf.field_node))[1]::text, ‘(?s).form_view_ref\s:\s“([^”]+)”.*’, ‘\1’)
)
END AS form_view_xmlid
FROM page_fields pf
LEFT JOIN o2m_defs d
ON pf.field_name = d.field_name
OR pf.field_name = d.field_name || ‘_ids’
),
page_o2m_first AS ( — 代表 one2many(先頭)を採用
SELECT DISTINCT ON (view_id, page_idx)
view_id, page_idx,
field_name, child_model, child_link_field,
tree_editable, tree_create, tree_delete, options_raw,
view_mode, domain_raw, context_raw,
tree_view_xmlid, form_view_xmlid
FROM page_o2m
WHERE child_model IS NOT NULL
ORDER BY view_id, page_idx, field_name
),

— 最終行の整形
tab_rows AS (
SELECT
pi.view_id,
pi.page_idx,
— キー:nameがあれば name:、無ければ page:
COALESCE(‘name:’||NULLIF(pi.page_name_attr,”), ‘page:’||pi.page_idx::text) AS tab_key,

-- タブ名(日英)
COALESCE(lj.tab_label_ja, pi.tab_label_default) AS tab_label_ja,
COALESCE(le.tab_label_en, pi.tab_label_default) AS tab_label_en,

-- 出自
vo.origin, vo.module,

-- 子モデル(代表)
p1.child_model, p1.child_link_field,

-- サブビュー:ビューモードと参照先
COALESCE(NULLIF(p1.view_mode,''), 'tree,form') AS view_mode,
p1.tree_view_xmlid, p1.form_view_xmlid,
CASE WHEN p1.tree_view_xmlid IS NOT NULL THEN 'reference'::portal_subview_policy
     WHEN pi.has_inline_tree                  THEN 'auto'::portal_subview_policy
     ELSE 'none'::portal_subview_policy END  AS subview_policy_tree,
CASE WHEN p1.form_view_xmlid IS NOT NULL THEN 'reference'::portal_subview_policy
     ELSE 'none'::portal_subview_policy END  AS subview_policy_form,

-- 表示制御(要否+値)
(p1.domain_raw  IS NOT NULL) AS use_domain,
p1.domain_raw,
(p1.context_raw IS NOT NULL) AS use_context,
p1.context_raw,

-- 編集動作(インライン/行追加/削除)
(NULLIF(p1.tree_editable,'') IS NOT NULL) AS inline_edit,
CASE
  WHEN p1.tree_create='false' THEN FALSE
  WHEN p1.options_raw ~* '\bno_create\b\s*:\s*(1|true)\b' THEN FALSE
  ELSE TRUE
END AS allow_create_rows,
CASE
  WHEN p1.tree_delete='false' THEN FALSE
  WHEN p1.options_raw ~* '\bno_delete\b\s*:\s*(1|true)\b' THEN FALSE
  ELSE TRUE
END AS allow_delete_rows,
p1.options_raw

FROM pages_info pi
LEFT JOIN page_o2m_first p1 ON p1.view_id=pi.view_id AND p1.page_idx=pi.page_idx
LEFT JOIN labels_ja lj ON lj.view_id=pi.view_id AND lj.page_idx=pi.page_idx
LEFT JOIN labels_en le ON le.view_id=pi.view_id AND le.page_idx=pi.page_idx
LEFT JOIN view_origin vo ON vo.view_id=pi.view_id
)

— ★ UPSERT:portal_tab_portal へ投入
INSERT INTO portal_tab_portal (
view_id, tab_key, page_idx,
tab_label_ja, tab_label_en,
child_model, child_link_field,
origin, module,
is_codegen_target, notes, github_url,
view_mode, tree_view_xmlid, form_view_xmlid,
subview_policy_tree, subview_policy_form,
use_domain, domain_raw, use_context, context_raw,
inline_edit, allow_create_rows, allow_delete_rows, options_raw
)
SELECT
t.view_id, t.tab_key, t.page_idx,
NULLIF(t.tab_label_ja,”), NULLIF(t.tab_label_en,”),
t.child_model, t.child_link_field,
COALESCE(t.origin,’standard’)::portal_origin, t.module,
FALSE, NULL, NULL, — is_codegen_target / notes / github_url は初期空
t.view_mode, t.tree_view_xmlid, t.form_view_xmlid,
t.subview_policy_tree, t.subview_policy_form,
t.use_domain, t.domain_raw, t.use_context, t.context_raw,
t.inline_edit, t.allow_create_rows, t.allow_delete_rows, t.options_raw
FROM tab_rows t
ON CONFLICT (view_id, tab_key) DO UPDATE SET
page_idx = EXCLUDED.page_idx,
tab_label_ja = EXCLUDED.tab_label_ja,
tab_label_en = EXCLUDED.tab_label_en,
child_model = EXCLUDED.child_model,
child_link_field = EXCLUDED.child_link_field,
origin = EXCLUDED.origin,
module = EXCLUDED.module,
view_mode = EXCLUDED.view_mode,
tree_view_xmlid = EXCLUDED.tree_view_xmlid,
form_view_xmlid = EXCLUDED.form_view_xmlid,
subview_policy_tree = EXCLUDED.subview_policy_tree,
subview_policy_form = EXCLUDED.subview_policy_form,
use_domain = EXCLUDED.use_domain,
domain_raw = EXCLUDED.domain_raw,
use_context = EXCLUDED.use_context,
context_raw = EXCLUDED.context_raw,
inline_edit = EXCLUDED.inline_edit,
allow_create_rows = EXCLUDED.allow_create_rows,
allow_delete_rows = EXCLUDED.allow_delete_rows,
options_raw = EXCLUDED.options_raw,
updated_at = now();

ポータル「タブ」入力ガイド(コンサル向け)

このレコードが表すもの

  • 対象:Odoo フォームビュー内の 1 つのタブ(<notebook><page>
  • 主目的:ポータル上で明細タブの表示・編集・遷移を宣言的に管理し、必要ならXMLを自動生成できる状態にする

入力項目と記入ルール

1) タブ名(日本語 / 英語)

  • 何を書く:画面に表示するタブラベル。
  • 形式:自由テキスト。和英どちらも推奨(英語のみでも可)。
  • 自動取得の目安
    • 既定:ir_ui_view<page string="...">
    • 代替:<page name="...">(継承で attribute name="string" 上書きの可能性あり)
  • コツ:業務用語をそのまま/句読点は最小限。例)受注明細 / Order Lines

2) 子モデル

  • 何を書く:このタブで扱う明細モデル名。例)sale.order.line
  • 形式:モデルのテクニカル名(ドット区切り)。
  • 自動取得の目安
    • <page> 配下の one2many フィールドの relation(例:order_linesale.order.line)。
    • 複数ある場合は「主タブ」に相当するもの(通常は最上段/最大件数)を選択。
  • 注意:セットしたら リンクフィールド も埋める(次項)。

3) リンクフィールド(子側の Many2one 名)

  • 何を書く:子モデル側で親を指す Many2one フィールド名。例)order_id
  • 形式:フィールド名(スネークケース)。
  • 自動取得の目安ir_model_fields.relation_fieldone2many の逆参照)。

4) 出自(origin)

  • 何を書く:このタブ定義の出どころ。
  • 選択肢standard(標準), module(カスタムモジュール), studio(Web Studio), portal(本ポータル起点), unknown
  • 自動取得の目安ir_model_data(module)web_studio なら studio、値ありなら module、なしは standard

5) コード生成対象(トグル)

  • 何を書く:このタブをXML自動生成の対象にするか
  • 推奨:運用で固定化するタブは ON、暫定や検証中は OFF。
  • 影響:ONにすると「生成ジョブ(開発者依頼)」の対象に載ります。

6) 備考

  • 何を書く:要件・設計意図・注意事項、関連チケット番号など。
  • 形式:自由テキスト(短く箇条書き推奨)。

サブビュー & 遷移

7) ビューモード

  • 何を書くtree / form / tree,form
  • 決め方
    • 一覧のみで十分 → tree
    • 既存フォームに遷移したい → tree,form
    • 行クリックでフォーム不要 → tree + editable で代替

8) サブビュー(参照 or 生成方針)

  • 何を書く参照先ビューXMLIDまたは生成ポリシー
    • tree_view_xmlid 例:sale.view_order_line_tree
    • form_view_xmlid 例:sale.view_order_line_form
    • 生成ポリシー:reference(参照優先) / auto(インライン構造から自動生成) / none
  • 自動取得の目安one2manycontexttree_view_ref / form_view_ref があれば参照可。
  • ルールview_modetree が含まれるなら ツリー参照 or 生成のいずれかは必須。

表示制御 & 既定値

9) 表示制御(Domain / Context の要否)

  • 何を書く:フィルタ条件(domain)と、既定値/挙動(context)。
  • 形式
    • domain:Odoo ドメイン式の文字列
      • 例)[('state','!=','cancel')]
    • context:辞書式の文字列
      • 例){'default_order_id': active_id, 'search_default_confirmed': 1}
  • 自動取得の目安<field name="...">domain / context 属性。
  • 注意use_domain / use_context は値が入っているときに ON(空なら OFF)。

編集動作

10) インライン編集(editable)

  • 何を書く:ツリーでのインライン編集可否。
  • 入力例editable="bottom"(末尾に追加行 / 行内編集可)
  • 自動取得の目安<tree editable="...">
  • ヒント:伝票明細は bottom が一般的。承認済は OFF などの運用ルールを備考に。

11) 行の追加/削除可否

  • 何を書く:行追加・削除の許容。
  • 決め方
    • 追加:optionsno_create: True があれば 不可
    • 削除:optionsno_delete: True があれば 不可
    • 直接属性:<tree create="false" delete="false"> があればそれを優先
  • 自動取得の目安<tree @create/@delete><field @options> の両方を見て合成。

12) GitHub リンク

  • 何を書く:参照モジュールの XML / Python の永続URL(コミット or ブランチ固定)。
  • 用途:差分・レビューの導線確保。origin=module/code の場合は必須推奨。

入力チェック(クイックルール)

  • 必須
    • タブ名(少なくとも英語または日本語)
    • 子モデル+リンクフィールド(明細タブの場合)
    • ビューモード
  • 一貫性
    • view_modetree がある → tree_view_xmlid 参照 or 生成ポリシー auto をセット
    • ドメインのフィールド名は子モデル側のフィールドで書く(親の state を参照するなら domain 側で parent.state などに注意)
  • 権限:ビュー参照先にグループ制限がある場合は備考に記載(実行時の404/権限不足を避ける)

画面項目 ↔ DB カラム対応(実装メモ)

画面項目portal_tab のカラム
タブ名(日本語 / 英語)tab_label_ja / tab_label_en
子モデルchild_model
リンクフィールドchild_link_field
出自origin(+ module
コード生成対象is_codegen_target
備考notes
GitHub リンクgithub_url
ビューモードview_mode(例:tree,form
生成方針(tree/form)subview_policy_tree / subview_policy_formreference / auto / none
Domain 要否 / 値use_domain / domain_raw
Context 要否 / 値use_context / context_raw
インライン編集inline_edit
行追加可否allow_create_rows
行削除可否allow_delete_rows

入力例(sale.order の「受注明細」タブの想定)

  • タブ名(日/英):受注明細 / Order Lines
  • 子モデル:sale.order.line
  • リンクフィールド:order_id
  • 出自:standard
  • コード生成対象:ON
  • ビューモード:tree,form
  • サブビュー:
    • ツリー参照:sale.view_order_line_tree(policy=reference
    • フォーム参照:sale.view_order_line_form(policy=reference
  • 表示制御:
    • Domain:[('state','!=','cancel')]
    • Context:{'default_order_id': active_id}
  • 編集動作:
    • インライン編集:ON(editable="bottom" 相当)
    • 行追加:可、行削除:可
  • GitHub:https://github.com/<org>/<repo>/.../views/sale_order_views.xml#L...
  • 備考:承認後は編集不可。FY25から税計算は外部連携。

Comments

コメントを残す

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