/** * 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 implimentation Neo4j 関係をコードに焼きこみ/検証 – Raqqa

Odoo implimentation Neo4j 関係をコードに焼きこみ/検証

テキストを焼きこむ方法を順を追ってやる必要があるかもしれない。つまりエージェントに少しずつ。一気にやるんじゃなくて。

はい。
一気にやらせるのは難しいです。しかも今回のようなコードは、単に長いだけでなく、

  • 作成対象ラベルが多い
  • セクションごとに依存関係がある
  • MATCH 前提の接続部がある
  • 最後に検閲クエリまで含んでいる

ので、一発生成より分割生成のほうが現実的です。

特に今回のスクリプトは、すでに構造が章立てされています。

  • 制約・インデックス
  • ImplementationScope
  • Model Specs
  • BusinessElement / DomainGuide 接続
  • Field Specs
  • View / Tab / Button / Smart Button
  • Action / Menu
  • Security / Compute / Onchange / Constraint / Report
  • Mapping Rules
  • CodeGenerationUnit
  • 確認用 Cypher

というまとまりになっているので、この単位で分けるのが自然です。

さらに、key 一意制約と MERGE が多いので、分割して再実行しやすい設計でもあります。
これは再焼き込みループ向きです。

ただし接続部は注意が必要です。
BusinessElementDomainGuideTopic への接続は MATCH 前提なので、前段ノードが無ければそのリンクは作られません。
ここが一括生成を不安定にするポイントです。

おすすめの進め方

一発で「最終Cypher全文」を作らせるのではなく、3段階に分けるのがよいです。

1. まず設計を作る

最初にコードではなく、生成対象の一覧だけ作ります。

  • どの ModelSpec を作るか
  • どの FieldSpec をどの model にぶら下げるか
  • どの View / Button / Compute / Report が必要か
  • どの DomainGuideTopic と接続するか

ここは JSON や YAML でもよいです。

2. 次に単位ごとに Cypher 化する

おすすめの単位はこれです。

  • A: bootstrap
    制約 + ImplementationScope
  • B: model
    OdooModelSpec
  • C: field
    OdooFieldSpec
  • D: ui
    OdooViewSpec, OdooTabSpec, OdooButtonSpec, OdooSmartButtonSpec, OdooActionSpec, OdooMenuSpec
  • E: logic
    OdooSecurityRuleSpec, OdooComputeSpec, OdooOnchangeSpec, OdooConstraintSpec, OdooReportSpec
  • F: trace
    DomainGuideMappingRule, CodeGenerationUnit, GENERATES_*
  • G: validation
    確認用Cypher

3. 各単位ごとに焼いて検閲する

1ユニット焼いたら、その場で

  • 件数確認
  • 孤立検出
  • 必須リンク欠落
  • upstream 参照欠落

を見ます。

実務上の考え方

Odoo 実装で大事なのは「全部一気に書けるか」ではなく、

壊れたときに、どの層で壊れたかを切り分けられるか

です。

たとえば、

  • Model は作れた
  • Field は作れた
  • でも DomainGuideTopic -> IMPLEMENTED_BY_MODEL が付かない

なら、問題は接続層です。
こういう切り分けは、分割していないとかなり難しいです。

なので結論

長大なCypherを一発で生成させる運用はおすすめしません。
おすすめは、

  • 設計定義
  • 小さなCypher生成
  • 焼き込み
  • 検閲
  • 必要部分だけ再焼き込み

のループです。

今回の Phase12 の形は、むしろその運用に向いています。
章立てされていて、MERGE と一意制約が多く、最後に確認用クエリまであるからです。

次は、あなたのこの Phase12 を前提にして、
「どの単位でLLMに生成させるか」の分割テンプレートを作るのが一番実用的です。

Phase12:Odoo実装仕様Graph Cypher

0. このCypherで作るもの

OdooModelSpec
OdooFieldSpec
OdooViewSpec
OdooTabSpec
OdooButtonSpec
OdooSmartButtonSpec
OdooMenuSpec
OdooActionSpec
OdooSecurityRuleSpec
OdooComputeSpec
OdooOnchangeSpec
OdooConstraintSpec
OdooReportSpec
DomainGuideMappingRule
CodeGenerationUnit

1. 制約・インデックス

// =====================================================
// Phase12 Odoo Implementation Spec Graph
// Constraints / Indexes
// =====================================================CREATE CONSTRAINT odoo_model_spec_key IF NOT EXISTS
FOR (n:OdooModelSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_field_spec_key IF NOT EXISTS
FOR (n:OdooFieldSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_view_spec_key IF NOT EXISTS
FOR (n:OdooViewSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_tab_spec_key IF NOT EXISTS
FOR (n:OdooTabSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_button_spec_key IF NOT EXISTS
FOR (n:OdooButtonSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_smart_button_spec_key IF NOT EXISTS
FOR (n:OdooSmartButtonSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_menu_spec_key IF NOT EXISTS
FOR (n:OdooMenuSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_action_spec_key IF NOT EXISTS
FOR (n:OdooActionSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_security_rule_spec_key IF NOT EXISTS
FOR (n:OdooSecurityRuleSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_compute_spec_key IF NOT EXISTS
FOR (n:OdooComputeSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_onchange_spec_key IF NOT EXISTS
FOR (n:OdooOnchangeSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_constraint_spec_key IF NOT EXISTS
FOR (n:OdooConstraintSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT odoo_report_spec_key IF NOT EXISTS
FOR (n:OdooReportSpec)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT domain_guide_mapping_rule_key IF NOT EXISTS
FOR (n:DomainGuideMappingRule)
REQUIRE n.key IS UNIQUE;CREATE CONSTRAINT code_generation_unit_key IF NOT EXISTS
FOR (n:CodeGenerationUnit)
REQUIRE n.key IS UNIQUE;

2. 実装スコープ定義

// =====================================================
// Implementation Scope
// =====================================================MERGE (scope:ImplementationScope {key:"purchase_module_odoo_impl_v1"})
SET
scope.name = "Odoo購買モジュール実装仕様 v1",
scope.description = "Dynamics購買設計書をOdoo購買モジュールに適用するための実装仕様Graph",
scope.scope = "purchase_module",
scope.status = "draft",
scope.confidence = "assumed",
scope.source = "Phase1-11 Graph / DomainGuide / 想定回答",
scope.updated_at = datetime();

3. Odooモデル仕様

3-1. 標準モデル拡張

// =====================================================
// Odoo Model Specs - Standard Model Extensions
// =====================================================UNWIND [
{
key:"model_purchase_order_ext",
model:"purchase.order",
name:"Purchase Order Extension",
mode:"inherit",
module:"purchase",
description:"発注量予実チェック、輸入情報、価格協定参照、承認状態を発注書に表示するための標準モデル拡張",
reason:"発注書はOdoo購買の中核であり、業務ルールの結果を表示する器として拡張する"
},
{
key:"model_purchase_order_line_ext",
model:"purchase.order.line",
name:"Purchase Order Line Extension",
mode:"inherit",
module:"purchase",
description:"明細単位で予定数量、実績数量、差異率、価格協定情報を確認するための拡張",
reason:"発注量予実チェックは品目・明細単位で確認する必要があるため"
},
{
key:"model_product_template_ext",
model:"product.template",
name:"Product Template Purchase Extension",
mode:"inherit",
module:"product",
description:"重要品目、標準差異閾値、メーカー、発注制約を保持するための拡張",
reason:"発注量チェックや仕入先選定は品目属性に依存するため"
},
{
key:"model_product_supplierinfo_ext",
model:"product.supplierinfo",
name:"Supplier Info Price Agreement Extension",
mode:"inherit",
module:"product",
description:"価格協定番号、正式価格協定フラグ、輸入条件、数量階層を補足するための拡張",
reason:"価格協定は正式な購買単価マスタとして扱うため"
},
{
key:"model_res_partner_ext",
model:"res.partner",
name:"Vendor Purchase Condition Extension",
mode:"inherit",
module:"contacts",
description:"仕入先・メーカー区分、購買条件、標準リードタイム、取扱制約を補足するための拡張",
reason:"仕入先とメーカーを別概念として扱い、購買条件を整理するため"
}
] AS row
MERGE (m:OdooModelSpec {key: row.key})
SET
m.model = row.model,
m.name = row.name,
m.mode = row.mode,
m.module = row.module,
m.description = row.description,
m.reason = row.reason,
m.scope = "purchase_module",
m.status = "draft",
m.confidence = "assumed",
m.updated_at = datetime()
WITH m
MATCH (scope:ImplementationScope {key:"purchase_module_odoo_impl_v1"})
MERGE (scope)-[:HAS_MODEL_SPEC]->(m);

3-2. カスタムモデル

// =====================================================
// Odoo Model Specs - Custom Models
// =====================================================UNWIND [
{
key:"model_purchase_quantity_check",
model:"purchase.quantity.check",
name:"Purchase Quantity Check",
mode:"new",
module:"purchase_quantity_control",
description:"発注量予実チェックの実行単位を管理するカスタムモデル",
reason:"発注書に簡易結果を残し、詳細はログモデルに残す方針のため"
},
{
key:"model_purchase_quantity_check_line",
model:"purchase.quantity.check.line",
name:"Purchase Quantity Check Line",
mode:"new",
module:"purchase_quantity_control",
description:"発注量予実チェックの明細単位の差異結果を管理するカスタムモデル",
reason:"予定・実績・差異・閾値・判定結果は品目明細単位で保持する必要があるため"
},
{
key:"model_purchase_decision_rule",
model:"purchase.decision.rule",
name:"Purchase Decision Rule",
mode:"new",
module:"purchase_quantity_control",
description:"発注判断に使うDomainGuide由来ルールをOdoo側で参照するためのモデル",
reason:"DomainGuide条件をOdoo画面で参照・説明可能にするため"
},
{
key:"model_purchase_import_info",
model:"purchase.import.info",
name:"Purchase Import Info",
mode:"new",
module:"purchase_import_report",
description:"輸入レポートに必要な補足情報を発注書に紐づけて管理するモデル",
reason:"輸入レポートは発注書データを基礎としつつ輸入条件などの補足が必要なため"
},
{
key:"model_purchase_assumption_answer",
model:"purchase.assumption.answer",
name:"Purchase Assumption Answer",
mode:"new",
module:"purchase_domain_guide_bridge",
description:"質問・回答・Decisionの履歴をOdoo側で参照するための補助モデル",
reason:"Graph上の想定回答や確定回答をOdoo画面から確認できるようにするため"
}
] AS row
MERGE (m:OdooModelSpec {key: row.key})
SET
m.model = row.model,
m.name = row.name,
m.mode = row.mode,
m.module = row.module,
m.description = row.description,
m.reason = row.reason,
m.scope = "purchase_module",
m.status = "draft",
m.confidence = "assumed",
m.updated_at = datetime()
WITH m
MATCH (scope:ImplementationScope {key:"purchase_module_odoo_impl_v1"})
MERGE (scope)-[:HAS_MODEL_SPEC]->(m);

4. 業務要素・DomainGuideとの接続

// =====================================================
// Connect BusinessElement / DomainGuide to Model Specs
// =====================================================UNWIND [
{be:"purchase_order", model:"model_purchase_order_ext"},
{be:"all_purchase_orders", model:"model_purchase_order_ext"},
{be:"purchase_order", model:"model_purchase_order_line_ext"},
{be:"vendor", model:"model_res_partner_ext"},
{be:"manufacturer", model:"model_res_partner_ext"},
{be:"manufacturer", model:"model_product_template_ext"},
{be:"purchase_quantity_variance_check", model:"model_purchase_quantity_check"},
{be:"purchase_quantity_variance_check", model:"model_purchase_quantity_check_line"},
{be:"import_report", model:"model_purchase_import_info"},
{be:"mtx_mx_material_management_status", model:"model_purchase_decision_rule"},
{be:"mtx_mx_material_management_status_editable", model:"model_purchase_decision_rule"}
] AS row
MATCH (be:BusinessElement {key: row.be})
MATCH (m:OdooModelSpec {key: row.model})
MERGE (be)-[:GENERATES_MODEL_SPEC]->(m);UNWIND [
{topic:"purchase_quantity_variance", model:"model_purchase_quantity_check"},
{topic:"purchase_quantity_variance", model:"model_purchase_quantity_check_line"},
{topic:"price_agreement", model:"model_product_supplierinfo_ext"},
{topic:"vendor_selection", model:"model_res_partner_ext"},
{topic:"vendor_selection", model:"model_product_template_ext"},
{topic:"purchase_report_policy", model:"model_purchase_import_info"},
{topic:"mtx_mx_decomposition", model:"model_purchase_decision_rule"},
{topic:"receipt_connection_policy", model:"model_purchase_order_ext"},
{topic:"billing_connection_policy", model:"model_purchase_order_ext"}
] AS row
MATCH (topic:DomainGuideTopic {key: row.topic})
MATCH (m:OdooModelSpec {key: row.model})
MERGE (topic)-[:IMPLEMENTED_BY_MODEL]->(m);

5. フィールド仕様

5-1. purchase.order 拡張フィールド

// =====================================================
// Field Specs - purchase.order
// =====================================================UNWIND [
{
key:"field_po_x_check_status",
model_key:"model_purchase_order_ext",
name:"x_check_status",
label:"発注量チェック状態",
ttype:"selection",
required:false,
readonly:true,
store:true,
selection:"not_checked:未チェック,ok:問題なし,warning:警告,approval_required:承認必要",
reason:"発注量予実チェックの状態を発注書ヘッダーで確認するため"
},
{
key:"field_po_x_has_variance_warning",
model_key:"model_purchase_order_ext",
name:"x_has_variance_warning",
label:"差異警告あり",
ttype:"boolean",
required:false,
readonly:true,
store:true,
selection:null,
reason:"発注量に差異がある発注書を一覧や検索で抽出するため"
},
{
key:"field_po_x_requires_manager_approval",
model_key:"model_purchase_order_ext",
name:"x_requires_manager_approval",
label:"責任者承認要",
ttype:"boolean",
required:false,
readonly:true,
store:true,
selection:null,
reason:"大きな差異または重要品目により責任者承認が必要な発注を識別するため"
},
{
key:"field_po_x_variance_summary",
model_key:"model_purchase_order_ext",
name:"x_variance_summary",
label:"発注量チェック要約",
ttype:"text",
required:false,
readonly:true,
store:true,
selection:null,
reason:"発注量予実チェック結果の要約を発注書に残すため"
},
{
key:"field_po_x_quantity_check_count",
model_key:"model_purchase_order_ext",
name:"x_quantity_check_count",
label:"発注量チェック件数",
ttype:"integer",
required:false,
readonly:true,
store:false,
selection:null,
reason:"スマートボタンに発注量チェック履歴件数を表示するため"
},
{
key:"field_po_x_import_required",
model_key:"model_purchase_order_ext",
name:"x_import_required",
label:"輸入取引",
ttype:"boolean",
required:false,
readonly:false,
store:true,
selection:null,
reason:"一部輸入取引・外貨建て発注を識別するため"
},
{
key:"field_po_x_manual_price_change_reason",
model_key:"model_purchase_order_ext",
name:"x_manual_price_change_reason",
label:"単価変更理由",
ttype:"text",
required:false,
readonly:false,
store:true,
selection:null,
reason:"価格協定と異なる単価を使う場合の理由を残すため"
}
] AS row
MERGE (f:OdooFieldSpec {key: row.key})
SET
f.name = row.name,
f.label = row.label,
f.ttype = row.ttype,
f.required = row.required,
f.readonly = row.readonly,
f.store = row.store,
f.selection = row.selection,
f.reason = row.reason,
f.scope = "purchase_module",
f.status = "draft",
f.confidence = "assumed",
f.updated_at = datetime()
WITH f, row
MATCH (m:OdooModelSpec {key: row.model_key})
MERGE (m)-[:HAS_FIELD]->(f);

5-2. purchase.order.line 拡張フィールド

// =====================================================
// Field Specs - purchase.order.line
// =====================================================UNWIND [
{
key:"field_pol_x_planned_qty",
model_key:"model_purchase_order_line_ext",
name:"x_planned_qty",
label:"予定数量",
ttype:"float",
required:false,
readonly:true,
store:true,
reason:"生産計画に基づく原材料使用予定を明細単位で保持・表示するため"
},
{
key:"field_pol_x_actual_qty",
model_key:"model_purchase_order_line_ext",
name:"x_actual_qty",
label:"実績数量",
ttype:"float",
required:false,
readonly:true,
store:true,
reason:"発注実績・入荷実績を明細単位で参照するため"
},
{
key:"field_pol_x_variance_qty",
model_key:"model_purchase_order_line_ext",
name:"x_variance_qty",
label:"差異数量",
ttype:"float",
required:false,
readonly:true,
store:true,
reason:"予定数量と実績数量の差を明細単位で表示するため"
},
{
key:"field_pol_x_variance_rate",
model_key:"model_purchase_order_line_ext",
name:"x_variance_rate",
label:"差異率",
ttype:"float",
required:false,
readonly:true,
store:true,
reason:"標準閾値±10%などの判定に使うため"
},
{
key:"field_pol_x_variance_judgement",
model_key:"model_purchase_order_line_ext",
name:"x_variance_judgement",
label:"差異判定",
ttype:"selection",
required:false,
readonly:true,
store:true,
reason:"明細単位でOK、警告、承認必要を判定するため"
},
{
key:"field_pol_x_price_agreement_no",
model_key:"model_purchase_order_line_ext",
name:"x_price_agreement_no",
label:"価格協定番号",
ttype:"char",
required:false,
readonly:true,
store:true,
reason:"発注明細に適用された価格協定を確認するため"
},
{
key:"field_pol_x_manual_price_changed",
model_key:"model_purchase_order_line_ext",
name:"x_manual_price_changed",
label:"単価手動変更",
ttype:"boolean",
required:false,
readonly:true,
store:true,
reason:"価格協定から取得した単価と異なる単価を使っているか識別するため"
}
] AS row
MERGE (f:OdooFieldSpec {key: row.key})
SET
f.name = row.name,
f.label = row.label,
f.ttype = row.ttype,
f.required = row.required,
f.readonly = row.readonly,
f.store = row.store,
f.reason = row.reason,
f.scope = "purchase_module",
f.status = "draft",
f.confidence = "assumed",
f.updated_at = datetime()
WITH f, row
MATCH (m:OdooModelSpec {key: row.model_key})
MERGE (m)-[:HAS_FIELD]->(f);

5-3. product.template / supplierinfo / partner フィールド

// =====================================================
// Field Specs - Product / SupplierInfo / Partner
// =====================================================UNWIND [
{
key:"field_product_x_is_critical_material",
model_key:"model_product_template_ext",
name:"x_is_critical_material",
label:"重要品目",
ttype:"boolean",
required:false,
readonly:false,
store:true,
reason:"重要品目では発注量予実チェックを必須にするため"
},
{
key:"field_product_x_default_variance_threshold",
model_key:"model_product_template_ext",
name:"x_default_variance_threshold",
label:"標準差異閾値",
ttype:"float",
required:false,
readonly:false,
store:true,
reason:"品目ごとに発注量予実チェックの閾値を上書きするため"
},
{
key:"field_product_x_manufacturer_id",
model_key:"model_product_template_ext",
name:"x_manufacturer_id",
label:"メーカー",
ttype:"many2one",
relation:"res.partner",
required:false,
readonly:false,
store:true,
reason:"仕入先とメーカーを別概念として管理するため"
},
{
key:"field_product_x_purchase_rounding_qty",
model_key:"model_product_template_ext",
name:"x_purchase_rounding_qty",
label:"発注丸め数量",
ttype:"float",
required:false,
readonly:false,
store:true,
reason:"最低発注数量・発注単位・丸め数量に対応するため"
},
{
key:"field_supplierinfo_x_price_agreement_no",
model_key:"model_product_supplierinfo_ext",
name:"x_price_agreement_no",
label:"価格協定番号",
ttype:"char",
required:false,
readonly:false,
store:true,
reason:"価格協定を正式な購買単価マスタとして識別するため"
},
{
key:"field_supplierinfo_x_is_official_agreement",
model_key:"model_product_supplierinfo_ext",
name:"x_is_official_agreement",
label:"正式価格協定",
ttype:"boolean",
required:false,
readonly:false,
store:true,
reason:"正式な価格協定か参考情報かを区別するため"
},
{
key:"field_supplierinfo_x_incoterm_note",
model_key:"model_product_supplierinfo_ext",
name:"x_incoterm_note",
label:"輸入条件補足",
ttype:"char",
required:false,
readonly:false,
store:true,
reason:"輸入レポートに必要な輸入条件・通貨・インコタームズ相当情報を補足するため"
},
{
key:"field_partner_x_is_manufacturer",
model_key:"model_res_partner_ext",
name:"x_is_manufacturer",
label:"メーカー",
ttype:"boolean",
required:false,
readonly:false,
store:true,
reason:"仕入先とメーカーを別概念として扱うため"
},
{
key:"field_partner_x_standard_lead_time",
model_key:"model_res_partner_ext",
name:"x_standard_lead_time",
label:"標準リードタイム",
ttype:"integer",
required:false,
readonly:false,
store:true,
reason:"仕入先ごとの標準納期条件を管理するため"
}
] AS row
MERGE (f:OdooFieldSpec {key: row.key})
SET
f.name = row.name,
f.label = row.label,
f.ttype = row.ttype,
f.relation = row.relation,
f.required = row.required,
f.readonly = row.readonly,
f.store = row.store,
f.reason = row.reason,
f.scope = "purchase_module",
f.status = "draft",
f.confidence = "assumed",
f.updated_at = datetime()
WITH f, row
MATCH (m:OdooModelSpec {key: row.model_key})
MERGE (m)-[:HAS_FIELD]->(f);

5-4. カスタムモデルのフィールド

// =====================================================
// Field Specs - purchase.quantity.check / lines
// =====================================================UNWIND [
{
key:"field_pqc_purchase_id",
model_key:"model_purchase_quantity_check",
name:"purchase_id",
label:"発注書",
ttype:"many2one",
relation:"purchase.order",
required:true,
readonly:false,
store:true,
reason:"発注書に紐づくチェック結果として管理するため"
},
{
key:"field_pqc_state",
model_key:"model_purchase_quantity_check",
name:"state",
label:"状態",
ttype:"selection",
selection:"draft:下書き,ok:問題なし,warning:警告,approval_required:承認必要,approved:承認済",
required:true,
readonly:false,
store:true,
reason:"チェック結果の業務状態を管理するため"
},
{
key:"field_pqc_checked_at",
model_key:"model_purchase_quantity_check",
name:"checked_at",
label:"チェック日時",
ttype:"datetime",
required:false,
readonly:true,
store:true,
reason:"発注量チェックをいつ実行したかを記録するため"
},
{
key:"field_pqc_checked_by",
model_key:"model_purchase_quantity_check",
name:"checked_by",
label:"チェック実行者",
ttype:"many2one",
relation:"res.users",
required:false,
readonly:true,
store:true,
reason:"誰がチェックを実行したかを記録するため"
},
{
key:"field_pqc_summary",
model_key:"model_purchase_quantity_check",
name:"summary",
label:"チェック要約",
ttype:"text",
required:false,
readonly:true,
store:true,
reason:"チェック結果の要約を保存するため"
},
{
key:"field_pqc_requires_approval",
model_key:"model_purchase_quantity_check",
name:"requires_approval",
label:"承認要",
ttype:"boolean",
required:false,
readonly:true,
store:true,
reason:"差異が大きい場合や重要品目の場合に責任者承認を要求するため"
},
{
key:"field_pqcl_check_id",
model_key:"model_purchase_quantity_check_line",
name:"check_id",
label:"発注量チェック",
ttype:"many2one",
relation:"purchase.quantity.check",
required:true,
readonly:false,
store:true,
reason:"チェックヘッダーに紐づく明細として管理するため"
},
{
key:"field_pqcl_product_id",
model_key:"model_purchase_quantity_check_line",
name:"product_id",
label:"品目",
ttype:"many2one",
relation:"product.product",
required:true,
readonly:false,
store:true,
reason:"品目単位で発注量予実チェックを行うため"
},
{
key:"field_pqcl_planned_qty",
model_key:"model_purchase_quantity_check_line",
name:"planned_qty",
label:"予定数量",
ttype:"float",
required:false,
readonly:true,
store:true,
reason:"生産計画に基づく原材料使用予定を保存するため"
},
{
key:"field_pqcl_actual_qty",
model_key:"model_purchase_quantity_check_line",
name:"actual_qty",
label:"実績数量",
ttype:"float",
required:false,
readonly:true,
store:true,
reason:"発注実績・入荷実績を保存するため"
},
{
key:"field_pqcl_variance_rate",
model_key:"model_purchase_quantity_check_line",
name:"variance_rate",
label:"差異率",
ttype:"float",
required:false,
readonly:true,
store:true,
reason:"閾値判定に使用するため"
},
{
key:"field_pqcl_judgement",
model_key:"model_purchase_quantity_check_line",
name:"judgement",
label:"判定",
ttype:"selection",
selection:"ok:問題なし,warning:警告,approval_required:承認必要",
required:false,
readonly:true,
store:true,
reason:"明細単位の判定結果を保持するため"
}
] AS row
MERGE (f:OdooFieldSpec {key: row.key})
SET
f.name = row.name,
f.label = row.label,
f.ttype = row.ttype,
f.relation = row.relation,
f.selection = row.selection,
f.required = row.required,
f.readonly = row.readonly,
f.store = row.store,
f.reason = row.reason,
f.scope = "purchase_module",
f.status = "draft",
f.confidence = "assumed",
f.updated_at = datetime()
WITH f, row
MATCH (m:OdooModelSpec {key: row.model_key})
MERGE (m)-[:HAS_FIELD]->(f);

6. View / Tab 仕様

// =====================================================
// View Specs
// =====================================================UNWIND [
{
key:"view_purchase_order_form_quantity_check",
model_key:"model_purchase_order_ext",
model:"purchase.order",
view_type:"form",
inherit_mode:"inherit",
target_view:"purchase.purchase_order_form",
name:"purchase.order.form.quantity.check",
reason:"発注書フォームに発注量チェック・輸入情報・価格協定参照を追加するため"
},
{
key:"view_purchase_order_line_tree_quantity_check",
model_key:"model_purchase_order_line_ext",
model:"purchase.order.line",
view_type:"tree",
inherit_mode:"inherit",
target_view:"purchase.purchase_order_line_tree",
name:"purchase.order.line.tree.quantity.check",
reason:"発注明細一覧に予定数量・実績数量・差異率を表示するため"
},
{
key:"view_purchase_quantity_check_tree",
model_key:"model_purchase_quantity_check",
model:"purchase.quantity.check",
view_type:"tree",
inherit_mode:"new",
target_view:null,
name:"purchase.quantity.check.tree",
reason:"発注量チェック結果を一覧で確認するため"
},
{
key:"view_purchase_quantity_check_form",
model_key:"model_purchase_quantity_check",
model:"purchase.quantity.check",
view_type:"form",
inherit_mode:"new",
target_view:null,
name:"purchase.quantity.check.form",
reason:"発注量チェック結果の詳細と明細を確認するため"
},
{
key:"view_product_template_purchase_control",
model_key:"model_product_template_ext",
model:"product.template",
view_type:"form",
inherit_mode:"inherit",
target_view:"product.product_template_only_form_view",
name:"product.template.form.purchase.control",
reason:"商品フォームに重要品目・閾値・メーカー・発注制約を表示するため"
},
{
key:"view_supplierinfo_price_agreement",
model_key:"model_product_supplierinfo_ext",
model:"product.supplierinfo",
view_type:"tree_form",
inherit_mode:"inherit",
target_view:"product.product_supplierinfo_tree_view",
name:"product.supplierinfo.price.agreement",
reason:"価格協定番号・正式協定・輸入条件を表示するため"
}
] AS row
MERGE (v:OdooViewSpec {key: row.key})
SET
v.model = row.model,
v.view_type = row.view_type,
v.inherit_mode = row.inherit_mode,
v.target_view = row.target_view,
v.name = row.name,
v.reason = row.reason,
v.scope = "purchase_module",
v.status = "draft",
v.confidence = "assumed",
v.updated_at = datetime()
WITH v, row
MATCH (m:OdooModelSpec {key: row.model_key})
MERGE (m)-[:HAS_VIEW]->(v);

タブ仕様

// =====================================================
// Tab Specs
// =====================================================UNWIND [
{
key:"tab_po_quantity_check",
view_key:"view_purchase_order_form_quantity_check",
name:"発注量チェック",
sequence:30,
reason:"発注量予実チェックの要約・明細・警告を確認するため"
},
{
key:"tab_po_import_info",
view_key:"view_purchase_order_form_quantity_check",
name:"輸入情報",
sequence:40,
reason:"輸入レポートに必要な輸入条件・通貨・補足情報を確認するため"
},
{
key:"tab_po_price_agreement",
view_key:"view_purchase_order_form_quantity_check",
name:"価格協定",
sequence:50,
reason:"発注書に適用された価格協定を確認するため"
},
{
key:"tab_product_purchase_constraints",
view_key:"view_product_template_purchase_control",
name:"購買制約",
sequence:60,
reason:"重要品目、閾値、最低発注数量、丸め数量を管理するため"
}
] AS row
MERGE (t:OdooTabSpec {key: row.key})
SET
t.name = row.name,
t.sequence = row.sequence,
t.visible_when = "always",
t.reason = row.reason,
t.scope = "purchase_module",
t.status = "draft",
t.confidence = "assumed",
t.updated_at = datetime()
WITH t, row
MATCH (v:OdooViewSpec {key: row.view_key})
MERGE (v)-[:HAS_TAB]->(t);

7. ボタン仕様

// =====================================================
// Button Specs
// =====================================================UNWIND [
{
key:"button_po_run_quantity_check",
view_key:"view_purchase_order_form_quantity_check",
name:"action_run_quantity_check",
label:"発注量チェック実行",
button_type:"object",
placement:"header",
visible_when:"state in ('draft','sent')",
permission:"purchase.group_purchase_user",
topic:"purchase_quantity_variance",
reason:"発注前にDomainGuideに基づく発注量チェックを実行するため"
},
{
key:"button_po_request_variance_approval",
view_key:"view_purchase_order_form_quantity_check",
name:"action_request_variance_approval",
label:"差異承認依頼",
button_type:"object",
placement:"header",
visible_when:"x_requires_manager_approval == True",
permission:"purchase.group_purchase_user",
topic:"purchase_quantity_variance",
reason:"差異が大きい場合に購買責任者へ承認依頼するため"
},
{
key:"button_po_approve_variance",
view_key:"view_purchase_order_form_quantity_check",
name:"action_approve_variance",
label:"差異承認",
button_type:"object",
placement:"header",
visible_when:"x_requires_manager_approval == True",
permission:"purchase.group_purchase_manager",
topic:"purchase_quantity_variance",
reason:"購買責任者が差異あり発注を承認するため"
},
{
key:"button_po_generate_import_report",
view_key:"view_purchase_order_form_quantity_check",
name:"action_generate_import_report",
label:"輸入レポート出力",
button_type:"object",
placement:"header",
visible_when:"x_import_required == True",
permission:"purchase.group_purchase_user",
topic:"purchase_report_policy",
reason:"輸入取引の発注書から輸入レポートを出力するため"
}
] AS row
MERGE (b:OdooButtonSpec {key: row.key})
SET
b.name = row.name,
b.label = row.label,
b.button_type = row.button_type,
b.placement = row.placement,
b.visible_when = row.visible_when,
b.permission = row.permission,
b.reason = row.reason,
b.scope = "purchase_module",
b.status = "draft",
b.confidence = "assumed",
b.updated_at = datetime()
WITH b, row
MATCH (v:OdooViewSpec {key: row.view_key})
MERGE (v)-[:HAS_BUTTON]->(b)
WITH b, row
MATCH (topic:DomainGuideTopic {key: row.topic})
MERGE (b)-[:EXECUTES_DOMAIN_GUIDE_TOPIC]->(topic);

8. スマートボタン仕様

// =====================================================
// Smart Button Specs
// =====================================================UNWIND [
{
key:"smart_po_quantity_checks",
model_key:"model_purchase_order_ext",
label:"発注量チェック",
model:"purchase.order",
target_model:"purchase.quantity.check",
count_field:"x_quantity_check_count",
action:"action_purchase_quantity_check",
reason:"発注書に紐づく発注量チェック履歴へ遷移するため"
},
{
key:"smart_po_vendor_prices",
model_key:"model_purchase_order_ext",
label:"価格協定",
model:"purchase.order",
target_model:"product.supplierinfo",
count_field:null,
action:"action_product_supplierinfo",
reason:"発注書で使用される仕入先価格・価格協定を参照するため"
},
{
key:"smart_product_purchase_history",
model_key:"model_product_template_ext",
label:"発注履歴",
model:"product.template",
target_model:"purchase.order.line",
count_field:null,
action:"action_purchase_order_line",
reason:"品目ごとの発注実績を確認するため"
}
] AS row
MERGE (sb:OdooSmartButtonSpec {key: row.key})
SET
sb.label = row.label,
sb.model = row.model,
sb.target_model = row.target_model,
sb.count_field = row.count_field,
sb.action = row.action,
sb.reason = row.reason,
sb.scope = "purchase_module",
sb.status = "draft",
sb.confidence = "assumed",
sb.updated_at = datetime()
WITH sb, row
MATCH (m:OdooModelSpec {key: row.model_key})
MERGE (m)-[:HAS_SMART_BUTTON]->(sb);

9. Action / Menu 仕様

// =====================================================
// Action Specs
// =====================================================UNWIND [
{
key:"action_purchase_quantity_check",
name:"発注量チェック",
res_model:"purchase.quantity.check",
view_mode:"tree,form",
domain:null,
context:null,
reason:"発注量チェック結果を一覧・詳細で確認するため"
},
{
key:"action_purchase_decision_rule",
name:"発注判断ルール",
res_model:"purchase.decision.rule",
view_mode:"tree,form",
domain:null,
context:null,
reason:"DomainGuide由来の発注判断ルールを確認するため"
},
{
key:"action_purchase_import_info",
name:"輸入レポート情報",
res_model:"purchase.import.info",
view_mode:"tree,form",
domain:null,
context:null,
reason:"輸入レポートに必要な補足情報を確認するため"
}
] AS row
MERGE (a:OdooActionSpec {key: row.key})
SET
a.name = row.name,
a.res_model = row.res_model,
a.view_mode = row.view_mode,
a.domain = row.domain,
a.context = row.context,
a.reason = row.reason,
a.scope = "purchase_module",
a.status = "draft",
a.confidence = "assumed",
a.updated_at = datetime();
// =====================================================
// Menu Specs
// =====================================================UNWIND [
{
key:"menu_purchase_quantity_check",
name:"発注量チェック",
parent:"purchase.menu_purchase_root",
sequence:80,
action:"action_purchase_quantity_check",
groups:"purchase.group_purchase_user",
reason:"発注量チェック結果を購買メニューから確認するため"
},
{
key:"menu_purchase_decision_rule",
name:"発注判断ルール",
parent:"purchase.menu_purchase_config",
sequence:90,
action:"action_purchase_decision_rule",
groups:"purchase.group_purchase_manager",
reason:"DomainGuide由来の発注判断ルールを管理者が確認するため"
},
{
key:"menu_purchase_import_info",
name:"輸入レポート情報",
parent:"purchase.menu_purchase_root",
sequence:95,
action:"action_purchase_import_info",
groups:"purchase.group_purchase_user",
reason:"輸入レポート関連情報を購買メニューから確認するため"
}
] AS row
MERGE (menu:OdooMenuSpec {key: row.key})
SET
menu.name = row.name,
menu.parent = row.parent,
menu.sequence = row.sequence,
menu.action = row.action,
menu.groups = row.groups,
menu.reason = row.reason,
menu.scope = "purchase_module",
menu.status = "draft",
menu.confidence = "assumed",
menu.updated_at = datetime()
WITH menu, row
MATCH (a:OdooActionSpec {key: row.action})
MERGE (menu)-[:TRIGGERS_ACTION]->(a);

10. 権限仕様

// =====================================================
// Security Rule Specs
// =====================================================UNWIND [
{
key:"access_purchase_quantity_check_user",
model_key:"model_purchase_quantity_check",
model:"purchase.quantity.check",
group:"purchase.group_purchase_user",
perm_read:true,
perm_write:false,
perm_create:false,
perm_unlink:false,
reason:"購買担当はチェック結果を参照できるが、手動編集はしない"
},
{
key:"access_purchase_quantity_check_manager",
model_key:"model_purchase_quantity_check",
model:"purchase.quantity.check",
group:"purchase.group_purchase_manager",
perm_read:true,
perm_write:true,
perm_create:true,
perm_unlink:false,
reason:"購買責任者はチェック結果の承認・管理を行う"
},
{
key:"access_purchase_decision_rule_manager",
model_key:"model_purchase_decision_rule",
model:"purchase.decision.rule",
group:"purchase.group_purchase_manager",
perm_read:true,
perm_write:true,
perm_create:true,
perm_unlink:false,
reason:"発注判断ルールは購買責任者のみ管理する"
},
{
key:"access_purchase_import_info_user",
model_key:"model_purchase_import_info",
model:"purchase.import.info",
group:"purchase.group_purchase_user",
perm_read:true,
perm_write:true,
perm_create:true,
perm_unlink:false,
reason:"購買担当が輸入レポート補足情報を管理する"
}
] AS row
MERGE (sec:OdooSecurityRuleSpec {key: row.key})
SET
sec.model = row.model,
sec.group = row.group,
sec.perm_read = row.perm_read,
sec.perm_write = row.perm_write,
sec.perm_create = row.perm_create,
sec.perm_unlink = row.perm_unlink,
sec.reason = row.reason,
sec.scope = "purchase_module",
sec.status = "draft",
sec.confidence = "assumed",
sec.updated_at = datetime()
WITH sec, row
MATCH (m:OdooModelSpec {key: row.model_key})
MERGE (m)-[:HAS_SECURITY_RULE]->(sec);

11. compute / onchange / constraint 仕様

compute

// =====================================================
// Compute Specs
// =====================================================UNWIND [
{
key:"compute_po_quantity_check_status",
model:"purchase.order",
field:"x_check_status",
method:"_compute_quantity_check_status",
depends:"quantity_check_ids.state",
logic:"関連する発注量チェック結果のうち、承認必要があれば approval_required、警告があれば warning、すべてOKなら ok、未実行なら not_checked とする",
field_key:"field_po_x_check_status"
},
{
key:"compute_po_quantity_check_count",
model:"purchase.order",
field:"x_quantity_check_count",
method:"_compute_quantity_check_count",
depends:"quantity_check_ids",
logic:"発注書に紐づく purchase.quantity.check の件数を算出する",
field_key:"field_po_x_quantity_check_count"
},
{
key:"compute_pol_variance_rate",
model:"purchase.order.line",
field:"x_variance_rate",
method:"_compute_variance_rate",
depends:"x_planned_qty,x_actual_qty",
logic:"予定数量が0でなければ、実績数量との差分を予定数量で割って差異率を算出する",
field_key:"field_pol_x_variance_rate"
}
] AS row
MERGE (c:OdooComputeSpec {key: row.key})
SET
c.model = row.model,
c.field = row.field,
c.method = row.method,
c.depends = row.depends,
c.logic = row.logic,
c.scope = "purchase_module",
c.status = "draft",
c.confidence = "assumed",
c.updated_at = datetime()
WITH c, row
MATCH (f:OdooFieldSpec {key: row.field_key})
MERGE (f)-[:COMPUTED_BY]->(c);

onchange

// =====================================================
// Onchange Specs
// =====================================================UNWIND [
{
key:"onchange_pol_product_vendor_price",
model:"purchase.order.line",
trigger:"product_id,partner_id,product_qty",
method:"_onchange_product_vendor_price_agreement",
logic:"品目、仕入先、数量が変わった場合、正式価格協定から単価・価格協定番号を再取得する",
topic:"price_agreement"
},
{
key:"onchange_po_import_required",
model:"purchase.order",
trigger:"currency_id,partner_id",
method:"_onchange_import_required",
logic:"外貨建てまたは輸入対象仕入先の場合、輸入取引フラグを候補設定する",
topic:"purchase_report_policy"
}
] AS row
MERGE (o:OdooOnchangeSpec {key: row.key})
SET
o.model = row.model,
o.trigger = row.trigger,
o.method = row.method,
o.logic = row.logic,
o.scope = "purchase_module",
o.status = "draft",
o.confidence = "assumed",
o.updated_at = datetime()
WITH o, row
MATCH (topic:DomainGuideTopic {key: row.topic})
MERGE (o)-[:DERIVED_FROM_DOMAIN_GUIDE]->(topic);

constraint

// =====================================================
// Constraint Specs
// =====================================================UNWIND [
{
key:"constraint_manual_price_change_reason",
model:"purchase.order.line",
name:"_check_manual_price_change_reason",
condition:"単価が価格協定から取得した単価と異なり、かつ変更理由が空の場合はエラー",
message:"価格協定と異なる単価を使用する場合は、変更理由を入力してください。",
topic:"price_agreement"
},
{
key:"constraint_variance_approval_before_confirm",
model:"purchase.order",
name:"_check_variance_approval_before_confirm",
condition:"発注量チェック状態が approval_required かつ未承認の場合、発注確定を禁止する",
message:"発注量差異が大きいため、購買責任者の承認が必要です。",
topic:"purchase_quantity_variance"
}
] AS row
MERGE (cons:OdooConstraintSpec {key: row.key})
SET
cons.model = row.model,
cons.name = row.name,
cons.condition = row.condition,
cons.message = row.message,
cons.scope = "purchase_module",
cons.status = "draft",
cons.confidence = "assumed",
cons.updated_at = datetime()
WITH cons, row
MATCH (topic:DomainGuideTopic {key: row.topic})
MERGE (cons)-[:DERIVED_FROM_DOMAIN_GUIDE]->(topic);

12. 帳票仕様

// =====================================================
// Report Specs
// =====================================================UNWIND [
{
key:"report_purchase_order_custom",
name:"発注書帳票 拡張",
model:"purchase.order",
report_type:"qweb-pdf",
base_report:"purchase.report_purchaseorder",
reason:"Odoo標準発注書をベースに、メーカー名、原料区分、納入条件、備考、担当者、価格協定番号を追加するため",
topic:"purchase_report_policy"
},
{
key:"report_purchase_import",
name:"輸入レポート",
model:"purchase.order",
report_type:"qweb-pdf/xlsx",
base_report:null,
reason:"輸入条件、通貨、インコタームズ相当情報を含む社内・通関補助用レポートを出すため",
topic:"purchase_report_policy"
},
{
key:"report_raw_material_purchase_list",
name:"原料・資材発注一覧",
model:"purchase.order.line",
report_type:"xlsx",
base_report:null,
reason:"未発注、発注済、納期遅延、入荷予定、発注金額をExcel出力するため",
topic:"purchase_report_policy"
}
] AS row
MERGE (r:OdooReportSpec {key: row.key})
SET
r.name = row.name,
r.model = row.model,
r.report_type = row.report_type,
r.base_report = row.base_report,
r.reason = row.reason,
r.scope = "purchase_module",
r.status = "draft",
r.confidence = "assumed",
r.updated_at = datetime()
WITH r, row
MATCH (topic:DomainGuideTopic {key: row.topic})
MERGE (r)-[:DERIVED_FROM_DOMAIN_GUIDE]->(topic);

13. DomainGuide → Odooコード変換ルール

// =====================================================
// DomainGuide Mapping Rules
// =====================================================UNWIND [
{
key:"dg_map_purchase_quantity_variance",
topic:"purchase_quantity_variance",
mapping_type:"rule_to_button_log_compute_constraint",
target_models:"purchase.order,purchase.order.line,purchase.quantity.check,purchase.quantity.check.line",
target_buttons:"action_run_quantity_check,action_request_variance_approval,action_approve_variance",
target_views:"purchase_order_form_quantity_check,purchase_quantity_check_form",
description:"発注量予実チェックDomainGuideを、発注書ボタン、チェックログモデル、差異率compute、承認constraintへ変換する"
},
{
key:"dg_map_price_agreement",
topic:"price_agreement",
mapping_type:"rule_to_supplierinfo_fields_onchange_constraint",
target_models:"product.supplierinfo,purchase.order.line",
target_buttons:null,
target_views:"supplierinfo_price_agreement,purchase_order_line_tree_quantity_check",
description:"価格協定DomainGuideを、supplierinfo拡張、単価再取得onchange、単価変更理由constraintへ変換する"
},
{
key:"dg_map_vendor_selection",
topic:"vendor_selection",
mapping_type:"rule_to_partner_product_fields",
target_models:"res.partner,product.template,product.supplierinfo",
target_buttons:null,
target_views:"product_template_purchase_control",
description:"仕入先選定DomainGuideを、仕入先・メーカー・商品・仕入先価格の関係へ変換する"
},
{
key:"dg_map_purchase_report_policy",
topic:"purchase_report_policy",
mapping_type:"rule_to_reports",
target_models:"purchase.order,purchase.order.line,purchase.import.info",
target_buttons:"action_generate_import_report",
target_views:"purchase_order_form_quantity_check",
description:"発注帳票・輸入レポート方針をQWeb/PDF/XLSXレポート仕様へ変換する"
},
{
key:"dg_map_mtx_mx_decomposition",
topic:"mtx_mx_decomposition",
mapping_type:"rule_to_do_not_migrate_and_dashboard",
target_models:"purchase.decision.rule",
target_buttons:null,
target_views:null,
description:"MTX_MX管理状況を画面単位では移植せず、在庫・価格・最小在庫・発注判断に分解する"
}
] AS row
MERGE (r:DomainGuideMappingRule {key: row.key})
SET
r.mapping_type = row.mapping_type,
r.target_models = row.target_models,
r.target_buttons = row.target_buttons,
r.target_views = row.target_views,
r.description = row.description,
r.scope = "purchase_module",
r.status = "draft",
r.confidence = "assumed",
r.updated_at = datetime()
WITH r, row
MATCH (topic:DomainGuideTopic {key: row.topic})
MERGE (topic)-[:HAS_CODE_MAPPING_RULE]->(r);

14. コード生成単位

// =====================================================
// Code Generation Units
// =====================================================UNWIND [
{
key:"code_unit_manifest",
file_path:"__manifest__.py",
unit_type:"manifest",
description:"Odooモジュール依存関係とデータファイルを定義する"
},
{
key:"code_unit_models_purchase_order",
file_path:"models/purchase_order.py",
unit_type:"python_model",
description:"purchase.order / purchase.order.line の拡張を実装する"
},
{
key:"code_unit_models_product",
file_path:"models/product.py",
unit_type:"python_model",
description:"product.template / product.supplierinfo の購買制約・価格協定拡張を実装する"
},
{
key:"code_unit_models_partner",
file_path:"models/res_partner.py",
unit_type:"python_model",
description:"res.partner の仕入先・メーカー関連拡張を実装する"
},
{
key:"code_unit_models_quantity_check",
file_path:"models/purchase_quantity_check.py",
unit_type:"python_model",
description:"purchase.quantity.check / line のカスタムモデルを実装する"
},
{
key:"code_unit_views_purchase_order",
file_path:"views/purchase_order_views.xml",
unit_type:"xml_view",
description:"発注書フォーム・明細ビュー・ボタン・タブを定義する"
},
{
key:"code_unit_views_quantity_check",
file_path:"views/purchase_quantity_check_views.xml",
unit_type:"xml_view",
description:"発注量チェック結果のtree/form/action/menuを定義する"
},
{
key:"code_unit_security_access",
file_path:"security/ir.model.access.csv",
unit_type:"security_csv",
description:"カスタムモデルのアクセス権を定義する"
},
{
key:"code_unit_reports_purchase",
file_path:"reports/purchase_reports.xml",
unit_type:"xml_report",
description:"発注書拡張帳票・輸入レポート・原料資材発注一覧を定義する"
}
] AS row
MERGE (u:CodeGenerationUnit {key: row.key})
SET
u.file_path = row.file_path,
u.unit_type = row.unit_type,
u.description = row.description,
u.scope = "purchase_module",
u.status = "draft",
u.confidence = "assumed",
u.updated_at = datetime()
WITH u
MATCH (scope:ImplementationScope {key:"purchase_module_odoo_impl_v1"})
MERGE (scope)-[:HAS_CODE_GENERATION_UNIT]->(u);

15. CodeGenerationUnit と Spec の接続

// =====================================================
// Connect Code Units to Specs
// =====================================================UNWIND [
{unit:"code_unit_models_purchase_order", spec:"model_purchase_order_ext"},
{unit:"code_unit_models_purchase_order", spec:"model_purchase_order_line_ext"},
{unit:"code_unit_models_product", spec:"model_product_template_ext"},
{unit:"code_unit_models_product", spec:"model_product_supplierinfo_ext"},
{unit:"code_unit_models_partner", spec:"model_res_partner_ext"},
{unit:"code_unit_models_quantity_check", spec:"model_purchase_quantity_check"},
{unit:"code_unit_models_quantity_check", spec:"model_purchase_quantity_check_line"},
{unit:"code_unit_models_quantity_check", spec:"model_purchase_decision_rule"},
{unit:"code_unit_models_quantity_check", spec:"model_purchase_import_info"}
] AS row
MATCH (u:CodeGenerationUnit {key: row.unit})
MATCH (m:OdooModelSpec {key: row.spec})
MERGE (u)-[:GENERATES_MODEL]->(m);UNWIND [
{unit:"code_unit_views_purchase_order", view:"view_purchase_order_form_quantity_check"},
{unit:"code_unit_views_purchase_order", view:"view_purchase_order_line_tree_quantity_check"},
{unit:"code_unit_views_quantity_check", view:"view_purchase_quantity_check_tree"},
{unit:"code_unit_views_quantity_check", view:"view_purchase_quantity_check_form"},
{unit:"code_unit_views_purchase_order", view:"view_product_template_purchase_control"},
{unit:"code_unit_views_purchase_order", view:"view_supplierinfo_price_agreement"}
] AS row
MATCH (u:CodeGenerationUnit {key: row.unit})
MATCH (v:OdooViewSpec {key: row.view})
MERGE (u)-[:GENERATES_VIEW]->(v);UNWIND [
{unit:"code_unit_security_access", sec:"access_purchase_quantity_check_user"},
{unit:"code_unit_security_access", sec:"access_purchase_quantity_check_manager"},
{unit:"code_unit_security_access", sec:"access_purchase_decision_rule_manager"},
{unit:"code_unit_security_access", sec:"access_purchase_import_info_user"}
] AS row
MATCH (u:CodeGenerationUnit {key: row.unit})
MATCH (s:OdooSecurityRuleSpec {key: row.sec})
MERGE (u)-[:GENERATES_SECURITY_RULE]->(s);UNWIND [
{unit:"code_unit_reports_purchase", report:"report_purchase_order_custom"},
{unit:"code_unit_reports_purchase", report:"report_purchase_import"},
{unit:"code_unit_reports_purchase", report:"report_raw_material_purchase_list"}
] AS row
MATCH (u:CodeGenerationUnit {key: row.unit})
MATCH (r:OdooReportSpec {key: row.report})
MERGE (u)-[:GENERATES_REPORT]->(r);

16. 確認用Cypher

16-1. モデル仕様一覧

MATCH (m:OdooModelSpec {scope:"purchase_module"})
OPTIONAL MATCH (m)-[:HAS_FIELD]->(f:OdooFieldSpec)
RETURN
m.key AS key,
m.model AS model,
m.mode AS mode,
m.module AS module,
m.status AS status,
count(f) AS field_count,
m.reason AS reason
ORDER BY m.mode, m.model;

16-2. フィールド仕様一覧

MATCH (m:OdooModelSpec)-[:HAS_FIELD]->(f:OdooFieldSpec)
WHERE f.scope = "purchase_module"
RETURN
m.model AS model,
f.name AS field,
f.label AS label,
f.ttype AS type,
f.required AS required,
f.readonly AS readonly,
f.store AS store,
f.reason AS reason
ORDER BY model, field;

16-3. View / Tab / Button構成

MATCH (m:OdooModelSpec)-[:HAS_VIEW]->(v:OdooViewSpec)
OPTIONAL MATCH (v)-[:HAS_TAB]->(t:OdooTabSpec)
OPTIONAL MATCH (v)-[:HAS_BUTTON]->(b:OdooButtonSpec)
RETURN
m.model AS model,
v.name AS view,
v.view_type AS view_type,
collect(DISTINCT t.name) AS tabs,
collect(DISTINCT b.label) AS buttons
ORDER BY model, view;

16-4. DomainGuideから実装仕様への流れ

MATCH (topic:DomainGuideTopic {scope:"purchase_module"})
OPTIONAL MATCH (topic)-[:HAS_CODE_MAPPING_RULE]->(rule:DomainGuideMappingRule)
OPTIONAL MATCH (topic)-[:IMPLEMENTED_BY_MODEL]->(m:OdooModelSpec)
OPTIONAL MATCH (topic)<-[:DERIVED_FROM_DOMAIN_GUIDE]-(x)
RETURN
topic.name AS topic,
collect(DISTINCT rule.key) AS mapping_rules,
collect(DISTINCT m.model) AS models,
collect(DISTINCT labels(x)[0] + ":" + coalesce(x.key, x.name)) AS derived_specs
ORDER BY topic;

16-5. コード生成単位

MATCH (u:CodeGenerationUnit {scope:"purchase_module"})
OPTIONAL MATCH (u)-[:GENERATES_MODEL]->(m:OdooModelSpec)
OPTIONAL MATCH (u)-[:GENERATES_VIEW]->(v:OdooViewSpec)
OPTIONAL MATCH (u)-[:GENERATES_SECURITY_RULE]->(s:OdooSecurityRuleSpec)
OPTIONAL MATCH (u)-[:GENERATES_REPORT]->(r:OdooReportSpec)
RETURN
u.file_path AS file,
u.unit_type AS type,
u.description AS description,
collect(DISTINCT m.model) AS models,
collect(DISTINCT v.name) AS views,
collect(DISTINCT s.key) AS security_rules,
collect(DISTINCT r.name) AS reports
ORDER BY file;

17. Phase12の結論

このPhase12をNeo4jに焼くと、次の状態になります。

業務要件Graph

質問回答Graph

DomainGuideGraph

Odoo実装仕様Graph

ここまで来ると、次にできることは明確です。

1. 実装仕様Graphをレビューする
2. 不要なモデル・フィールド・Viewを削る
3. 未確定Specを質問に戻す
4. 確定したSpecからOdooコードを生成する

エージェント案:

今回の焼き込みスクリプトは、

  • key 一意制約が多い
  • ImplementationScope で束ねている
  • DomainGuideTopic / BusinessElement / CodeGenerationUnit と仕様ノードをつないでいる
  • 最後に一覧確認クエリを持っている

ので、受け入れ判定パイプラインを載せやすい形です。

まず結論

おすすめはこの流れです。

  1. 焼き込み対象を scope_key で固定する
  2. 焼き込みを実行する
  3. 受け入れクエリを全部流す
  4. 結果を PASS / WARN / FAIL に分類する
  5. FAIL が1つでもあれば再焼き込み対象を特定する
  6. 修正後に再焼き込みする
  7. PASS なら scope.status = "verified" に上げる

一覧確認だけだと人間レビュー止まりですが、
ValidationRun / ValidationIssue を残すと、検閲→再焼き込みのループが自動化しやすいです。


1. ループ全体の考え方

今回のグラフは「業務設計 → Odoo仕様 → コード生成単位」までつながっています。
なので、検閲も3層に分けると分かりやすいです。

A. 構文・重複レベル

  • ノードが重複していないか
  • key 制約に違反しそうな作りになっていないか
  • scope がちゃんと付いているか

B. 構造レベル

  • Model に Field がぶら下がっているか
  • View に Tab / Button がついているか
  • CodeGenerationUnit が空振りしていないか

C. トレーサビリティレベル

  • DomainGuideTopic から実装仕様へつながっているか
  • DERIVED / IMPLEMENTED / GENERATES のどこかが欠けていないか

今回のスクリプトは、まさにこの3層を検査できる構造になっています。
ImplementationScopeHAS_MODEL_SPECIMPLEMENTED_BY_MODELHAS_CODE_MAPPING_RULEGENERATES_MODEL などがすでに用意されているからです。


2. 受け入れ判定の基準

おすすめはこうです。

FAIL

次フェーズへ進めない

  • 必須ノードがゼロ
  • 必須関係が欠落
  • 孤立ノードがある
  • DomainGuideTopic がどこにも落ちていない
  • CodeGenerationUnit が何も生成していない

WARN

進めてもよいがレビュー要

  • Field 数が少なすぎる
  • View はあるが Button がない
  • Compute / Constraint が少ない
  • expected count と少しずれる

PASS

想定どおり


3. 実際の運用ロジック

まず人間向けに書くとこうです。

burn(scope)
  ↓
acceptance checks
  ↓
if FAIL:
    issues を記録
    issue の種類ごとに再焼き込み方針を決める
    partial reburn or full reburn
    再度 acceptance
else if WARN:
    人間レビュー
    必要なら修正して reburn
else:
    verified

Odooモジュール実装に近い言い方をすると、

  • Phase12 Cypher を焼く
  • 仕様Graphを検閲する
  • 欠落があれば upstream の設計Graph か変換ルールを直す
  • もう一度焼く
  • 通ったらコード生成へ進む

です。


4. まず入れておくと良い検閲メタノード

これを別に持つと便利です。

CREATE CONSTRAINT validation_run_key IF NOT EXISTS
FOR (n:ValidationRun)
REQUIRE n.key IS UNIQUE;

CREATE CONSTRAINT validation_issue_key IF NOT EXISTS
FOR (n:ValidationIssue)
REQUIRE n.key IS UNIQUE;

検閲実行の親ノードです。

MERGE (run:ValidationRun {key: "validate_purchase_module_odoo_impl_v1_001"})
SET
  run.scope_key = "purchase_module_odoo_impl_v1",
  run.scope = "purchase_module",
  run.phase = "phase12_acceptance",
  run.started_at = datetime(),
  run.status = "running";

これで「いつ、どの scope を、どの条件で検査したか」を残せます。


5. コピペで使える受け入れ判定 Cypher セット

以下は 実際に回せる検閲セットです。
purchase_module_odoo_impl_v1 を対象にしています。


01. scope が存在するか

MATCH (s:ImplementationScope {key:"purchase_module_odoo_impl_v1"})
RETURN s.key, s.name, s.scope, s.status, s.updated_at;

これが 0 件なら即 FAIL です。
今回のスクリプトは ImplementationScope {key:"purchase_module_odoo_impl_v1"} を起点にしています。


02. ラベル別件数サマリ

MATCH (n)
WHERE n.scope = "purchase_module"
RETURN labels(n)[0] AS label, count(*) AS cnt
ORDER BY label;

これは最初の全体確認です。
特に OdooModelSpec, OdooFieldSpec, OdooViewSpec, OdooButtonSpec, OdooSecurityRuleSpec, DomainGuideMappingRule, CodeGenerationUnit がゼロでないことを見ます。作成対象ラベル群は冒頭で定義されています。


03. scope 配下の Model が scope に接続されているか

MATCH (m:OdooModelSpec {scope:"purchase_module"})
WHERE NOT (:ImplementationScope {key:"purchase_module_odoo_impl_v1"})-[:HAS_MODEL_SPEC]->(m)
RETURN m.key, m.model, m.name;

1件でも返れば FAIL 候補です。
今回の焼き込みでは ImplementationScope -[:HAS_MODEL_SPEC]-> OdooModelSpec が前提です。


04. Field の孤立検出

MATCH (f:OdooFieldSpec {scope:"purchase_module"})
WHERE NOT (:OdooModelSpec)-[:HAS_FIELD]->(f)
RETURN f.key, f.name, f.label
ORDER BY f.key;

Field が model につながっていないのは、実装仕様としてかなり危険です。


05. View / Tab / Button の孤立検出

MATCH (v:OdooViewSpec {scope:"purchase_module"})
WHERE NOT (:OdooModelSpec)-[:HAS_VIEW]->(v)
RETURN "VIEW" AS kind, v.key AS key, v.name AS name
UNION ALL
MATCH (t:OdooTabSpec {scope:"purchase_module"})
WHERE NOT (:OdooViewSpec)-[:HAS_TAB]->(t)
RETURN "TAB" AS kind, t.key AS key, t.name AS name
UNION ALL
MATCH (b:OdooButtonSpec {scope:"purchase_module"})
WHERE NOT (:OdooViewSpec)-[:HAS_BUTTON]->(b)
RETURN "BUTTON" AS kind, b.key AS key, b.label AS name;

このあたりは元スクリプトの HAS_VIEW, HAS_TAB, HAS_BUTTON に対応しています。


06. DomainGuideTopic の未反映検出

MATCH (topic:DomainGuideTopic {scope:"purchase_module"})
WHERE NOT (topic)-[:IMPLEMENTED_BY_MODEL]->(:OdooModelSpec)
  AND NOT (topic)-[:HAS_CODE_MAPPING_RULE]->(:DomainGuideMappingRule)
  AND NOT (topic)<-[:DERIVED_FROM_DOMAIN_GUIDE]-()
RETURN topic.key, topic.name
ORDER BY topic.key;

これが重要です。
今回の設計思想では DomainGuide からモデル・ルール・onchange・constraint・report などへ落としているので、topic がどこにも反映されていないなら「焼き込み漏れ」の可能性が高いです。


07. CodeGenerationUnit の空振り検出

MATCH (u:CodeGenerationUnit {scope:"purchase_module"})
WHERE NOT (u)-[:GENERATES_MODEL|GENERATES_VIEW|GENERATES_SECURITY_RULE|GENERATES_REPORT]->()
RETURN u.key, u.file_path, u.unit_type
ORDER BY u.file_path;

これはかなり実務的です。
ファイル単位ノードがあるのに生成対象がないなら、コード生成フェーズへ進めません。 GENERATES_MODEL, GENERATES_VIEW, GENERATES_SECURITY_RULE, GENERATES_REPORT は元スクリプトに定義されています。


08. Compute の未接続検出

MATCH (c:OdooComputeSpec {scope:"purchase_module"})
WHERE NOT (:OdooFieldSpec)-[:COMPUTED_BY]->(c)
RETURN c.key, c.model, c.field, c.method
ORDER BY c.key;

Field と Compute が切れていたら FAIL 寄りです。


09. Security Rule の未接続検出

MATCH (s:OdooSecurityRuleSpec {scope:"purchase_module"})
WHERE NOT (:OdooModelSpec)-[:HAS_SECURITY_RULE]->(s)
RETURN s.key, s.model, s.group
ORDER BY s.key;

権限制御が宙に浮いているかを見ます。


10. Report の未接続検出

MATCH (r:OdooReportSpec {scope:"purchase_module"})
WHERE NOT (r)-[:DERIVED_FROM_DOMAIN_GUIDE]->(:DomainGuideTopic)
RETURN r.key, r.name, r.model
ORDER BY r.key;

帳票が DomainGuide 由来である前提なら、ここが切れていると仕様トレースが弱いです。


11. Button が topic を実行しているか

MATCH (b:OdooButtonSpec {scope:"purchase_module"})
WHERE NOT (b)-[:EXECUTES_DOMAIN_GUIDE_TOPIC]->(:DomainGuideTopic)
RETURN b.key, b.label, b.name
ORDER BY b.key;

ボタン仕様の trace 用です。


12. 人間レビュー用一覧

これは元スクリプトの 16 系をそのまま使えばよいです。
特に下の3本は残すべきです。

  • View / Tab / Button 構成
  • DomainGuide から実装仕様への流れ
  • コード生成単位

元ファイルにも確認用として入っています。


6. FAIL/WARN/PASS の自動判定ロジック

運用上は、各クエリの結果件数で判定すると楽です。

例です。

Q01 scope existence:
  0件 => FAIL

Q03 scope-model link missing:
  1件以上 => FAIL

Q04 field orphan:
  1件以上 => FAIL

Q05 view/tab/button orphan:
  1件以上 => FAIL

Q06 unimplemented DomainGuideTopic:
  1件以上 => FAIL

Q07 empty CodeGenerationUnit:
  1件以上 => FAIL

Q08 compute orphan:
  1件以上 => WARN または FAIL

Q09 security orphan:
  1件以上 => FAIL

Q10 report not traced:
  1件以上 => WARN

Q02 label counts:
  閾値未満 => WARN or FAIL

7. 検閲結果を Issue ノードに残すやり方

各クエリ結果を見て、問題があれば ValidationIssue を作ります。

例として、孤立 Field 用です。

MATCH (run:ValidationRun {key:"validate_purchase_module_odoo_impl_v1_001"})
MATCH (f:OdooFieldSpec {scope:"purchase_module"})
WHERE NOT (:OdooModelSpec)-[:HAS_FIELD]->(f)
MERGE (issue:ValidationIssue {key: "issue_orphan_field_" + f.key})
SET
  issue.scope_key = "purchase_module_odoo_impl_v1",
  issue.kind = "orphan_field",
  issue.severity = "FAIL",
  issue.target_key = f.key,
  issue.target_label = "OdooFieldSpec",
  issue.message = "Field is not linked from any OdooModelSpec",
  issue.updated_at = datetime()
MERGE (run)-[:FOUND_ISSUE]->(issue);

こうすると、
結果がテーブルに出て終わりではなく、
次の再焼き込みロジックが issue を参照できるようになります。


8. 再焼き込みの戦略

再焼き込みは、全部消して焼き直す方式でもよいですが、
今回のように MERGE 中心なら 部分再焼き込みができます。

おすすめは次のルールです。

A. upstream 欠落

例:

  • BusinessElement がいない
  • DomainGuideTopic がいない

この場合は Phase12 ではなく、前段 Graph の補完が必要です。
今回のスクリプトは MATCH (be:BusinessElement ...)MATCH (topic:DomainGuideTopic ...) を前提にしているため、参照先がないとリンクが作られません。

B. spec 欠落

例:

  • Field が足りない
  • View が足りない
  • Report が足りない

この場合は Phase12 の該当セクションだけ再実行します。
MERGE + key 制約なので再実行しやすいです。

C. 接続欠落

例:

  • CodeGenerationUnit はあるが GENERATES_* がない
  • Topic はあるが IMPLEMENTED_BY_MODEL がない

この場合は 接続セクションだけ再焼き込みします。
今回のファイルでは「CodeGenerationUnit と Spec の接続」ブロックが独立しているのでやりやすいです。


9. 実運用でのループ例

運用を一番シンプルに書くとこうです。

Step 1

scope_key = purchase_module_odoo_impl_v1 を決める

Step 2

Phase12 Cypher を実行する

Step 3

ValidationRun を作る

Step 4

上の 12 本を順に流す

Step 5

結果を集約する

MATCH (run:ValidationRun {key:"validate_purchase_module_odoo_impl_v1_001"})-[:FOUND_ISSUE]->(i:ValidationIssue)
RETURN i.severity, count(*) AS cnt
ORDER BY i.severity;

Step 6

  • FAIL > 0 → 差し戻し
  • FAIL = 0 and WARN > 0 → レビュー
  • FAIL = 0 and WARN = 0 → verified

Step 7

verified なら scope を更新

MATCH (s:ImplementationScope {key:"purchase_module_odoo_impl_v1"})
SET s.status = "verified",
    s.verified_at = datetime();

10. いちばん大事な設計思想

このループで本当に重要なのは、
Cypher の結果を読むことではなく、何を“不合格条件”にするかを固定することです。

おすすめの不合格条件はこの5つです。

  • scope がない
  • 孤立 spec がある
  • DomainGuide から実装仕様に落ちていない topic がある
  • CodeGenerationUnit が空振りしている
  • 参照先不足で接続が張れていない

この5つを抑えるだけで、
「焼けたっぽい」から
「次のコード生成フェーズへ進める品質で焼けた」
に変わります。


必要なら次に、
この検閲セットを そのまま Neo4j Browser に貼れる 1本の acceptance.cypher にまとめます。


Comments

コメントを残す

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