概要

項目内容
案件IDMAS-125
案件名発注ステータス遷移ワークフローの標準化
カテゴリUX・ワークフロー
PhaseP2
優先度★★
所要時間4〜6 時間(Step 1 + Step 2 合計)
対象ファイル(新規)300_ui/302_order_status_handler.js
対象ファイル(変更)100_config/101_sys_config.jsonEdit ディスパッチ追加・案B採用時は DDL 追加)、200_data/202_repository.jsInvoiceRepository.save() 末尾に完納判定フック)、CLAUDE.md(案B 採用時の DDL 管理外タブ節へ追記)
前提案件なし(独立実装可。MAS-129 相見積比較の前提となる基盤機能)

目的

受発注プロセスの状態遷移を厳格化し、不正な後戻り遷移(例: 発注済見積中)を防止する。同時に、ステータス変更の監査証跡(いつ・誰が・何を)を保存することで、内部統制・SOC2 対応および「誰がいつ発注確定したか」の説明責任を果たすための基盤を整備する。

背景

  • 31_wrk_order の「発注ステータス」列は現在自由入力で、遷移制御が存在しない(000_infra/003_contracts.js L32 の OrderDTO では "見積中" | "発注済" | "検収済" と定義されているが、実値はセルに直接文字列を書き込む運用)
  • RPA による自動起票(401_rpa_hc.js L128 他)では 発注済 が自動設定されるが、その後の遷移は手動で任意値を書き込める
  • 「完納」への遷移は現状存在せず、発注残高がゼロになっても 発注ステータス検収済 のまま滞留する
  • 変更履歴が残らないため、「誰がいつ発注を取り消したか」を事後追跡する手段がない

現在のコード

OrderDTO 定義(000_infra/003_contracts.js L13-36)

/**
 * 31_wrk_order — 発注レコード
 * @typedef {Object} OrderDTO
 * @property {string}      発注ステータス       - "見積中" | "発注済" | "検収済"
 * @property {number}      発注残高(自動計算)
 * @property {string}      参照元区分           - "SUB" | "HC" | "CAPEX" 等
 * @property {string}      参照元ID
 * ...
 */

SHEET_DEFAULTS のデフォルト値(000_infra/002_constants.js L84)

{ pattern: '31_wrk_order', defaults: { '発注ステータス': '見積中', '契約形態': 'スポット' } },

RPA での自動設定値(例: 400_domain/401_rpa_hc.js L128)

'発注ステータス': '発注済',
'参照元区分': 'HC',

onEdit エントリポイント(100_config/101_sys_config.js L363-413)

function onEdit(e) {
  if (!e || !e.range) return;
  const sheetName = e.range.getSheet().getName();
  // ...(CONFIG_SHEET 自動採番処理)
  // I-01: 36_wrk_bank_import のマッチ方法同期
  // N-03 Step 2: 仕訳発行後の変更を auditLog で記録
  try { if (typeof handleUxAssist === 'function') handleUxAssist(e); } catch(err) { ... }
}

→ 現状 31_wrk_order の発注ステータス列は一切バリデーションされず、任意文字列で上書き可能。

Utils.auditLog シグネチャ(000_infra/004_utils.js L264-273)

/**
 * @param {string} operation - 'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'
 * @param {string} targetSheet - 対象シート名
 * @param {string} targetId - 対象レコードID
 * @param {string} targetCol - 対象列名
 * @param {string} funcName - 呼び出し元関数名
 * @param {*} [beforeValue] - 変更前値
 * @param {*} [afterValue]  - 変更後値
 * @param {string} [note]   - 備考
 */
auditLog: function(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note) { ... }

→ 既存の 98_audit_logLOG_AUDIT DDL: 100_config/101_sys_config.js L667)は操作追跡用 WORM シートとして運用されており、本案件の変更履歴を流用できる。

OrderRepository / InvoiceRepository200_data/202_repository.js L107-206)

var OrderRepository = {
  findAll: function() { return readSheetAsDtos_(OrderRepository._getSheet()); },
  save: function(dtos) { /* writeDtosToSheet_ 経由で全行上書き */ },
  append: function(dtos) { /* 末尾追記 */ },
};

var InvoiceRepository = {
  findAll: function() { return readSheetAsDtos_(InvoiceRepository._getSheet()); },
  save: function(dtos) { /* writeDtosToSheet_ 経由で全行上書き */ },
  append: function(dtos) { /* 末尾追記(科目マスタから諸表区分・大分類を自動付与) */ },
};

OrderRepository.save() / InvoiceRepository.save() は全行上書き。ステータス列単体の更新にも全行書き換えが発生するため、計算式セル(発注残高(自動計算))の扱いに注意が必要。

修正方針

本案件は 2 Step 構成とし、Step 1(バリデーション+ログ)を先行実装、Step 2(完納自動遷移)を後続実装する独立デプロイ可能な分割とする。

Step 1: 状態遷移バリデーションとログ記録

アーキテクチャ

  1. 既存 onEdit100_config/101_sys_config.js L363)にシート名ディスパッチを追加
  2. sheetName === '31_wrk_order' かつ編集列が「発注ステータス」列の場合、新規ハンドラ handleOrderStatusEdit_(e)300_ui/302_order_status_handler.js に新設)に委譲
  3. ハンドラ内で遷移ルールを検証し、不正な場合は変更前値に戻してアラート表示
  4. 許可される遷移の場合は Utils.auditLog で変更履歴を記録

遷移ルール定数 VALID_ORDER_TRANSITIONS

300_ui/302_order_status_handler.js ファイル内の冒頭にファイルローカル定数として定義する(000_infra/002_constants.js への追加は避け、UI レイヤーの関心事として 300 系に閉じる)。

// 発注ステータス遷移ルール(終端ステータスは空配列)
// OrderDTO(003_contracts.js L32)+ SHEET_DEFAULTS(002_constants.js L84)から導出
var VALID_ORDER_TRANSITIONS = {
  '見積中':     ['発注済', 'キャンセル'],
  '発注済':     ['検収済', 'キャンセル'],
  '検収済':     ['完納', 'キャンセル'],
  '完納':       [],  // 終端
  'キャンセル': []   // 終端
};

: 実値は OrderDTO の現行定義("見積中" | "発注済" | "検収済")に、本案件で追加する "完納""キャンセル" を加えた 5 値。TODO_future.md では "納品完了" と記載があるが、OrderDTO と既存 RPA 実装の用語に合わせ "検収済" を採用。命名差異は「人間が検討すべき事項」に記載。

