MAS-104: 支払依頼ワークフロー
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-104 |
| カテゴリ | ワークフロー |
| Phase | P3 |
| 優先度 | ★ |
| 所要時間 | 4〜6時間 |
| 対象ファイル | 400_domain/411_payment_workflow.js(新規)100_config/101_sys_config.js(onOpen() メニュー追加 + setupAllSchemas() DDL列追加)000_infra/003_contracts.js(InvoiceDTO @typedef コメントに 5 フィールド追記)000_infra/002_constants.js(SHEET_DEFAULTS の 32_wrk_invoice 請求ステータス初期値変更) |
| 前提案件 | MAS-179(監査証跡 Utils.auditLog)、MAS-192(Repository層) |
目的
経費・仕入の支払依頼を「申請 → 承認(または差戻し)→ 確定」の 2 段階で記録する簡易ワークフロー機能を提供する。現状は 32_wrk_invoice で 請求ステータス 列を誰でも直接書き換えられるため、「誰が申請し、誰が承認したか」の証跡が残らない。本案件では以下を実現する:
- 誰が申請したか(
申請者/申請日時)、誰が承認したか(承認者/承認日時)、差戻しの場合の理由(差戻し理由)を列として永続化する - 申請ステータスを
未申請 → 申請中 → 承認済 / 差戻しの有限状態機械として扱い、GAS メニュー経由のみで状態遷移できるようにする - 承認者は
03_sys_paramsのWORKFLOW_APPROVER_EMAIL(カンマ区切り可)で設定し、Session.getActiveUser().getEmail()と照合して権限判定する - 申請者と承認者が同一ユーザーになる自己承認を禁止する(代表1名体制では一時的に緩和できるよう「人間が検討すべき事項」に運用方針を残す)
- すべての状態遷移を
Utils.auditLog()で98_audit_logへ記録し、LockServiceで二重実行を防止する
将来の MAS-264(複雑な承認チェーン)や金額閾値による自動承認(WORKFLOW_AUTO_APPROVE_THRESHOLD キーを予約)への段階的拡張を想定した「簡易版」の実装と位置づける。
現在のコード
000_infra/003_contracts.js L39-67 — InvoiceDTO の現状
/**
* 32_wrk_invoice — 請求レコード
* @typedef {Object} InvoiceDTO
* @property {boolean} 有効フラグ
* @property {string} 請求ID(INV) - "INV_YYYYMMDD_NNNN"
* ...
* @property {string} 請求ステータス - "未処理" | "承認済" | "却下"
* ...
*/
現在の値セットは "未処理" | "承認済" | "却下" の 3 値のみで、「誰が申請したか/承認したか」を記録する列は存在しない。
000_infra/002_constants.js L85 — SHEET_DEFAULTS の 32_wrk_invoice エントリ
{ pattern: '32_wrk_invoice', defaults: { '請求ステータス': '未処理', '収支区分': '支出', '通貨': 'JPY', '申請種別': '請求書受領(AP)' } },
prefix フィールドは存在せず、ID_PREFIX_MAP 側で INV_ を発番している('32_wrk_invoice', prefix: 'INV_', digit: 4, isDate: true)。
100_config/101_sys_config.js L299-308 — 現行 onOpen()
function onOpen() {
const ui = SpreadsheetApp.getUi();
// 全操作は右側サイドバーに集約 (狭い画面での top-level メニュー切り詰めを回避)
ui.createMenu('🚀 BizLP')
.addItem('操作パネルを開く', 'openOperationsSidebar')
.addSeparator()
.addItem('✅ 自動起動を有効化', 'installAutoOpenSidebarTrigger')
.addItem('🚫 自動起動を無効化', 'uninstallAutoOpenSidebarTrigger')
.addToUi();
}
現状は「操作パネル」サイドバーに大半の機能を集約している。ワークフローも最終的にはサイドバーへ収容したいが、本案件では最小実装として top-level メニューを 1 つ追加する(本文 L302 ui.createMenu('🚀 BizLP') チェインの .addToUi() の 前 にサブメニューを追記する方式は取らず、addToUi() 後に新規 ui.createMenu() を追加する構造を採る)。
100_config/101_sys_config.js L654 — setupAllSchemas() の WRK_INVC DDL
'WRK_INVC': { headers: ["有効フラグ","請求ID(INV)","親発注ID(ORD)","起票日時","起票者","申請種別","発生日(P/L計上日)","決済日_計画","請求ステータス","収支区分","取引先名","PJ名","組織名","諸表区分","大分類","科目名","税区分","通貨","税抜金額_計画","消費税額_計画","税込金額_計画","未決済残高(自動計算)","決済手段","摘要","証憑URL","自動仕訳JNL_ID","決済日_実績"], color: "#1155cc" },
請求ステータス は 9 列目(index=8)、末尾は 決済日_実績(27 列目)。
100_config/101_sys_config.js L1034 — MST_DICT の 請求ステータス プルダウン値
['請求ステータス', [['INV_NEW','未処理'], ['INV_APR','承認済'], ['INV_PAR','部分決済'], ['INV_CMP','決済完了'], ['INV_CAN','取消']]],
プルダウン辞書には 未処理 / 承認済 / 部分決済 / 決済完了 / 取消 の 5 値がある。DTO 型定義の "未処理" | "承認済" | "却下" とは不整合で、実運用では部分決済・決済完了も使われる(32タブは「支払依頼ステータス」と「決済サイクルステータス」が混在している)。本案件では 「支払依頼ワークフロー」の 4 値(未申請 / 申請中 / 承認済 / 差戻し)を取り入れるが、部分決済 / 決済完了 / 取消 は決済エンジン側の自動遷移値として温存する。
InvoiceRepository.findAll() / save()(200_data/202_repository.js L163-177)
findAll: function() {
return readSheetAsDtos_(InvoiceRepository._getSheet());
},
save: function(dtos) {
var sheet = InvoiceRepository._getSheet();
if (!sheet) return;
var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0]
.map(function(h) { return String(h).trim(); });
writeDtosToSheet_(sheet, headers, dtos);
},
findAll は { headers, dtos } を返し、save は全行を writeDtosToSheet_ で全置換する(既存バリデーション維持)。本案件はこの API 契約を変更しない。
修正方針
Step 1: 000_infra/003_contracts.js — InvoiceDTO コメントの拡張(JSDoc のみ)
@typedef InvoiceDTO の 請求ステータス 行を差し替え、末尾に 5 行追加する:
* @property {string} 請求ステータス - "未申請" | "申請中" | "承認済" | "差戻し" | "部分決済" | "決済完了" | "取消"
* ...
* @property {string} 申請者 - Session.getActiveUser().getEmail()
* @property {Date} 申請日時 - new Date()
* @property {string} 承認者 - 承認操作者のメールアドレス
* @property {Date} 承認日時 - 承認または差戻しの実行時刻
* @property {string} 差戻し理由 - 差戻し時のみ。承認時は空
toDto / toRow はヘッダー名ベースで動作するためコード変更は不要。
Step 2: 000_infra/002_constants.js — デフォルト値の変更
SHEET_DEFAULTS の 32_wrk_invoice エントリ内:
// 変更前
'請求ステータス': '未処理'
// 変更後
'請求ステータス': '未申請'
影響: 新規 INV 行(RPA 経由・手動起票)が「未申請」で始まる。既存レコードには触れない(マイグレーションは「人間が検討すべき事項」で別途検討)。RPA 関連ファイル(400_domain/401_rpa_hc.js, 402_rpa_subscription.js, 403_rpa_capex.js, 404_rpa_finance.js, 405_rpa_adhoc.js, 406_rpa_pipeline.js 内の '請求ステータス': '未処理' ハードコード)は 本案件の範囲では変更しない(Action A/B エンジン側の '承認済' 文字列比較に依存しているため、後続マイグレーション案件で一括変換する)。
Step 3: 100_config/101_sys_config.js — メニューと DDL 追加
3a. onOpen() への追記(L307 .addToUi(); の直後)
ui.createMenu('💳 支払依頼ワークフロー')
.addItem('選択行を申請', 'applyForPayment')
.addItem('選択行を承認', 'approvePayment')
.addItem('選択行を差戻し', 'rejectPayment')
.addToUi();
3b. setupAllSchemas() の WRK_INVC 定義拡張(L654)
末尾に 5 列追加(決済日_実績 の後ろ):
'WRK_INVC': { headers: ["有効フラグ","請求ID(INV)","親発注ID(ORD)","起票日時","起票者","申請種別","発生日(P/L計上日)","決済日_計画","請求ステータス","収支区分","取引先名","PJ名","組織名","諸表区分","大分類","科目名","税区分","通貨","税抜金額_計画","消費税額_計画","税込金額_計画","未決済残高(自動計算)","決済手段","摘要","証憑URL","自動仕訳JNL_ID","決済日_実績","申請者","申請日時","承認者","承認日時","差戻し理由"], color: "#1155cc" },
3c. MST_DICT の 請求ステータス プルダウン拡張(L1034)
['請求ステータス', [['INV_DRF','未申請'], ['INV_REQ','申請中'], ['INV_NEW','未処理'], ['INV_APR','承認済'], ['INV_REJ','差戻し'], ['INV_PAR','部分決済'], ['INV_CMP','決済完了'], ['INV_CAN','取消']]],
Step 4: 400_domain/411_payment_workflow.js(新規作成)
公開関数 3 本(メニューから直接呼び出される GAS グローバル関数)と内部ヘルパー 3 本を定義する。
公開関数 (3)
| 関数 | ステータス遷移 | auditLog operation |
|---|---|---|
applyForPayment() | 未申請 → 申請中 | UPDATE |
approvePayment() | 申請中 → 承認済 | CONFIRM |
rejectPayment() | 申請中 → 差戻し | CANCEL |
内部ヘルパー (3)
getActiveInvoiceDto_()— 選択行の請求ID(INV)を特定し、InvoiceRepository.findAll()から DTO を返す。複数行選択時は先頭行のみを対象とし Toast 通知するgetApproverEmails_()—Constants.getParam('WORKFLOW_APPROVER_EMAIL', '')を読み取り、カンマ区切りをArray<string>に分解する。空なら[]sendWorkflowNotification_(dto, eventType, recipients, extra)—MailApp.sendEmailで件名・本文を組み立てて通知送信。eventTypeは'APPLIED' | 'APPROVED' | 'REJECTED'。クォータ超過時はUtils.logErrorで記録してスキップ(処理自体は継続)
データ読み書きパターン(必ず踏襲)
var result = InvoiceRepository.findAll(); // { headers, dtos }
var dto = result.dtos.find(function(d) { return d['請求ID(INV)'] === targetId; });
// dto を更新(プロパティ直接代入)...
dto['請求ステータス'] = '申請中';
dto['申請者'] = Session.getActiveUser().getEmail();
dto['申請日時'] = new Date();
InvoiceRepository.save(result.dtos);
対象行の INV ID は以下で取得:
var sheet = SpreadsheetApp.getActiveSheet();
if (sheet.getName() !== '32_wrk_invoice') {
SpreadsheetApp.getUi().alert('32_wrk_invoice タブで実行してください。');
return;
}
var rowNum = sheet.getActiveRange().getRow();
if (rowNum < 2) { /* ヘッダー行対策 */ return; }
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var idCol = headers.indexOf('請求ID(INV)');
var targetId = String(sheet.getRange(rowNum, idCol + 1).getValue()).trim();
if (!targetId) { SpreadsheetApp.getUi().alert('請求ID(INV) が空の行は処理できません。'); return; }
承認者メールアドレスの読み取り
// Constants.getParam() を使用する(Utils.getParam は存在しない)
var raw = Constants.getParam('WORKFLOW_APPROVER_EMAIL', '');
var approvers = String(raw).split(',').map(function(s) { return s.trim(); }).filter(Boolean);
if (approvers.length === 0) {
SpreadsheetApp.getUi().alert('承認者が設定されていません。03_sys_params に WORKFLOW_APPROVER_EMAIL を登録してください。');
return;
}
Utils.auditLog() の使用(引数順を厳守)
// 申請
Utils.auditLog('UPDATE', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'applyForPayment', '未申請', '申請中', '');
// 承認
Utils.auditLog('CONFIRM', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'approvePayment', '申請中', '承認済', '');
// 差戻し
Utils.auditLog('CANCEL', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'rejectPayment', '申請中', '差戻し', reason);
LockService による二重実行防止
var lock = LockService.getScriptLock();
try {
lock.waitLock(30000); // タイムアウト時は例外
var result = InvoiceRepository.findAll(); // ロック後に再読込
var dto = result.dtos.find(function(d) { return d['請求ID(INV)'] === targetId; });
if (!dto) {
SpreadsheetApp.getUi().alert('対象の INV が見つかりません。画面を更新して再試行してください。');
return;
}
if (dto['請求ステータス'] !== expectedStatus) {
SpreadsheetApp.getUi().alert('ステータスが変更されています(現在: ' + dto['請求ステータス'] + ')。画面を更新して再試行してください。');
return;
}
// ... 処理 ...
InvoiceRepository.save(result.dtos);
} catch (e) {
if (e.message && e.message.indexOf('waitLock') !== -1) {
SpreadsheetApp.getUi().alert('他の処理が実行中です。しばらく待ってから再試行してください。');
} else {
Utils.logError('applyForPayment', e);
throw e;
}
} finally {
try { lock.releaseLock(); } catch (_) {}
}
差戻し理由入力
var ui = SpreadsheetApp.getUi();
var response = ui.prompt('差戻し理由を入力してください', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() !== ui.Button.OK) {
ui.alert('差戻し理由が未入力のため処理を中断しました。');
return;
}
var reason = String(response.getResponseText() || '').trim();
if (!reason) {
ui.alert('差戻し理由が未入力のため処理を中断しました。');
return;
}
通知メール送信(MailApp.sendEmail)
var url = SpreadsheetApp.getActiveSpreadsheet().getUrl();
var subject = '[支払依頼] ' + String(dto['契約・件名'] || dto['摘要'] || '(無題)');
var body =
'申請者: ' + (dto['申請者'] || '') + '\n' +
'件名: ' + (dto['契約・件名'] || dto['摘要'] || '') + '\n' +
'金額(税込): ' + (dto['税込金額_計画'] || 0) + '\n' +
'取引先: ' + (dto['取引先名'] || '') + '\n' +
'請求ID: ' + dto['請求ID(INV)'] + '\n' +
'URL: ' + url;
try {
approvers.forEach(function(to) { MailApp.sendEmail(to, subject, body); });
} catch (mailErr) {
// クォータ超過等はログのみ記録。申請処理そのものは成立させる
Utils.logError('sendWorkflowNotification_', mailErr, 'approvers=' + approvers.join(','));
}
影響範囲
| 層 | ファイル | 変更内容 |
|---|---|---|
| ドメイン(新規) | 400_domain/411_payment_workflow.js | 新規作成。公開関数 3 + 内部ヘルパー 3 |
| UI・トリガー | 100_config/101_sys_config.js | onOpen() にメニュー追加、setupAllSchemas() の WRK_INVC DDL 拡張、MST_DICT プルダウン拡張 |
| 契約 | 000_infra/003_contracts.js | InvoiceDTO の @typedef に 5 フィールド追記 + ステータス値セット拡張 |
| 定数 | 000_infra/002_constants.js | SHEET_DEFAULTS['32_wrk_invoice'] の 請求ステータス 初期値を '未処理'→'未申請' |
| Repository | 200_data/202_repository.js | 変更なし(findAll / save のシグネチャ・ヘルパーを変更しない) |
| 監査ログ | 000_infra/004_utils.js | 変更なし(Utils.auditLog を既存シグネチャのまま利用) |
注意事項
InvoiceRepository.findAll()/save()の戻り値形式・引数を 変更しないこと。202_repository.js の内部ヘルパーreadSheetAsDtos_/writeDtosToSheet_にも手を入れない請求ステータス値セットの変更('未処理'→'未申請'、'却下'→'差戻し')は既存データと非互換となる 破壊的変更。本案件のスコープでは 新規 INV のデフォルト値のみ変更し、既存レコードへのマイグレーションは別案件(800_ops に809_migration_s32_status_rename.js等で冪等実装)として「人間が検討すべき事項」に明記する33_wrk_bankの消込処理・600_report/のデータマート処理は請求ステータス === '承認済'を依存条件としており(410_subledger_engine.jsAction A など)、本案件では'承認済'値を 温存する。新規追加する値(未申請/申請中/差戻し)は AP ワークフロー固有 のため、決済エンジン側は従来通り'未処理'/'承認済'を参照する。ただし、RPA 生成 INV のデフォルトが'未申請'になることで Action A の起動条件('承認済'比較)を満たさなくなる可能性がある点は 要検証(Phase 1 Grep で'承認済'ハードコードが410_subledger_engine.js複数箇所と900_test/901_test_runner.jsにあることを確認済。本案件のスコープでは RPA 起票 INV は'未処理'のまま温存し、手動起票かつ「支払依頼」用途の INV のみ新ワークフローを適用する運用とする)Session.getActiveUser().getEmail()は Workspace 組織内でないと空文字を返す場合がある。空の場合は'SYSTEM'で代替せず、エラーダイアログで中断する(申請者の記録は監査上必須)03_sys_paramsにWORKFLOW_APPROVER_EMAILが未登録の場合は処理開始時にエラーダイアログを表示して中断する。Constants._paramsCacheは起動時に 1 回だけ読み込むため、パラメータ追加後は GAS エディタでSpreadsheetApp.getActive()を再起動するか、Constants._paramsCache = null;でキャッシュクリアが必要MailApp.sendEmailの日次クォータ(Workspace 無料枠 100 通/日、Workspace 有料 1500 通/日)を超えた場合、Exception: Service invoked too many times for one day: email.がスローされる。本案件では 通知失敗時も状態遷移は成立させる(try/catchで握りつぶしてUtils.logErrorに記録)方針を採用する- メニューは
onOpen()のトップレベルに追加するが、最終形ではサイドバー(templates/operations_sidebar.html)へ統合すべき。本案件のスコープ外として扱う - 複数行選択時は 先頭行のみ処理し、Toast で「先頭行のみ処理しました」と通知する(複数 INV 一括処理は多重
LockService取得の原子性が担保できないため将来課題とする)
エッジケース
| # | 操作 | 条件 | 動作 |
|---|---|---|---|
| E01 | 申請 | 請求ステータス !== '未申請' | 「この依頼はすでに申請済みまたは処理済みです(現在: <ステータス>)」を表示して中断 |
| E02 | 申請 | 税込金額_計画 <= 0(Number() 化後) | 「金額が 0 以下のため申請できません」を表示して中断 |
| E03 | 申請 | 取引先名 または 科目名 が空文字・空白のみ | 「必須項目(取引先名・科目名)が未入力です」を表示して中断 |
| E04 | 申請 | 有効フラグ === false または 'FALSE' | 「無効行のため申請できません」を表示して中断 |
| E05 | 申請 | Session.getActiveUser().getEmail() が空文字 | 「ユーザー情報が取得できません。GAS の認可状態を確認してください」を表示して中断 |
| E06 | 承認 / 差戻し | 請求ステータス !== '申請中' | 「この依頼は申請中ではありません(現在: <ステータス>)」を表示して中断 |
| E07 | 承認 / 差戻し | 操作者メールが WORKFLOW_APPROVER_EMAIL の配列に非含 | 「承認権限がありません」を表示して中断 |
| E08 | 承認 / 差戻し | 操作者メール === 申請者 列の値(自己承認) | 「申請者自身は承認できません」を表示して中断 |
| E09 | 差戻し | ui.prompt() が Cancel / Close | 「差戻し理由が未入力のため処理を中断しました」を表示して中断 |
| E10 | 差戻し | prompt() 入力が空文字 or 空白のみ | 「差戻し理由が未入力のため処理を中断しました」を表示して中断 |
| E11 | 共通 | LockService.waitLock(30000) タイムアウト | 「他の処理が実行中です。しばらく待ってから再試行してください」を表示して中断 |
| E12 | 共通 | WORKFLOW_APPROVER_EMAIL 未設定(Constants.getParam が空文字を返却) | 「承認者が設定されていません。03_sys_params に WORKFLOW_APPROVER_EMAIL を登録してください」を表示して中断 |
| E13 | 共通 | アクティブシートが 32_wrk_invoice でない | 「32_wrk_invoice タブで実行してください」を表示して中断 |
| E14 | 共通 | 選択行がヘッダー行(rowNum < 2) | 「ヘッダー行は処理できません。データ行を選択してください」を表示して中断 |
| E15 | 共通 | 選択行の 請求ID(INV) が空 | 「請求ID(INV) が空の行は処理できません」を表示して中断 |
| E16 | 共通 | 複数行選択時 | 先頭行(選択範囲の最初の行)のみ処理し、完了後 ss.toast('先頭行のみ処理しました', '...') で通知 |
| E17 | 共通 | MailApp.sendEmail がクォータ超過で例外 | Utils.logError に記録しつつ状態遷移は完了させる。Toast で「通知メール送信に失敗しました(ログ参照)」を表示 |
| E18 | 共通 | ロック取得後の再読込で対象 INV が見つからない(削除・ID改変) | 「対象の INV が見つかりません。画面を更新して再試行してください」を表示して中断 |
| E19 | 共通 | setupAllSchemas() 未実行で 5 列が存在しない | DTO プロパティに代入しても書き戻し時に無視されるため、事前に DDL を再実行しておく(「人間が検討すべき事項」7 参照) |
実データ検証
以下は MCP / スプレッドシート直接確認 で実装前に確認すべき項目:
32_wrk_invoiceの実データ分布:請求ステータス = '未処理'の件数(マイグレーション規模の見積もり)請求ステータス = '却下'の件数(差戻しへのリネーム対象)請求ステータス = '承認済'の件数(決済エンジンの依存先、触らない対象)
請求ステータスの値リネームが影響する箇所(Phase 1 Grep 確認済。本案件では変更しないが記録として残す):400_domain/410_subledger_engine.jsL193, L303, L652 —'承認済'文字列比較(Action A 起動条件)600_report/601_datamart_ingest.jsL40 — データマート取込時のステータス参照600_report/606_datamart_daily_cf.jsL227 — 日次 CF 予測200_data/201_data_validator.jsL471 — データ整合性チェック900_test/901_test_runner.jsL258, L329, L478 — テスト判定条件400_domain/401_rpa_hc.js/402_rpa_subscription.js/403_rpa_capex.js/404_rpa_finance.js/405_rpa_adhoc.js/406_rpa_pipeline.js— RPA 生成 INV が'請求ステータス': '未処理'をハードコード
03_sys_paramsにWORKFLOW_APPROVER_EMAILキーが未登録の場合、DDL 実行または手動追加で登録する03_sys_paramsにWORKFLOW_AUTO_APPROVE_THRESHOLDキー(金額閾値自動承認、本案件では参照しない)を予約登録することを推奨
関連ドキュメント
- C.1 TODO_future.md MAS-104 — 案件の元要件・期待効果
- C.1 TODO_future.md MAS-264 — 多段階承認(Out of Scope、将来 MAS-104 で簡易版を扱う旨の参照元)
- C.1 TODO_future.md MAS-125 — 発注ステータス遷移ワークフロー標準化(MAS-104 の兄弟案件、共通基盤化を検討)
- C.1 TODO_future.md MAS-128 — 発注承認ワークフロー(金額閾値ベース、MAS-104 との共通化検討)
- E.1.8 MAS-179 監査証跡 —
Utils.auditLog()の仕様 - E.1.7 MAS-192 Repository完全移行 —
InvoiceRepositoryの設計方針
人間が検討すべき事項
1. 既存レコードのマイグレーション方針(最優先)
請求ステータス = '未処理' の既存レコード数が多い場合、以下のいずれかを選択する必要がある:
| 選択肢 | メリット | デメリット |
|---|---|---|
a. 放置(新規 INV のみ '未申請' で始まる) | 既存コードへの影響なし | '未処理' と '未申請' が混在して運用者が混乱 |
b. 一括リネーム(800_ops/809_migration_s32_status_rename.js を新規作成) | 値セットが統一される | 410_subledger_engine.js の '承認済' 比較や RPA 側の '未処理' ハードコードとの整合確認が必要 |
c. 決済サイクルと支払依頼で列を分離(請求ステータス に加えて 支払依頼ステータス 列を新設) | 責務が分離され既存コードに影響なし | 列が増えて運用者の認知負荷が上がる |
推奨: c(列分離)。ただし本案件では段階的に a(放置)から始め、運用で必要性が確認されたら c に移行する。
2. 本ワークフローの適用対象(申請種別 の範囲)
TODO_future.md には「経費や仕入の支払依頼」とあるが、具体的な 申請種別 の範囲が定められていない。以下を代表者と合意する:
申請種別 = '請求書受領(AP)'(APL_AP): 対象(支払先からの請求書受領)申請種別 = '経費精算(社員立替)'(APL_EX): 対象(立替精算の社員申請)申請種別 = '月額給与・報酬支払'(APL_HC): 対象外(HC RPA で自動生成、承認不要)申請種別 = '財務仕訳(振替等)'(APL_JE): 対象外(振替のみ、承認不要)申請種別 = '自動引落'(APL_DD)/'手動振込'(APL_TR): 要検討
現在の実装では 申請種別 でフィルタせずに全 INV を対象としている。申請種別 IN ('請求書受領(AP)','経費精算(社員立替)') でガードするかを決定する必要がある。
3. 自己承認の運用
代表 1 名体制では申請者 = 承認者になるケースが発生する。現仕様は E08 で禁止しているが、以下の選択肢がある:
- a. 禁止(現仕様): 代表 1 名体制では運用不能になる
- b. 警告のみ:
ui.alert(..., ButtonSet.YES_NO)で確認を取って進める - c.
WORKFLOW_SELF_APPROVAL_ALLOWED(Boolean パラメータ)で切替: 組織規模に応じて柔軟に変更可能
推奨: c。03_sys_params の WORKFLOW_SELF_APPROVAL_ALLOWED = true/false で制御する。
4. MailApp.sendEmail クォータ超過時の挙動
日次クォータ(無料 100 通 / 有料 1500 通)超過時、以下のいずれかを選ぶ:
- a. 通知失敗でも状態遷移は成立させる(現仕様): 業務を止めない
- b. 通知失敗時はロールバックして中断: 承認プロセスの完全性を優先
- c. 通知キューを
03_sys_paramsに記録し、バッチで再送: 実装コスト高
推奨: a。ログに記録して運用者が後日確認できるようにする。
5. 将来の金額閾値自動承認(WORKFLOW_AUTO_APPROVE_THRESHOLD)
現時点では参照しないが、将来 税込金額_計画 < 閾値 の場合は申請と同時に自動承認する仕様を検討する。キーだけ 03_sys_params に予約登録しておく:
WORKFLOW_AUTO_APPROVE_THRESHOLD = 100000 // 10万円以下は自動承認(将来実装)
6. 多段階承認への拡張(MAS-128 / MAS-125 との共通化)
本案件は「申請→承認」の 1 段階のみ。将来、部門承認→経営承認などの 2 段階が必要になった場合、以下の設計変更が必要:
承認者/承認日時/差戻し理由を承認者1/承認者2/承認日時1/承認日時2に拡張- 状態を
申請中 → 一次承認済 → 最終承認済に拡張 - MAS-125(発注ステータス遷移ワークフロー)、MAS-128(発注承認ワークフロー)との共通基盤化
この段階で 400_domain/411_payment_workflow.js は 400_domain/411_workflow_engine.js に汎化(発注 / 請求で共通の状態遷移エンジン)を検討する。
7. DDL 再実行の必要性
本案件は setupAllSchemas() の WRK_INVC ヘッダーに 5 列を追加するため、既存スプレッドシートで setupAllSchemas() を再実行しなければ新列が表示されない。dev 環境で実行 → 動作確認 → prod 環境で実行の順序を厳守する(CLAUDE.md の「動作未確認のコードを GitHub に push しない」原則)。
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-104「支払依頼ワークフロー」を実装してください。
## 実行前タスク(Read で実在コードを確認してから実装すること)
- `000_infra/003_contracts.js` を Read し、InvoiceDTO の全フィールド名と請求ステータス値セットを確認する
- `000_infra/002_constants.js` を Read し、SHEET_DEFAULTS の 32_wrk_invoice エントリの現在の形式を確認する
- `200_data/202_repository.js` を Read し、InvoiceRepository.findAll() の戻り値形式({ headers, dtos })と save() の引数を確認する
- `000_infra/004_utils.js` を Read し、Utils.auditLog() の引数順(8引数: operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)を確認する。Constants.getParam() は 002_constants.js 側で定義されている(Utils.getParam は存在しない)ことも確認する
- `100_config/101_sys_config.js` を Read し、onOpen() の既存メニュー末尾(L307 `.addToUi();`)の文字列と setupAllSchemas() 内の 32_wrk_invoice / WRK_INVC DDL 定義(L654)を確認する
## 修正対象ファイル
- 新規作成: `400_domain/411_payment_workflow.js`
- 変更: `100_config/101_sys_config.js`
- onOpen() に「💳 支払依頼ワークフロー」メニューを追加(L307 `.addToUi();` の直後に新規 `ui.createMenu()` を追加)
- setupAllSchemas() の WRK_INVC ヘッダー配列(L654)末尾に 5 列追加("申請者","申請日時","承認者","承認日時","差戻し理由")
- MST_DICT の 請求ステータス プルダウン値(L1034)に 3 行追加(INV_DRF=未申請, INV_REQ=申請中, INV_REJ=差戻し)
- 変更: `000_infra/003_contracts.js`(InvoiceDTO の @typedef コメントに 5 フィールド追記 + 請求ステータス値セット拡張)
- 変更: `000_infra/002_constants.js`(SHEET_DEFAULTS の 32_wrk_invoice エントリの 請求ステータス デフォルト値を '未処理' → '未申請' に変更)
## 実装内容
### 411_payment_workflow.js の構成
- グローバル関数: applyForPayment(), approvePayment(), rejectPayment()(onOpen メニューから直接呼び出す)
- 各関数は LockService.getScriptLock().waitLock(30000) を try/finally でラップする
- ロック取得後に InvoiceRepository.findAll() で再読み込みし、対象 DTO のステータスをガード節で検証する
- 承認者メールは Constants.getParam('WORKFLOW_APPROVER_EMAIL', '') で取得(Utils.getParam ではない)
- 現在の操作者は Session.getActiveUser().getEmail() で取得する
- auditLog の operation 値: 申請='UPDATE'、承認='CONFIRM'、差戻し='CANCEL'
- auditLog 引数順: (operation, '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 関数名, 変更前値, 変更後値, note)
- 内部ヘルパー: getActiveInvoiceDto_(), getApproverEmails_(), sendWorkflowNotification_(dto, eventType, recipients, extra)
### applyForPayment() の骨格
```javascript
function applyForPayment() {
var ui = SpreadsheetApp.getUi();
var lock = LockService.getScriptLock();
try {
lock.waitLock(30000);
var sheet = SpreadsheetApp.getActiveSheet();
if (sheet.getName() !== '32_wrk_invoice') { ui.alert('32_wrk_invoice タブで実行してください。'); return; }
var rowNum = sheet.getActiveRange().getRow();
if (rowNum < 2) { ui.alert('ヘッダー行は処理できません。データ行を選択してください。'); return; }
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var idCol = headers.indexOf('請求ID(INV)');
var targetId = String(sheet.getRange(rowNum, idCol + 1).getValue()).trim();
if (!targetId) { ui.alert('請求ID(INV) が空の行は処理できません。'); return; }
var applicant = Session.getActiveUser().getEmail();
if (!applicant) { ui.alert('ユーザー情報が取得できません。GAS の認可状態を確認してください。'); return; }
var approvers = getApproverEmails_();
if (approvers.length === 0) { ui.alert('承認者が設定されていません。03_sys_params に WORKFLOW_APPROVER_EMAIL を登録してください。'); return; }
var result = InvoiceRepository.findAll();
var dto = result.dtos.find(function(d) { return d['請求ID(INV)'] === targetId; });
if (!dto) { ui.alert('対象の INV が見つかりません。画面を更新して再試行してください。'); return; }
if (dto['有効フラグ'] === false || String(dto['有効フラグ']).toUpperCase() === 'FALSE') { ui.alert('無効行のため申請できません。'); return; }
if (dto['請求ステータス'] !== '未申請') { ui.alert('この依頼はすでに申請済みまたは処理済みです(現在: ' + dto['請求ステータス'] + ')'); return; }
if (Number(dto['税込金額_計画']) <= 0) { ui.alert('金額が 0 以下のため申請できません。'); return; }
if (!String(dto['取引先名'] || '').trim() || !String(dto['科目名'] || '').trim()) { ui.alert('必須項目(取引先名・科目名)が未入力です。'); return; }
dto['請求ステータス'] = '申請中';
dto['申請者'] = applicant;
dto['申請日時'] = new Date();
InvoiceRepository.save(result.dtos);
Utils.auditLog('UPDATE', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'applyForPayment', '未申請', '申請中', '');
sendWorkflowNotification_(dto, 'APPLIED', approvers, '');
Utils.toastResult('applyForPayment', '申請を送信しました: ' + dto['請求ID(INV)']);
} catch (e) {
if (e && e.message && e.message.indexOf('waitLock') !== -1) {
ui.alert('他の処理が実行中です。しばらく待ってから再試行してください。');
} else {
Utils.logError('applyForPayment', e);
throw e;
}
} finally {
try { lock.releaseLock(); } catch (_) {}
}
}
```
approvePayment() と rejectPayment() も同じ骨格(タブ検証→ロック→再読込→ガード節→状態更新→save→auditLog→通知→Toast)で実装する。rejectPayment() は ui.prompt() で差戻し理由を取得し、Cancel または空入力で中断する(E09 / E10)。
### onOpen() へのメニュー追加(101_sys_config.js L307 `.addToUi();` の直後に挿入)
```javascript
ui.createMenu('💳 支払依頼ワークフロー')
.addItem('選択行を申請', 'applyForPayment')
.addItem('選択行を承認', 'approvePayment')
.addItem('選択行を差戻し', 'rejectPayment')
.addToUi();
```
### setupAllSchemas() WRK_INVC ヘッダー拡張(L654)
既存 `"決済日_実績"` の直後に 5 列を追加:
```
"決済日_実績","申請者","申請日時","承認者","承認日時","差戻し理由"
```
### MST_DICT 請求ステータス プルダウン拡張(L1034)
```javascript
['請求ステータス', [['INV_DRF','未申請'], ['INV_REQ','申請中'], ['INV_NEW','未処理'], ['INV_APR','承認済'], ['INV_REJ','差戻し'], ['INV_PAR','部分決済'], ['INV_CMP','決済完了'], ['INV_CAN','取消']]],
```
### InvoiceDTO 型定義の更新(003_contracts.js L49)
```
* @property {string} 請求ステータス - "未申請" | "申請中" | "承認済" | "差戻し" | "部分決済" | "決済完了" | "取消"
```
末尾(L66 の `自動仕訳JNL_ID` の後)に 5 行追加:
```
* @property {string} 申請者 - Session.getActiveUser().getEmail()
* @property {Date} 申請日時 - new Date()
* @property {string} 承認者 - 承認操作者のメールアドレス
* @property {Date} 承認日時 - 承認または差戻しの実行時刻
* @property {string} 差戻し理由 - 差戻し時のみ。承認時は空
```
### SHEET_DEFAULTS 変更(002_constants.js L85)
```javascript
// 変更前
{ pattern: '32_wrk_invoice', defaults: { '請求ステータス': '未処理', ... } },
// 変更後
{ pattern: '32_wrk_invoice', defaults: { '請求ステータス': '未申請', ... } },
```
## 制約
- InvoiceRepository.findAll() / save() のシグネチャを変更しないこと
- 202_repository.js 内の readSheetAsDtos_、writeDtosToSheet_ を変更しないこと
- 400_domain/401_rpa_hc.js 〜 406_rpa_pipeline.js 内の `'請求ステータス': '未処理'` ハードコードは **触らない**(RPA 生成 INV は従来通り `'未処理'` で起票、Action A の `'承認済'` 遷移を維持)
- 410_subledger_engine.js の `'承認済'` 文字列比較は **触らない**
- 推測でコードを書かず、必ず Read で確認してから実装すること
## エッジケース(仕様書のエッジケーステーブル E01-E19 と完全一致させること)
- 申請: ステータス≠'未申請'、金額≤0、必須項目空欄、有効フラグ=false、ユーザーメール空 → それぞれエラーメッセージを表示して処理中断
- 承認/差戻し: ステータス≠'申請中'、承認権限なし、申請者=操作者 → それぞれエラーメッセージを表示して処理中断
- 差戻し: prompt() がキャンセルまたは空入力 → 処理中断
- LockService タイムアウト: ユーザー通知して中断
- WORKFLOW_APPROVER_EMAIL 未設定: ユーザー通知して中断
- タブ検証: 32_wrk_invoice 以外で実行 → 中断
- 行検証: ヘッダー行選択、INV_ID 空、ロック後に INV 消失 → 中断
- MailApp クォータ超過: ログのみ記録、状態遷移は完了
- 複数行選択: 先頭行のみ処理、Toast で通知
## 動作確認
1. `npm run push:dev` を実行してデプロイする
2. 開発用スプレッドシートで `onOpen()` を手動再実行(タブ再読込)し、「💳 支払依頼ワークフロー」メニューが表示されることを確認
3. `setupAllSchemas()` を実行し、32_wrk_invoice に 申請者/申請日時/承認者/承認日時/差戻し理由 の 5 列が追加されたことを確認
4. `03_sys_params` に `WORKFLOW_APPROVER_EMAIL` を自分のメールアドレスで登録
5. 32_wrk_invoice で `請求ステータス='未申請'` の INV レコードを 1 行選択する(必要なら手動で新規追加)
6. メニュー「💳 支払依頼ワークフロー」→「選択行を申請」を実行し、ステータスが「申請中」に変わること、申請者・申請日時が記録されることを確認する
7. 別ユーザー(または WORKFLOW_APPROVER_EMAIL に別のアドレスを設定して申請者と分離)で「選択行を承認」を実行し、ステータスが「承認済」になり承認者・承認日時が記録されることを確認する
8. 申請者と同一ユーザーで承認操作し、「申請者自身は承認できません」エラーが表示されることを確認する(E08)
9. 別の申請中 INV で「選択行を差戻し」を実行し、prompt で理由を入力 → ステータスが「差戻し」、差戻し理由が記録されることを確認する
10. prompt でキャンセル / 空入力して E09 / E10 が発火することを確認
11. 98_audit_log を開き、UPDATE / CONFIRM / CANCEL のログが記録されていることを確認する
12. `npm run push:prod` は自分で実行せず、動作確認後にユーザーへ依頼する
13. prod 反映後、ユーザーに `setupAllSchemas()` の手動実行を依頼する(DDL 再実行なしでは新列が表示されない)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read・Grep) | あり | 挿入位置・引数順・値セットを確定させる |
| 実装(Write・Edit) | なし | 確定済み内容の書き下しに徹する |
推奨実行モデル
| Step | 内容 | 推奨モデル | 理由 |
|---|---|---|---|
| 実行前タスク | Read 6 ファイル + Grep | Sonnet | 中程度の判断(挿入位置特定・既存パターン確認) |
| Step 1 (003_contracts.js) | @typedef コメント追記のみ | Haiku | 定義が完全に確定、機械的編集 |
| Step 2 (002_constants.js) | デフォルト値の 1 箇所変更 | Haiku | 文字列 1 箇所の置換 |
| Step 3 (101_sys_config.js) | onOpen() + setupAllSchemas() + MST_DICT の 3 箇所編集 | Sonnet | 既存メニュー構造への追記、配列末尾への追加の判断が必要 |
| Step 4 (411_payment_workflow.js) | 新規ファイル作成(公開 3 + ヘルパー 3) | Sonnet | エッジケース分岐が多い。ただし仕様書で骨格が確定済のため Opus は不要 |
| 動作確認 | dev デプロイ・UI 操作・監査ログ確認 | Sonnet | ユーザー対話、検証 |
変更履歴
| 日時 | 変更内容 |
|---|---|
| 2026-04-19 | 初版作成 |
仕様書作成プロンプト
仕様書作成プロンプト(展開して表示)
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
- 拡張思考の使い分け: Phase 1(設計)では拡張思考をフル活用し、ファイル名・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
- テキスト報告の禁止: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
- 4-5 分割の Write/Edit 実行: 2-1(骨格)/2-2(概要〜注意事項)/2-3a(エッジケース〜人間検討事項)/2-3b(実装プロンプト〜変更履歴)/2-4(
<details>プロンプト記録)に分割。1 回の Write/Edit は概ね 300 行以内。 - 各 Step で何を書くかを具体指示: Phase 2 実行時に設計判断を持ち込まない。Phase 1 で確定した内容のみ書き出す。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 MAS-104「支払依頼ワークフロー」の開発仕様書を作成してください。
作成後は docs/_config.json の nav 配列の適切なセクションに必ず追記してください。
Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
以下を全て Read/Grep で調査・確定してから Phase 2 に進む。推測で書かない。
docs/_internal/TODO_future.md— MAS-104 の概要・期待効果・人間が検討すべき事項を取得する。000_infra/003_contracts.js—InvoiceDTOの全フィールド名と現在の請求ステータス値セット("未処理" | "承認済" | "却下"であることを確認)を Read する。000_infra/002_constants.js—SHEET_DEFAULTSの32_wrk_invoiceエントリ(現在の請求ステータスデフォルト値は'未処理'、prefixフィールドなし)とID_PREFIX_MAPの32_wrk_invoiceエントリを Read する。200_data/202_repository.js—InvoiceRepository.findAll()/save()の実装と、内部ヘルパーreadSheetAsDtos_/writeDtosToSheet_のシグネチャを Read する。000_infra/004_utils.js—Utils.auditLog()の引数順(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)とConstants.getParam()ではなくConstants.getParamが002_constants.js側で定義されていることを確認する(Utils.getParamは存在しない)。100_config/101_sys_config.js—onOpen()の既存メニュー構造(メニュー名・サブメニュー名の文字列を正確に Read する)とsetupAllSchemas()内の32_wrk_invoiceスキーマ定義(列リスト・請求ステータスプルダウン値)を Read する。docs/dev/配下の既存仕様書を 1 件 Read してフォーマットを把握する(dev_mas-075_expense_date_validation.mdまたはdev_mas-094_boundary_month_selector.mdを推奨)。
Phase 1 で確定すべき事項(確定前に Phase 2 に進まないこと)
101_sys_config.jsのonOpen()に追加するメニュー文字列の正確な形式setupAllSchemas()の32_wrk_invoice定義への列追加の挿入位置(行番号)- 実在する
申請種別の値セット('請求書受領(AP)'等)から、本ワークフローの適用対象を確認 400_domain/ディレクトリ内の既存ファイル番号(410_subledger_engine.js,420_project_profitability.jsが確認済みのため、新規ファイルは411_payment_workflow.jsで可)
Phase 2: 仕様書の分割作成
出力先: docs/dev/dev_mas-104_payment_workflow.md(ファイル名の S は大文字)
1 回の tool_use は概ね 300 行以内。設計判断は Phase 1 確定済みのもののみ使用する。
Step 2-1: 骨格の作成(Write)
見出しのみ。本文は空で可。約 20 行。
セクション構成:
# S-32: 支払依頼ワークフロー
## 概要
## 目的
## 現在のコード
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト
Step 2-2: 概要〜注意事項の追記(Edit または Bash heredoc)
約 300 行。以下の内容を Phase 1 確定済みの情報で記述する。
概要テーブル: 案件ID=MAS-104、対象ファイル(400_domain/411_payment_workflow.js[新規]、100_config/101_sys_config.js、000_infra/003_contracts.js、000_infra/002_constants.js)を記載する。
修正方針(以下をアーキテクト指示として記述する):
- 新規作成ファイル:
400_domain/411_payment_workflow.js- 公開関数:
applyForPayment(),approvePayment(),rejectPayment()(メニューから直接呼び出し可能なグローバル関数として定義) - 内部ヘルパー:
getActiveInvoiceDto_(),getApproverEmails_(),sendWorkflowNotification_()
- 公開関数:
100_config/101_sys_config.js:onOpen()に「💳 支払依頼ワークフロー」メニューを追加。サブメニュー:「選択行を申請」(applyForPayment)、「選択行を承認」(approvePayment)、「選択行を差戻し」(rejectPayment)。挿入位置は Phase 1 で Read した既存メニュー末尾に追記setupAllSchemas()の32_wrk_invoiceDDL に列追加(申請者,申請日時,承認者,承認日時,差戻し理由)と請求ステータスプルダウン値を拡張
000_infra/003_contracts.js:InvoiceDTOの@typedefコメントに 5 フィールドを追記(実装コードの変更ではなく JSDoc コメントの更新のみ)000_infra/002_constants.js:SHEET_DEFAULTSの32_wrk_invoiceエントリ内'請求ステータス': '未処理'を'未申請'に変更
データ読み書きパターン(必ず踏襲すること):
// InvoiceRepository を用いた全件読み込み → メモリ上更新 → 全件書き戻し
var result = InvoiceRepository.findAll(); // { headers, dtos }
var dto = result.dtos.find(function(d) { return d['請求ID(INV)'] === targetId; });
// dto を更新 ...
InvoiceRepository.save(result.dtos);
対象行の INV ID は SpreadsheetApp.getActiveSheet().getActiveRange().getRow() で選択行番号を取得し、同行の INV ID 列(ヘッダー '請求ID(INV)')から読み取る。
承認者メールアドレスの読み取り:
// Constants.getParam() を使用する(Utils.getParam は存在しない)
var raw = Constants.getParam('WORKFLOW_APPROVER_EMAIL', '');
var approvers = raw.split(',').map(function(s) { return s.trim(); }).filter(Boolean);
Utils.auditLog() の使用(引数順を厳守すること):
- 申請:
Utils.auditLog('UPDATE', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'applyForPayment', '未申請', '申請中', '') - 承認:
Utils.auditLog('CONFIRM', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'approvePayment', '申請中', '承認済', '') - 差戻し:
Utils.auditLog('CANCEL', '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 'rejectPayment', '申請中', '差戻し', reason)
LockService による二重実行防止:
var lock = LockService.getScriptLock();
try {
lock.waitLock(30000); // タイムアウト時は例外をスロー → catch でユーザー通知
var result = InvoiceRepository.findAll(); // ロック後に再読み込みして最新状態を検証
var dto = result.dtos.find(...);
if (!dto || dto['請求ステータス'] !== expectedStatus) {
SpreadsheetApp.getUi().alert('ステータスが変更されています。画面を更新して再試行してください。');
return;
}
// ... 処理 ...
} catch (e) {
if (e.message && e.message.includes('waitLock')) {
SpreadsheetApp.getUi().alert('他の処理が実行中です。しばらく待ってから再試行してください。');
} else {
throw e;
}
} finally {
lock.releaseLock();
}
差戻し理由入力:
var response = SpreadsheetApp.getUi().prompt('差戻し理由を入力してください');
if (response.getSelectedButton() !== SpreadsheetApp.getUi().Button.OK || !response.getResponseText().trim()) {
SpreadsheetApp.getUi().alert('差戻し理由が未入力のため処理を中断しました。');
return;
}
var reason = response.getResponseText().trim();
通知メール送信(MailApp.sendEmail を使用):
var url = SpreadsheetApp.getActiveSpreadsheet().getUrl();
var body = '申請者: ' + applicant + '\n件名: ' + dto['契約・件名'] + '\n金額(税込): ' + dto['税込金額_計画'] + '\n取引先: ' + dto['取引先名'] + '\nURL: ' + url;
approvers.forEach(function(to) { MailApp.sendEmail(to, '[支払依頼] ' + dto['契約・件名'], body); });
注意事項:
InvoiceRepository.findAll()/save()のシグネチャ・内部ヘルパー(readSheetAsDtos_,writeDtosToSheet_)を変更しないこと請求ステータス値セットの変更('未処理'→'未申請','却下'→'差戻し')は既存データとの非互換が生じる破壊的変更。「人間が検討すべき事項」で必ず言及すること33_wrk_bankの消込処理や600_report/のマート処理が請求ステータス値を参照していないか、Phase 1 で Grep 確認すること(未確認なら「要調査」と明記)
Step 2-3a: エッジケース〜人間が検討すべき事項の追記(Edit または Bash)
約 200 行。
エッジケーステーブル:
| 操作 | 条件 | 動作 |
|---|---|---|
| 申請 | 請求ステータス !== '未申請' | 「この依頼はすでに申請済みまたは処理済みです」を表示して中断 |
| 申請 | 税込金額_計画 <= 0 | 「金額が0以下のため申請できません」を表示して中断 |
| 申請 | 取引先名 または 科目名 が空 | 「必須項目(取引先名・科目名)が未入力です」を表示して中断 |
| 申請 | 有効フラグ === false | 「無効行のため申請できません」を表示して中断 |
| 承認/差戻し | 請求ステータス !== '申請中' | 「この依頼は申請中ではありません」を表示して中断 |
| 承認/差戻し | 操作者メールが WORKFLOW_APPROVER_EMAIL に非含 | 「承認権限がありません」を表示して中断 |
| 承認/差戻し | 操作者メール === 申請者 列の値 | 「申請者自身は承認できません」を表示して中断 |
| 差戻し | prompt() 入力が空またはキャンセル | 「差戻し理由が未入力のため処理を中断しました」を表示して中断 |
| 共通 | LockService.waitLock タイムアウト | 「他の処理が実行中です。しばらく待ってから再試行してください」を表示して中断 |
| 共通 | WORKFLOW_APPROVER_EMAIL 未設定(空文字) | 「承認者が設定されていません。03_sys_params に WORKFLOW_APPROVER_EMAIL を登録してください」を表示して中断 |
| 共通 | 複数行選択時 | 先頭行(選択範囲の最初の行)のみ処理し、完了後にその旨を Toast 通知する(仕様書で明記) |
実データ検証(MCP で確認が必要な項目):
32_wrk_invoiceの実データに'未処理'/'却下'が格納されているレコード件数を確認し、マイグレーション規模を見積もる33_wrk_bankや600_report/で請求ステータスの値を文字列比較している箇所を Grep し、影響範囲を確定する03_sys_paramsにWORKFLOW_APPROVER_EMAILキーが未登録の場合、DDL 実行または手動追加が必要
人間が検討すべき事項:
請求ステータス値セット変更('未処理'→'未申請','却下'→'差戻し')と既存レコードのマイグレーション方針(800_ops/にマイグレーションスクリプトを追加するか)- 本ワークフローが適用される
申請種別の範囲('請求書受領(AP)'のみか全申請種別か)— TODO_future.md に記載がない場合は即実装不可、要確認 MailApp.sendEmailの日次送信クォータ(100通/日)超過時の挙動(申請を失敗させるか、メール送信エラーを無視して申請は成功させるか)WORKFLOW_AUTO_APPROVE_THRESHOLDキーを03_sys_paramsに登録しておく(将来の金額閾値自動承認に備える)が、現実装では参照しない- 承認フローが 1 段階(申請 → 承認)で足りるか、多段階承認(例: 部門承認 → 経営承認)が将来必要か
Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit または Bash)
約 250 行。実装プロンプトはバッククォートで囲まず、行頭 4 スペースインデントで出力すること。
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-104「支払依頼ワークフロー」を実装してください。
## 実行前タスク(Read で実在コードを確認してから実装すること)
- `000_infra/003_contracts.js` を Read し、InvoiceDTO の全フィールド名と請求ステータス値セットを確認する
- `000_infra/002_constants.js` を Read し、SHEET_DEFAULTS の 32_wrk_invoice エントリの現在の形式を確認する
- `200_data/202_repository.js` を Read し、InvoiceRepository.findAll() の戻り値形式({ headers, dtos })と save() の引数を確認する
- `000_infra/004_utils.js` を Read し、Utils.auditLog() の引数順(8引数)を確認する。Constants.getParam() は 002_constants.js 側で定義されていることも確認する
- `100_config/101_sys_config.js` を Read し、onOpen() の既存メニュー末尾の文字列と setupAllSchemas() 内の 32_wrk_invoice DDL 定義を確認する
## 修正対象ファイル
- 新規作成: `400_domain/411_payment_workflow.js`
- 変更: `100_config/101_sys_config.js`(onOpen() にメニュー追加、setupAllSchemas() に列追加)
- 変更: `000_infra/003_contracts.js`(InvoiceDTO の @typedef コメントに 5 フィールド追記)
- 変更: `000_infra/002_constants.js`(SHEET_DEFAULTS の 32_wrk_invoice エントリの請求ステータスデフォルト値を '未処理' → '未申請' に変更)
## 実装内容
### 411_payment_workflow.js の構成
- グローバル関数: applyForPayment(), approvePayment(), rejectPayment()(onOpen メニューから直接呼び出す)
- 各関数は LockService.getScriptLock().waitLock(30000) を try/finally でラップする
- ロック取得後に InvoiceRepository.findAll() で再読み込みし、対象 DTO のステータスをガード節で検証する
- 承認者メールは Constants.getParam('WORKFLOW_APPROVER_EMAIL', '') で取得(Utils.getParam ではない)
- 現在の操作者は Session.getActiveUser().getEmail() で取得する
- auditLog の operation 値: 申請='UPDATE'、承認='CONFIRM'、差戻し='CANCEL'
- auditLog 引数順: (operation, '32_wrk_invoice', dto['請求ID(INV)'], '請求ステータス', 関数名, 変更前値, 変更後値, '')
### onOpen() へのメニュー追加
- Read で確認した既存メニュー末尾に追記する
- メニュー名: '💳 支払依頼ワークフロー'
- サブメニュー: '選択行を申請' → applyForPayment、'選択行を承認' → approvePayment、'選択行を差戻し' → rejectPayment
## 制約
- InvoiceRepository.findAll() / save() のシグネチャを変更しないこと
- 202_repository.js 内の readSheetAsDtos_、writeDtosToSheet_ を変更しないこと
- 推測でコードを書かず、必ず Read で確認してから実装すること
## エッジケース(仕様書のエッジケーステーブルと完全一致させること)
- 申請: ステータス≠'未申請'、金額≤0、必須項目空欄、有効フラグ=false → それぞれエラーメッセージを表示して処理中断
- 承認/差戻し: ステータス≠'申請中'、承認権限なし、申請者=操作者 → それぞれエラーメッセージを表示して処理中断
- 差戻し: prompt() がキャンセルまたは空入力 → 処理中断
- LockService タイムアウト: ユーザー通知して中断
- WORKFLOW_APPROVER_EMAIL 未設定: ユーザー通知して中断
## 動作確認
1. npm run push:dev を実行してデプロイする
2. 開発用スプレッドシートの 32_wrk_invoice タブでステータス='未申請'の INV レコードを 1 行選択する
3. メニュー「💳 支払依頼ワークフロー」→「選択行を申請」を実行し、ステータスが「申請中」に変わることを確認する
4. 承認者メールアドレスのユーザー(または同一ユーザーで WORKFLOW_APPROVER_EMAIL に自分のアドレスを設定)で「選択行を承認」を実行し、ステータスが「承認済」になることを確認する
5. 申請者と同一ユーザーで承認操作し、「申請者自身は承認できません」エラーが表示されることを確認する
6. 98_audit_log を開き、CONFIRM / CANCEL のログが記録されていることを確認する
7. npm run push:prod は自分で実行せず、動作確認後にユーザーへ依頼する
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read・Grep) | あり | 挿入位置・引数順・値セットを確定させる |
| 実装(Write・Edit) | なし | 確定済み内容の書き下しに徹する |
変更履歴テーブル: | YYYY-MM-DD | 初版作成 | を記載する。
Step 2-4: 仕様書作成プロンプトの記録(Edit または Bash)
末尾に以下の形式で追記する:
<details><summary>仕様書作成プロンプト(展開して表示)</summary>
(この <instruction> 全文をそのまま貼り付ける)
</details>
Phase 3: _config.json への追記とコミット
docs/_config.jsonのnav配列に追記する。セクションは案件の性質(新機能・ワークフロー)に応じて §E.2(バリデーション拡張)または §E.6(ワークフロー・RPA)から選択する:{ "file": "dev/dev_mas-104_payment_workflow.md", "title": "E.X.X S-32 支払依頼ワークフロー" }docs/_internal/changelog.mdの先頭(ヘッダー直後)に追記する:| YYYY-MM-DD | [dev_mas-104_payment_workflow.md](dev_mas-104_payment_workflow.md) | 初版作成。支払依頼ワークフロー仕様書 |- コミット&プッシュ:
git add docs/dev/dev_mas-104_payment_workflow.md docs/_config.json docs/_internal/changelog.md git commit -m "docs: S-32 支払依頼ワークフローの開発仕様書を作成" git push -u origin {現在のブランチ名}