バリデーションフロー

  1. 対象シート絞り込み: sheetName === '31_wrk_order'
  2. 対象列絞り込み: ヘッダー行を読み取り headers.indexOf('発注ステータス')e.range.getColumn() を突き合わせ
  3. 1 行目(ヘッダー行)の編集はスキップ(row === 1
  4. 複数行ペースト対応: e.range.getNumRows() > 1 の場合は e.range.getValues() で全値取得、e.range.getSheet().getRange(row, col, numRows, 1).getValues() で反映前状態を取得する必要があるが、GASonEdit は既に編集後の状態で発火するため e.oldValue が単セルのみで複数行では失われる。→ 複数行ペーストは 1 件でも不正遷移が含まれていれば全行をロールバックする挙動とし、ロールバック先は「当該行のペースト前値」を e.oldValue が取れない場合は ペースト操作全体をキャンセルe.range.setValues(originalValues) で元値に戻す手段がないため、代替として「複数行ペーストは一律で警告し、手動で個別に修正させる」運用とする。詳細は「エッジケース」参照)
  5. 単セル編集時のバリデーション:
    • oldStatus = String(e.oldValue || '').trim()
    • newStatus = String(e.value || '').trim()
    • oldStatusVALID_ORDER_TRANSITIONS のキーに存在しない場合はスキップ(初期入力・RPA経由 発注済 直書き等)
    • newStatusVALID_ORDER_TRANSITIONS[oldStatus] に含まれない場合は不正
  6. 不正時の処理:
    • SpreadsheetApp.getUi().alert('ステータス遷移エラー', '「' + oldStatus + '」→「' + newStatus + '」は許可されていません。\n許可される遷移: ' + (VALID_ORDER_TRANSITIONS[oldStatus].join(' / ') || '(終端ステータス)'), ui.ButtonSet.OK)
    • e.range.setValue(oldStatus) で変更前値に戻す
  7. 正常遷移時の処理:
    • 発注ID(ORD) 列の値を e.range.getSheet().getRange(row, headers.indexOf('発注ID(ORD)') + 1).getValue() で取得
    • Utils.auditLog('UPDATE', '31_wrk_order', ordId, '発注ステータス', 'handleOrderStatusEdit_', oldStatus, newStatus, '')

Step 2: 「完納」への自動遷移

トリガー

InvoiceRepository.save() の末尾(200_data/202_repository.js L177 直前)に checkAndFinalizeOrder_(ordIds) 呼び出しを追加。INV 保存時に影響を受けた発注 ID を抽出し、各 ORD に対して完納判定を行う。

InvoiceRepository.append() 経由の新規 INV 追加時は残高は発生段階で非ゼロのため完納にはならないが、一貫性のため append() 末尾にも同じフックを入れる。実装上は共通ヘルパーで包む。

ロジック

checkAndFinalizeOrder_(ordIds):
  1. ordIds が空なら return
  2. invResult = InvoiceRepository.findAll()
  3. ordResult = OrderRepository.findAll()
  4. for each ordId in ordIds:
       a. 対象 ORD の DTO を ordResult.dtos から find(見つからなければ skip)
       b. 既に '完納' または 'キャンセル' なら skip(冪等)
       c. 同一 ordId の INV を invResult.dtos から filter
       d. 1 件も INV がなければ skip(未計上発注)
       e. 全 INV の '請求ステータス' が '承認済' でなければ skip
       f. 対象 ORD の '発注残高(自動計算)' が 0 でなければ skip
       g. 条件成立: DTO の '発注ステータス' を '完納' に更新、更新フラグ立てる
  5. 更新フラグが 1 件以上あれば:
       a. OrderRepository.save(ordResult.dtos)
       b. Utils.auditLog('UPDATE', '31_wrk_order', ordId, '発注ステータス',
          'checkAndFinalizeOrder_', '検収済', '完納',
          '完納自動遷移(残高=0, 全INV承認済)') を ORD ごとに呼び出す

自動遷移の起点ステータス

VALID_ORDER_TRANSITIONS では 発注済 → 完納 は定義しない(必ず 検収済 を経由する)。ただし自動遷移側(checkAndFinalizeOrder_)では「残高 0 かつ全 INV 承認済」という客観条件を満たす場合のみ起動するため、検収済 → 完納 以外の遷移(例: 発注済 のまま全 INV 承認済・残高 0 になった場合)も想定される。この場合は運用誤りなので「人間が検討すべき事項」に残す。

影響範囲

変更ファイル変更量影響の種類
300_ui/302_order_status_handler.js(新規)+約 100 行UI バリデーション、Step 2 の自動遷移ロジック
100_config/101_sys_config.js(変更)+約 10 行既存 onEdit(e) にディスパッチ 1 ブロック追加
200_data/202_repository.js(変更)+約 4 行InvoiceRepository.save() / append() 末尾に checkAndFinalizeOrder_ 呼び出し 1 行ずつ
新規タブ 38_ord_status_log案B採用時のみDDL 追加・CLAUDE.md 追記

DB 影響: Step 1 は 98_audit_log への append のみ(既存シート利用)。Step 2 は 31_wrk_order発注ステータス 列を書き換える(OrderRepository.save() 経由で全行上書き)。

注意事項

  1. onEdit のパフォーマンス: handleOrderStatusEdit_ 内では OrderRepository.findAll() / save() を呼ばず、ヘッダー取得とセルの読み書きのみで完結させる。大量データ書き換えは Step 2(InvoiceRepository.save() 連動)側に集約する。
  2. 複数行ペースト: onEdite.oldValue は単セル編集時のみ値を持ち、複数セル編集では undefinede.range.getNumRows() * e.range.getNumColumns() > 1 の場合は「検証不能のため全行をキャンセルし、ユーザーに 1 行ずつ修正するよう促す警告のみ」とする(setValues による復元は元値が取得できないため不可)。
  3. DDL 管理: 案B(38_ord_status_log 新設)を採用する場合のみ 100_config/101_sys_config.jssetupAllSchemas() にスキーマ定義を追加し、CLAUDE.md の「DDL (setupAllSchemas) で管理されないタブ」節ではなく、DDL 管理対象タブとして扱う。案A(Utils.auditLog 流用)を採用する場合は既存 LOG_AUDIT DDL のみで完結する。
  4. Utils.auditLog の operation 引数: 許容値は 'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'000_infra/004_utils.js L264)。本案件は 'UPDATE' を使用。'CANCEL' は「キャンセル遷移時」に使うかを検討事項として残す(本仕様は一律 'UPDATE')。
  5. 発注残高(自動計算) の扱い: OrderDTO 上は number 型だが、シート上では計算式(SUMIF 等)で自動計算されている可能性がある。OrderRepository.save() は全値を上書きするため、Step 2 実装時に計算式セルが上書きされないことを実データで確認する(計算式が失われる場合は別途対処が必要。人間検討事項)。
  6. RPA による直接書き込み: 400_domain/401_rpa_hc.js 等は OrderRepository.append() 経由で 発注ステータス: '発注済' を新規行として追加する。append は onEdit を発火しないため、本バリデーションの対象外(設計通り。RPA 経由の初期値は信頼できる前提)。

エッジケース

#条件振る舞い理由
E1許可された遷移(見積中発注済そのまま保存 + Utils.auditLog('UPDATE', ...) で履歴記録VALID_ORDER_TRANSITIONS['見積中']'発注済' を含む
E2禁止された遷移(発注済見積中検収済発注済SpreadsheetApp.getUi().alert() 通知 + e.range.setValue(oldStatus) で元値ロールバック後戻り遷移は内部統制違反
E3終端ステータスからの遷移(完納 / キャンセル → 任意の値)alert() 通知 + 元値ロールバックVALID_ORDER_TRANSITIONS['完納'] = [] / ['キャンセル'] = [] で全遷移を拒否
E4oldStatusVALID_ORDER_TRANSITIONS のキーに存在しない(空セル → 値、未定義値からの遷移)バリデーションをスキップ(=任意の newStatus を許可)し、ログのみ記録初期入力・RPA append・データ移行直後の状態にも対応するため
E5newStatusVALID_ORDER_TRANSITIONS のキーに存在しない(タイポ・存在しないステータス)alert() 通知 + 元値ロールバック5 値以外は許可しない
E6同じ値での「再保存」(発注済発注済バリデーションも履歴記録もスキップoldStatus === newStatus の場合は実質変化なし。冪等性を保つ
E7複数行コピー&ペースト(e.range.getNumRows() * getNumColumns() > 1alert() 通知のみ表示し、編集は受け入れる(ロールバックしない)e.oldValueundefined のため元値復元が不能。「複数行ペーストはバリデーション対象外。手動で 1 行ずつ修正してください」と警告し、ユーザーに後追い修正を促す
E81 行目(ヘッダー行)の編集スキップe.range.getRow() === 1 で除外
E9「発注ステータス」列以外の編集スキップheaders.indexOf('発注ステータス') + 1 !== e.range.getColumn() で除外
E1031_wrk_order 以外のシート編集スキップe.range.getSheet().getName() !== '31_wrk_order' で除外
E11自動「完納」遷移時に対象 ORD が既に 完納 または キャンセルスキップ(冪等)二重更新・キャンセル済 ORD の上書きを防止
E12自動「完納」遷移時に対象 ORD に紐づく INV が 0 件スキップ「未計上発注」は完納条件を満たさない
E13自動「完納」遷移時に一部 INV が未承認(請求ステータス !== '承認済'スキップ全件承認済が完納条件
E14自動「完納」遷移時に 発注残高(自動計算) が非ゼロスキップ残高ゼロが完納条件(仕様 Step 2 #f)
E15手動で 完納 を設定(残高非ゼロ)alert() で警告ダイアログ表示後、ユーザー確認で続行可管理者による手動クローズ(請求書未受領のまま完了扱い)を許容するための例外運用。要件は「人間が検討すべき事項」#3 で別途検討
E16RPA による OrderRepository.append() 経由の新規行追加バリデーションをスキップ(onEdit 自体が発火しない)設計通り。RPA 経由の初期値(発注済 等)は信頼できる前提
E17発注ID(ORD) 列が空欄の行で発注ステータスを編集バリデーションは実施するが Utils.auditLogtargetId には空文字を渡す想定外データだが onEdit を落とさない。後続の調査用にログのみ残す
E18自動「完納」遷移時に OrderRepository.save()発注残高(自動計算) が数式セルだった場合数式が定数値で上書きされる可能性「人間が検討すべき事項」#5 で別途検討。実装前に MCP で実態確認すること

実データ検証

確認項目確認方法目的
31_wrk_order発注ステータス 列に存在する実値の集合MCP で 31_wrk_order発注ステータス 列をユニーク抽出DDL 定義(OrderDTO"見積中" | "発注済" | "検収済")との乖離(例: 過去データに "納品完了" "完了" 等の表記揺れが存在しないか)を検出。乖離があれば VALID_ORDER_TRANSITIONS のキーに追加するか、データクレンジング案件を別途起票する
発注残高(自動計算) 列がスプレッドシート数式(SUMIF 等)か静的値かMCP で 31_wrk_order の任意行の同列を getFormula() 相当で確認数式セルの場合、OrderRepository.save() で全行上書きすると数式が失われる。失われる場合は Step 2 実装方針を「全行 save ではなく対象行のみ getRange().setValue() で更新する」に変更する
発注ID(ORD) 列の最終列位置MCP で 31_wrk_order のヘッダー行を確認headers.indexOf('発注ID(ORD)') でランタイム取得するため位置依存はないが、列名の誤記(全角/半角・括弧種別)が DDL 定義と一致しているかを確認
Utils.auditLogoperation 引数許容値の現行実装000_infra/004_utils.js L264-273 を Read で再確認仕様書で前提とした 'UPDATE' が許容されることを最終確認(Phase 1 で確認済)
既存 onEdit 内の他ハンドラとの実行順序100_config/101_sys_config.js L363-413 を Read本案件のディスパッチを既存ハンドラより前に置くべきか後ろに置くべきかを判断(バリデーションは早期 return が望ましいため、MAS-179 の auditLog 後・handleUxAssist より前を推奨)

関連ドキュメント

ドキュメント関連箇所
CLAUDE.md「コーディング規約 > データアクセス」列参照はヘッダー名ベース(indexOf / buildHeaderIndex_)。本案件もハードコード禁止に従う
CLAUDE.md「プロダクトポリシー > Human-in-the-Loop」バリデーション失敗時の alert() 通知・自動「完納」遷移の透明性(auditLog で履歴記録)は Human-in-the-Loop の延長
CLAUDE.md「DDL (setupAllSchemas) で管理されないタブ」案B(38_ord_status_log 新設)採用時は本リストに追記しないこと(DDL 管理対象とする)
dev_mas-122 ORD↔INV 消化状況の整合性チェック発注残高の整合性検証ロジック。Step 2 自動「完納」遷移の前提となる残高計算の正確性を担保
dev_mas-124 31タブ発注残高エイジング&超過発注アラートTODO_future.md 上「発注ステータス自動更新」の起票が MAS-124 にも記載されているため、Step 2 の責務分担を明確化(本案件で実装する旨を MAS-124 仕様書側にも追記推奨)
dev_mas-179 監査証跡(auditLog)Utils.auditLog の運用方針・98_audit_log シート構造・ログ参照UI
docs/_internal/failure_patterns.md#18-#20(固有名詞の誤記)— ステータス値・列名は DDL 定義と完全一致させること

人間が検討すべき事項

  1. TODO_future.md からの転記事項:
    • ステータス後戻りの可否(取消・差戻しをどう表現するか): 本仕様は 見積中 → 発注済 → 検収済 → 完納 の前進遷移のみ許可し、キャンセル を全段階から到達可能な「分岐終端」として扱う。差戻し(例: 検収済発注済 で再納品)を許可するかは未決定。許可する場合は (a) 一律許可するか (b) 承認者ロール別に許可するか (c) 差戻し専用ステータス 差戻し中 を新設するかの 3 案がある。本案件は (a)/(b)/(c) いずれも採用せず「差戻しは禁止、必要時は別 PR で VALID_ORDER_TRANSITIONS を更新」とする。
    • 検収日の定義(ORD 側で管理 or 別タブ): 本案件は検収日列を新設せず、「検収済 への遷移日 = 38_ord_status_log または 98_audit_log のタイムスタンプ」で代用する。検収日を独立した列として OrderDTO に追加するかは別案件で検討。
  2. ログ記録方式(案A: Utils.auditLog 流用 vs 案B: 38_ord_status_log 新設):
    • 案A(既存 98_audit_log 流用): 実装コスト低・保守シンプル。ただし発注ステータス変更だけを抽出したいときに他の操作ログと混在し、フィルタリングが必要。operation = 'UPDATE' + targetSheet = '31_wrk_order' + targetCol = '発注ステータス' でフィルタ可能。
    • 案B(38_ord_status_log 新設): 専用ビューで発注プロセスの監査が容易(変更者・遷移パスの時系列分析)。DDL 追加・CLAUDE.md 追記・スキーマ管理の追加コストあり。SOC2 準備フェーズに入った時点で案B 移行を検討。
    • 本仕様の方針: まず案A でリリースし、運用状況を見て案B への移行を別案件で実施する。
  3. 手動「完納」設定時の警告ダイアログ要否(E15): 残高非ゼロのまま 完納 に手動遷移する場合、警告ダイアログで確認するか、無条件で許可するか。本仕様は VALID_ORDER_TRANSITIONS['検収済'] = ['完納', 'キャンセル']検収済 → 完納 手動遷移を許可するが、残高非ゼロ警告は実装しない(管理者の判断を尊重)。SOC2 対応フェーズで「残高非ゼロでの完納は要承認」ルールを導入するかを別途検討。
  4. LockService による排他制御の要否: onEdit の処理は軽量(セル読み書き + auditLog の append)なので必須ではない。Step 2 の checkAndFinalizeOrder_OrderRepository.findAll + save を伴うため、複数 INV が同時 save された場合の競合リスクがある。LockService.getScriptLock().tryLock(5000) で防御するかを実装時に判断(既存 InvoiceRepository.save() 内で排他制御している場合は不要)。
  5. 発注残高(自動計算) 列の数式上書き問題: 同列がスプレッドシート数式で計算されている場合、OrderRepository.save() の全行上書きで数式が失われる。実装前に MCP で実態を確認し、数式の場合は (a) save() で当該列をスキップするオプション追加 (b) Step 2 の実装方式を「対象 ORD の 1 セル setValue() のみ」に変更 (c) save 後に数式を再設定 のいずれかを選択する。
  6. 複数行ペースト時のロールバック実装(E7): 現行案は「警告のみ・ロールバックなし」。これを「ペースト前のシート状態をスナップショット取得してロールバック」する高度実装に拡張するかは、運用上の発生頻度を見て判断。
  7. 38_ord_status_log を案B で実装する場合のシート番号: 38 は他用途で使用されていないことを Phase 1 で確認したが、将来 36_wrk_bank_import 系の追加と競合する可能性があるため、最終的に 39 等への変更も含めて命名規約を再確認すること(30 番台が wrk_ グループであるため、ログ系として相応しいかも論点)。

実装プロンプト(Claude Code 用)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-125「発注ステータス遷移ワークフローの標準化」を実装してください。

## 実行前タスク

以下をこの順番で Read し、確認ポイントを把握してから実装を開始すること
(Grep の部分ヒットで推測しない・固有名詞は Read した実在文字列のみ使用):

1. `000_infra/003_contracts.js` — `OrderDTO`(L13-36)の全フィールド名を確認。
   特に「発注ステータス」「発注残高(自動計算)」「発注ID(ORD)」「参照元区分」「参照元ID」の実在を確認する。
2. `000_infra/002_constants.js` — `SHEET_DEFAULTS` 内の `'31_wrk_order'` パターン(L84 付近)の
   デフォルト値を確認。`'発注ステータス': '見積中'` が既定値。
3. `200_data/202_repository.js` — `OrderRepository.findAll()` / `OrderRepository.save()` /
   `InvoiceRepository.findAll()` / `InvoiceRepository.save()` / `InvoiceRepository.append()` の
   シグネチャと L107-206 の実装パターン。`save()` は全行上書きであることを確認する。
4. `000_infra/004_utils.js` — `Utils.auditLog()` のシグネチャ(L264-273)。
   `operation` 許容値: `'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'`。
5. `100_config/101_sys_config.js` — 既存 `onEdit(e)`(L363-413)のディスパッチ構造。
   新ハンドラを MAS-179 ログ記録の後・`handleUxAssist` 呼び出しの前に挿入する。
6. (案B 採用時のみ)`100_config/101_sys_config.js` の `setupAllSchemas` 内で
   `LOG_AUDIT` DDL(L667 付近)を Read し、シート DDL 定義パターンを把握する。

## 実データ確認(実装前に MCP で確認)

1. `31_wrk_order` の `発注ステータス` 列にユニーク値(過去データに `納品完了` 等の表記揺れがないか)
2. `発注残高(自動計算)` 列がスプレッドシート数式か静的値か
3. `38_ord_status_log` シートが既に存在しないこと(案B採用時)

## 修正対象ファイル

- **新規作成**: `300_ui/302_order_status_handler.js`
  (状態遷移バリデーション + 履歴記録 + 完納自動遷移ロジックを集約)
- **変更**: `100_config/101_sys_config.js`
  (`onEdit(e)` に `31_wrk_order` 用ディスパッチを 1 ブロック追加)
- **変更**: `200_data/202_repository.js`
  (`InvoiceRepository.save()` および `append()` 末尾に `checkAndFinalizeOrder_()` 呼び出しを追加)
- **変更(案B 採用時のみ)**: `100_config/101_sys_config.js` の `setupAllSchemas` に
  `38_ord_status_log` の DDL(`履歴ID` / `タイムスタンプ` / `発注ID(ORD)` / `変更者` /
  `変更前ステータス` / `変更後ステータス` / `備考`)を追加 + CLAUDE.md の DDL 管理外タブ節は触らない
- **変更(案B 採用時のみ・任意)**: `CLAUDE.md` の「DDL (setupAllSchemas) で管理されないタブ」節は変更不要
  (`38_ord_status_log` は DDL 管理対象とするため)

## 実装内容

### (1) `300_ui/302_order_status_handler.js`(新規)

```javascript
// 発注ステータス遷移ルール(終端ステータスは空配列)
var VALID_ORDER_TRANSITIONS = {
  '見積中':     ['発注済', 'キャンセル'],
  '発注済':     ['検収済', 'キャンセル'],
  '検収済':     ['完納', 'キャンセル'],
  '完納':       [],   // 終端
  'キャンセル': []    // 終端
};

/**
 * onEdit から委譲される 31_wrk_order 用ハンドラ
 * @param {GoogleAppsScript.Events.SheetsOnEdit} e
 */
function handleOrderStatusEdit_(e) {
  var sheet = e.range.getSheet();
  if (sheet.getName() !== '31_wrk_order') return;

  var row = e.range.getRow();
  if (row === 1) return; // ヘッダー行

  // ヘッダー位置から「発注ステータス」列の位置を算出
  var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  var statusColIdx = headers.indexOf('発注ステータス'); // 0-based
  if (statusColIdx === -1) return;
  if (e.range.getColumn() !== statusColIdx + 1) return;

  // 複数セル編集はバリデーション不能(e.oldValue が undefined)
  if (e.range.getNumRows() * e.range.getNumColumns() > 1) {
    SpreadsheetApp.getUi().alert(
      'ステータス一括変更の警告',
      '複数セルの一括変更は遷移バリデーションの対象外です。手動で 1 行ずつ修正してください。',
      SpreadsheetApp.getUi().ButtonSet.OK
    );
    return;
  }

  var oldStatus = String(e.oldValue || '').trim();
  var newStatus = String(e.value || '').trim();
  if (oldStatus === newStatus) return; // 同値再保存

  // oldStatus が遷移ルールに存在しない場合は新規入力扱いでスキップ
  if (!VALID_ORDER_TRANSITIONS.hasOwnProperty(oldStatus)) {
    // ログのみ記録(任意。記録不要なら return)
    return;
  }

  var allowed = VALID_ORDER_TRANSITIONS[oldStatus];
  if (allowed.indexOf(newStatus) === -1) {
    var ui = SpreadsheetApp.getUi();
    ui.alert(
      'ステータス遷移エラー',
      '「' + oldStatus + '」→「' + newStatus + '」は許可されていません。\n' +
      '許可される遷移: ' + (allowed.join(' / ') || '(終端ステータス)'),
      ui.ButtonSet.OK
    );
    e.range.setValue(oldStatus); // ロールバック
    return;
  }

  // 正常遷移: auditLog で履歴記録
  var ordIdColIdx = headers.indexOf('発注ID(ORD)');
  var ordId = ordIdColIdx === -1 ? '' :
    String(sheet.getRange(row, ordIdColIdx + 1).getValue() || '');
  Utils.auditLog(
    'UPDATE', '31_wrk_order', ordId, '発注ステータス',
    'handleOrderStatusEdit_', oldStatus, newStatus, ''
  );
}

/**
 * 完納自動遷移チェック(InvoiceRepository.save() / append() の末尾で呼び出す)
 * @param {string[]} ordIds - 影響を受けた親発注ID(ORD_xxxx_xxxx)配列。空ならスキップ
 */
function checkAndFinalizeOrder_(ordIds) {
  if (!ordIds || ordIds.length === 0) return;

  var invResult = InvoiceRepository.findAll();
  var ordResult = OrderRepository.findAll();
  var changed = [];

  ordIds.forEach(function(ordId) {
    var ordDto = ordResult.dtos.find(function(d) {
      return d['発注ID(ORD)'] === ordId;
    });
    if (!ordDto) return;
    var currentStatus = String(ordDto['発注ステータス'] || '').trim();
    if (currentStatus === '完納' || currentStatus === 'キャンセル') return;

    var relatedInvs = invResult.dtos.filter(function(inv) {
      return inv['親発注ID(ORD)'] === ordId && inv['有効FLG'] !== false;
    });
    if (relatedInvs.length === 0) return;

    var allApproved = relatedInvs.every(function(inv) {
      return inv['請求ステータス'] === '承認済';
    });
    if (!allApproved) return;

    var balance = Number(ordDto['発注残高(自動計算)'] || 0);
    if (balance !== 0) return;

    var prevStatus = currentStatus;
    ordDto['発注ステータス'] = '完納';
    changed.push({ ordId: ordId, before: prevStatus });
  });

  if (changed.length > 0) {
    OrderRepository.save(ordResult.dtos);
    changed.forEach(function(c) {
      Utils.auditLog(
        'UPDATE', '31_wrk_order', c.ordId, '発注ステータス',
        'checkAndFinalizeOrder_', c.before, '完納',
        '完納自動遷移(残高=0, 全INV承認済)'
      );
    });
  }
}
```

### (2) `100_config/101_sys_config.js` の `onEdit(e)` 改修(既存 L363-413 付近)

既存の MAS-179 auditLog 処理ブロックの直後・`handleUxAssist` 呼び出しの直前に下記を追加:

```javascript
// S-53: 発注ステータス遷移バリデーション
try {
  if (typeof handleOrderStatusEdit_ === 'function') handleOrderStatusEdit_(e);
} catch (err) {
  Utils.logError('handleOrderStatusEdit_ で例外: ' + err.message);
}
```

### (3) `200_data/202_repository.js` の `InvoiceRepository` 改修

`InvoiceRepository.save(dtos)` 末尾(書き込み完了後)に下記を追加:

```javascript
// S-53: 影響を受けた発注の完納自動遷移チェック
try {
  var ordIds = dtos
    .map(function(d) { return d['親発注ID(ORD)']; })
    .filter(function(v) { return v; });
  var uniqueOrdIds = Array.from(new Set(ordIds));
  if (typeof checkAndFinalizeOrder_ === 'function') {
    checkAndFinalizeOrder_(uniqueOrdIds);
  }
} catch (err) {
  Utils.logError('checkAndFinalizeOrder_ in InvoiceRepository.save で例外: ' + err.message);
}
```

`InvoiceRepository.append(dtos)` 末尾にも同様のフックを追加(共通ヘルパー化推奨)。

## 制約

- `OrderDTO` / `InvoiceDTO` に存在しない列名を参照しない。列名は必ず `003_contracts.js` の
  `@property` と一致させる(failure_patterns.md #18-#20)
- `sheet.getLastColumn()` でヘッダー行を取得する箇所は許可(行 1 のみ取得、列幅は静的でない)
- `OrderRepository.save()` は全行書き換えのため、`発注残高(自動計算)` が数式セルの場合は
  上書きされる可能性がある。実装前に実データで挙動確認し、数式上書きが発生する場合は
  Step 2 を「対象行 1 セルのみ `setValue()` する」方式に切り替え、人間にエスカレーションする
- `onEdit` 内では `SpreadsheetApp.getUi().alert()` を 1 回だけ呼ぶ(多重表示を避ける)
- 新規定数 `VALID_ORDER_TRANSITIONS` の名前衝突を Grep で確認してから追加する
- `InvoiceRepository.save()` への `checkAndFinalizeOrder_` フックは try/catch で囲み、
  自動遷移が失敗しても INV 保存自体は成功させる

## 動作確認

1. `npm run push:dev` でデプロイ
2. `31_wrk_order` の任意行で「発注ステータス」を `見積中` → `発注済`(許可)に変更
   → 正常保存・`98_audit_log` にレコード追記されることを確認
3. 同じ行で `発注済` → `見積中`(禁止)に変更
   → `alert` が表示され元の値に戻ることを確認
4. `完納` / `キャンセル` から任意の値への変更試行
   → `alert` で拒否されることを確認
5. 終端 `検収済` → `完納`(許可)に変更
   → 正常保存
6. 複数セル選択での一括ペースト
   → `alert` で警告表示され、編集自体は受け入れられる(バリデーション対象外であることを確認)
7. テスト用の ORD(残高ゼロ・全紐づき INV を「承認済」にして `InvoiceRepository.save()` を発火)
   → ステータスが `完納` に自動遷移し、`98_audit_log` に
   `funcName=checkAndFinalizeOrder_` のレコードが記録されることを確認
8. 既に `完納` の ORD に対して再度 `InvoiceRepository.save()` を実行
   → 何も起こらない(冪等)ことを確認

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 調査・設計(Phase 1相当) | あり | ファイル構造・遷移ルール確定・案A/案B判断 |
| 実装(Phase 2清書) | なし | 設計確定済みの書き下しのみ |

推奨実行モデル

工程推奨モデル理由
Step 1(バリデーション実装:302_order_status_handler.jshandleOrderStatusEdit_ + onEdit ディスパッチ追加)Claude Sonnet既存 onEdit パターンへの挿入位置特定・Utils.auditLog 引数構造の確認・複数行ペースト時の挙動設計が必要
Step 2(自動遷移実装:302_order_status_handler.jscheckAndFinalizeOrder_ + InvoiceRepository.save()/append() フック追加)Claude SonnetInvoiceRepository/OrderRepository の連携設計・冪等性設計・try/catch エラーハンドリングが必要
案B 採用時の DDL 追加(38_ord_status_logsetupAllSchemas に登録)Claude Haiku既存 LOG_AUDIT DDL の横展開。判断要素なし
_config.json の §E.2 への nav 登録Claude Haikuパターン化された 1 行追加

変更履歴

日付変更内容
2026-04-20初版作成(骨格〜注意事項まで)
2026-04-22Phase 2-3a〜2-4 を追記してエッジケース 18 件・実データ検証 5 件・関連ドキュメント 7 件・人間検討事項 7 件・実装プロンプト・推奨実行モデル・仕様書作成プロンプトを完成。docs/_config.json §E.2.21 に nav 登録

仕様書作成プロンプト

展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)では拡張思考をフル活用し、ファイル名・関数名・行番号・エッジケース一覧・Step 分割粒度を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 確定内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: 2-1(骨格)/ 2-2(概要〜注意事項)/ 2-3a(エッジケース〜人間検討事項)/ 2-3b(実装プロンプト〜変更履歴)/ 2-4(`<details>` プロンプト記録)に分割。1 回の Write/Edit は約 300 行以内。
4. **各 Step で何を書くかを具体指示**: Phase 2 実行時に設計判断を持ち込まない。Phase 1 で確定した内容のみ書き下す。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-53「発注ステータス遷移ワークフローの標準化」の開発仕様書を作成してください。
作成後、`docs/_config.json` の `nav` 配列の適切なセクション(§E.2 バグ修正・バリデーション相当)に必ず追記すること。

---

## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)

### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` で **S-53** の行を検索し、案件名・カテゴリ・Phase・優先度・概要・人間が検討すべき事項を取得する。

### 1-B: プロジェクト規約の確認
- `CLAUDE.md` の「コーディング規約」「GASファイル番号体系」「DDL管理外タブ」を再確認する。

### 1-C: 既存仕様書テンプレートの読み込み
- `docs/dev/dev_mas-075_expense_date_validation.md`(バリデーション追加案件)を 1 件読み込み、フォーマットを把握する。

### 1-D: 関連コードの調査(Grep は発見のみ・判断は必ず Read)

以下を **Read** で開き、型・関数名・行番号・呼び出し経路を確認する。推測で書かない。

| ファイル | 確認ポイント |
|---------|------------|
| `000_infra/003_contracts.js` | `OrderDTO` の全フィールド名(特に「発注ステータス」「発注残高(自動計算)」「参照元ID」の実在確認)|
| `000_infra/002_constants.js` | `SHEET_DEFAULTS` に `31_wrk_order` パターンのデフォルト値があるか。定数追加の挿入先行番号を特定 |
| `200_data/202_repository.js` | `OrderRepository.findAll()` / `OrderRepository.save()` / `InvoiceRepository.save()` の実装。`InvoiceRepository.append()` 内の処理フロー |
| `000_infra/004_utils.js` | `Utils.auditLog()` のシグネチャ(引数順・operation 許容値)。`98_audit_log` への書き込み方式(appendRow)|
| `300_ui/301_ui_assist.js` | `onEdit(e)` トリガー関数の実在確認・現在の実装パターン・他ハンドラへの委譲方式(**要 Read。ファイルが存在しない場合は実在するトリガー実装ファイルを Grep で特定する**)|
| `100_config/101_sys_config.js` | `setupAllSchemas()` のシート DDL 定義追加パターン(`LOG_AUDIT` 等の既存定義を参照)。メニュー `onOpen()` の構成 |
| `400_domain/410_subledger_engine.js` | `Action A`(仕訳生成)完了後に OrderRepository を更新するフックが既に存在するか確認 |

#### 調査で確定させる設計判断(Phase 2 実行前に全て Fix)

1. `onEdit` トリガーの実装ファイルパスと関数名(`300_ui/301_ui_assist.js` に実在するか、別ファイルか)
2. 新規ロジックファイルのファイル番号(`300_ui/` 配下で `301` の次に使える番号。CLAUDE.md 記載は `301_ui_assist` のみ → `302` が使えるか確認)
3. `38_ord_status_log` シートを新設するか、既存の `Utils.auditLog`(`98_audit_log`)で代替できるかの判断(`Utils.auditLog` は全操作共通ログ。発注専用の状態遷移履歴が必要か人間検討事項として残すか)
4. `OrderDTO` に実在する発注ステータスの現行値一覧(`SHEET_DEFAULTS` の `'発注ステータス': '見積中'` から出発し、他値を Grep で特定)
5. `InvoiceRepository` への「完納」自動遷移フックの挿入箇所(`save()` の末尾か、呼び出し元の上位関数か)

---

## Phase 2: 仕様書の分割作成

**出力先**: `docs/dev/dev_mas-125_order_status_workflow.md`

### Step 2-1: 骨格の作成(Write・約 20 行)

以下の見出しのみを書き出す(本文は空で可):

MAS-125: 発注ステータス遷移ワークフローの標準化

概要

目的

現在のコード

修正方針

Step 1: 状態遷移バリデーションとログ記録

Step 2: 「完納」への自動遷移

影響範囲

注意事項

エッジケース

実データ検証

関連ドキュメント

人間が検討すべき事項

実装プロンプト(Claude Code 用)

推奨実行モデル

変更履歴

仕様書作成プロンプト


### Step 2-2: 前半セクションの追記(Edit / Bash heredoc・約 300 行)

Phase 1 で確定した内容を書き下す(再調査・再考禁止)。記載内容:

**概要テーブル**: 案件ID / カテゴリ / Phase / 優先度 / 所要時間 / 対象ファイル(Phase 1 で確定したもの)/ 前提案件

**目的**: 受発注プロセスの状態遷移を厳格化し、不正な後戻り遷移を防止。変更履歴を監査証跡として記録することで内部統制・SOC2 対応の基盤を整備する。

**現在のコード**: `31_wrk_order` の「発注ステータス」列が自由入力で遷移制御がない旨を、`003_contracts.js` の `OrderDTO` 定義(実際のフィールド名で引用)と合わせて記載。

**修正方針 Step 1(バリデーション+ログ)**:
- **アーキテクチャ**: `onEdit(e)` トリガー(Phase 1 で確認した実在ファイル・関数名を引用)が `31_wrk_order` シートの「発注ステータス」列編集を検知 → Phase 1 で決定した新規ハンドラ関数(`300_ui/302_order_status_handler.js` 等。実在確認済みのファイル番号を使用)に委譲
- **遷移ルール定数** `VALID_TRANSITIONS`(`002_constants.js` への追加、または新規ハンドラファイル内に定義。Phase 1 で確認した挿入箇所を明記):
  ```javascript
  var VALID_TRANSITIONS = {
    '見積中':   ['発注済', 'キャンセル'],
    '発注済':   ['検収済', 'キャンセル'],
    '検収済':   ['完納'],
    '完納':     [],   // 終端
    'キャンセル': []  // 終端
  };

※ 実際のステータス値は Phase 1 の調査結果で上書きすること

  • バリデーション失敗時: SpreadsheetApp.getUi().alert() でエラー通知し、e.range.setValue(oldValue) で変更前値に戻す
  • ログ記録: Phase 1 の判断に従い、以下のいずれかを記載(判断未確定の場合は両案を人間検討事項へ):
    • 案A(既存流用): Utils.auditLog('UPDATE', '31_wrk_order', ordId, '発注ステータス', 'ORDER_STATUS_CHANGE', oldStatus, newStatus, '') を呼び出す
    • 案B(専用シート新設): 38_ord_status_log(DDL: 履歴ID / タイムスタンプ / 発注ID(ORD) / 変更者 / 変更前ステータス / 変更後ステータス / 備考)を新設し sheet.appendRow([...]) で記録。変更者は Session.getActiveUser().getEmail()

修正方針 Step 2(「完納」自動遷移):

  • トリガー: Phase 1 で確認した挿入箇所(InvoiceRepository.save() 末尾または上位の SubledgerService Action A 完了後)
  • ロジック: INV の 親発注ID(ORD) で同一 ORD に紐づく全 INV を InvoiceRepository.findAll() で取得 → 全件の 請求ステータス が「承認済」かつ OrderDTO発注残高(自動計算) が 0 → OrderRepository で当該 ORD のステータスを '完納' に更新
  • 対象 ORD の特定: OrderRepository.findAll()発注ID(ORD) を検索し、当該 dto のステータスを更新後 OrderRepository.save(dtos) で全置換

影響範囲テーブル: 変更ファイル / 変更量 / 影響の種類(Phase 1 で確定したファイル名・番号を使用)

注意事項:

  1. onEdit 内で OrderRepository.findAll() + save() を呼ぶとシート全体書き換えになりパフォーマンスに影響。まずは既存パターン踏襲で実装し、問題が出た場合は getRange().setValue() による行単位更新に切り替える
  2. onEdite.range が複数行にまたがる場合(コピー&ペースト)は e.range.getNumRows() > 1 で検知し、1 行ずつループしてバリデーションを実施。1 件でも不正な遷移があれば全体キャンセル
  3. 38_ord_status_log(案B の場合)は DDL 管理対象外タブとして 101_sys_config.jssetupAllSchemas() に追加し、CLAUDE.md の「DDL管理外タブ」リストにも追記する
  4. Utils.auditLog を使う場合(案A)は、operation 引数に 'UPDATE' を使用(004_utils.jsauditLog JSDoc に記載の許容値を Phase 1 で確認すること)

Step 2-3a: エッジケース〜人間検討事項の追記(Edit / Bash・約 200 行)

エッジケーステーブル:

条件表示・振る舞い理由
許可された遷移(例: 見積中発注済そのまま保存 + ログ記録VALID_TRANSITIONS に含まれる
禁止された遷移(例: 発注済見積中alert() 通知 + 元値に戻す後戻りは内部統制違反
終端ステータスからの遷移(完納 / キャンセル → 任意)alert() 通知 + 元値に戻すVALID_TRANSITIONS[status] = []
複数行コピー&ペースト1 行ずつループ。1 件でも不正があれば全行をロールバックし alert 表示e.range.getNumRows() で検知
手動での 完納 設定(残高が非ゼロ)警告ダイアログ表示後、ユーザー確認で続行可 / キャンセル可管理者による手動クローズを許容
自動「完納」遷移時に既に 完納 の場合スキップ(冪等)二重更新防止
発注残高(自動計算) の列名・計算式の実態Phase 1 の調査結果で補足OrderDTO フィールドが自動計算式の場合、save() で上書きしないよう注意

実データ検証: 不要(ロジック追加が主)。ただし Phase 1 で 発注ステータス の実データ値(見積中 以外の値が存在するか)を MCP で確認し、VALID_TRANSITIONS の起点ステータスを確定させること。

関連ドキュメント: CLAUDE.md の「コーディング規約 > データアクセス」「プロダクトポリシー > Human-in-the-Loop」

人間が検討すべき事項:

  1. TODO_future.md から転記した事項(Phase 1 で取得)
  2. 38_ord_status_log 新設 vs Utils.auditLog98_audit_log)流用の選択(専用ビュー vs 共通ログの保守性トレードオフ)
  3. 発注済検収済完納 の遷移において「差戻し」(例: 検収済発注済)を許可するか。許可する場合の承認プロセス(本仕様は禁止。解除する場合は VALID_TRANSITIONS 変更と別途 PR)
  4. LockService による排他制御の要否(onEdit の処理は軽量なため必須ではないが、Step 2 の自動遷移で findAll + save を行う場合は競合リスクを評価する)
  5. 発注残高(自動計算) がスプレッドシート数式によって計算される場合、OrderRepository.save() で DTO を書き戻すと数式が上書きされる可能性があることへの対処(列のクリア対象から除外する、または数式を再設定するロジックが必要)

Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit / Bash・約 250 行)

以下の内容を行頭 4 スペースインデント(バッククォート囲みなし)で記載する:

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 S-53「発注ステータス遷移ワークフローの標準化」を実装してください。

## 実行前タスク

以下をこの順番で Read し、確認ポイントを把握してから実装を開始すること:
1. `000_infra/003_contracts.js` — `OrderDTO` の全フィールド名(発注ステータス・発注残高(自動計算)の実在確認)
2. `000_infra/002_constants.js` — `SHEET_DEFAULTS` / `ID_PREFIX_MAP` のパターン。新定数の挿入位置を確認
3. `200_data/202_repository.js` — `OrderRepository.findAll()` / `OrderRepository.save()` / `InvoiceRepository.save()` のシグネチャと行番号
4. `000_infra/004_utils.js` — `Utils.auditLog()` の引数順と `operation` 許容値
5. `300_ui/301_ui_assist.js` — `onEdit(e)` の実装箇所・既存ハンドラへの委譲パターン(Grep で確認済みのファイル名を使用)
6. `100_config/101_sys_config.js` — `setupAllSchemas()` のシートDDL追加パターン(`LOG_AUDIT` 定義を参照)

## 修正対象ファイル(新規・変更)

- `300_ui/302_order_status_handler.js`(新規作成)— ステータス遷移バリデーション・ログ記録・完納自動遷移
- `300_ui/301_ui_assist.js`(要Read確認: 実在ファイル名・行番号で修正)— `onEdit(e)` から新ハンドラを呼び出す 1 行追加
- `100_config/101_sys_config.js` — `setupAllSchemas()` に `38_ord_status_log` DDL 追加(仕様書の案B を選択した場合のみ)
- `200_data/202_repository.js` — `InvoiceRepository.save()` 末尾に「完納」自動遷移呼び出しを追加(または仕様書で確定した挿入箇所)

## 実装内容

### (1) `300_ui/302_order_status_handler.js`(新規)

```javascript
// 発注ステータス遷移ルール(終端ステータスは空配列)
var VALID_TRANSITIONS = {
  '見積中':    ['発注済', 'キャンセル'],
  '発注済':    ['検収済', 'キャンセル'],
  '検収済':    ['完納'],
  '完納':      [],
  'キャンセル': []
};
// ※ Phase 1 で確認した実際のステータス値に合わせること

/**
 * onEdit から呼び出されるステータス遷移ハンドラ
 * @param {GoogleAppsScript.Events.SheetsOnEdit} e
 */
function handleOrderStatusEdit_(e) {
  // (a) 対象シート・列の絞り込み
  // (b) 複数行ペースト対応: e.range.getNumRows() > 1 の場合はループ
  // (c) VALID_TRANSITIONS で遷移可否チェック
  // (d) 不可の場合: SpreadsheetApp.getUi().alert() + e.range.setValue(oldValue)
  // (e) 可の場合: Utils.auditLog('UPDATE', '31_wrk_order', ordId, '発注ステータス', 'handleOrderStatusEdit_', oldStatus, newStatus)
  //     または 38_ord_status_log へ appendRow(仕様書の採用案に従う)
}

/**
 * 完納自動遷移チェック(InvoiceRepository.save() 後に呼び出す)
 * @param {string} ordId - 親発注ID (ORD_XXXXXX_XXXX)
 */
function checkAndFinalizeOrder_(ordId) {
  // (a) InvoiceRepository.findAll() で同一 ordId の INV を抽出
  // (b) 全件が承認済み かつ 発注残高(自動計算) が 0 かチェック
  // (c) 条件成立: OrderRepository.findAll() で該当 ORD の dto を取得
  //     → dto['発注ステータス'] = '完納' → OrderRepository.save(dtos)
  // (d) 既に '完納' の場合はスキップ(冪等)
}
```

## 制約

- `OrderDTO` に存在しない列名を参照しない。列名は必ず `003_contracts.js` の `@property` と一致させる
- `OrderRepository.save()` は全行書き換えのため、`発注残高(自動計算)` が数式セルの場合は上書きしないよう実装確認すること。数式上書きが発生する場合は人間にエスカレーションする
- `onEdit` 内では `SpreadsheetApp.getUi().alert()` を 1 回だけ呼ぶ(複数行ペーストでまとめてエラーを表示)
- 新規定数 `VALID_TRANSITIONS` の名前衝突を Grep で確認してから追加する

## 動作確認

1. `npm run push:dev` でデプロイ
2. `31_wrk_order` の任意行で「発注ステータス」を `見積中` → `発注済`(許可)に変更 → 正常保存・ログ記録されることを確認
3. 同じ行で `発注済` → `見積中`(禁止)に変更 → alert が表示され元の値に戻ることを確認
4. `完納` / `キャンセル` からの変更試行 → alert で拒否されることを確認
5. 複数行を選択して一括ペースト(一部不正遷移を含む)→ 全行が元の値に戻ることを確認
6. (案B の場合)`38_ord_status_log` に正しい形式でレコードが追記されることを確認
7. `InvoiceRepository.save()` を手動でトリガーし、残高ゼロ・全件承認済みの ORD が「完納」になることを確認

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 調査・設計(Phase 1相当) | あり | ファイル構造・遷移ルール確定 |
| 実装(Phase 2清書) | なし | 設計確定済みの書き下しのみ |

推奨実行モデルテーブル:

工程推奨モデル理由
Step 1(バリデーション実装)Claude Sonnet既存 onEdit パターンへの挿入位置特定・Utils.auditLog 引数確認が必要
Step 2(自動遷移実装)Claude SonnetInvoiceRepository/OrderRepository の連携設計が必要
DDL 追加・_config.json 登録Claude Haiku既存パターンの横展開。判断要素なし

変更履歴テーブル:

日付変更内容
2026-04-19初版作成

Step 2-4: 仕様書作成プロンプトの記録(Edit / Bash・最後尾の独立 Step)

末尾の ## 仕様書作成プロンプト セクションに以下を追記する:

<details><summary>展開して表示</summary>

(この <instruction>...</instruction>```

</details>