概要

項目内容
案件IDMAS-100
案件名請求書発行(見積書・納品書・請求書PDF生成)
カテゴリ請求管理(新機能)
Phase実装
優先度P2(★★)
実装ステータス📝 仕様書段階・実装未着手 (2026-04-28 監査時点)
対象ファイル100_config/101_sys_config.jssetupAllSchemasBUD_PIPE 列追加 + templates/operations_sidebar.html への導線)/ 400_domain/409_document_service.js(新規)/ 300_ui/302_ui_document.html(新規)/ templates/operations_sidebar.html(ボタン1行追記)
前提案件なし(既存の InvoiceRepository.append()Contracts.InvoiceDTO を再利用するのみ)

目的

21_bud_pipeline で管理される受注確定前後の売上パイプラインデータ(取引先名・PJ・案件名・計上開始年月・スポット売上・継続月額 MRR 等)から 見積書・納品書・請求書の 3 種類の PDF を Google ドキュメントテンプレート経由で生成し、発行確定時に同データを 32_wrk_invoice収支区分=収入InvoiceDTO として登録する AR(売上債権)発行ワークフローを実装する。現状は 400_domain/406_rpa_pipeline.jsgeneratePipelineInvoices()RPA 側で自動起票するのみで、顧客向けに送付する請求書 PDF の生成機能が存在しない。本案件で売上サイクル(受注→見積→納品→請求→売掛→入金)の書類発行フェーズを自動化し、手作業の二重管理を排除する。

現在のコード

InvoiceRepository.append() の振る舞い(200_data/202_repository.js L185-207)

append: function(dtos) {
  var sheet = InvoiceRepository._getSheet();
  if (!sheet) return 0;
  var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0]
    .map(function(h) { return String(h).trim(); });

  // 科目マスタから諸表区分・大分類を自動付与
  var acctMap = AccountRepository.findAsMap();
  for (var i = 0; i < dtos.length; i++) {
    var acc = String(dtos[i]['科目名'] || '').trim();
    var mapped = acc ? acctMap[acc] : null;
    if (mapped) {
      dtos[i]['諸表区分'] = mapped.stmt;
      dtos[i]['大分類'] = mapped.cat;
    }
    if (!dtos[i]['PJ名']) dtos[i]['PJ名'] = '指定なし_共通費など';
  }

  return appendDtosToSheet_(sheet, headers, dtos, 0);
},
  • AccountRepository.findAsMap()科目名 → {stmt, cat} を引き、11_mst_account に登録された科目名しか正しくマッピングされない(未登録時は 諸表区分/大分類 が空のまま書き込まれる)。
  • appendDtosToSheet_(sheet, headers, dtos, 0) の第4引数 lastRowCol=0 は A列(有効フラグ)で最終行を判定する。CLAUDE.md の「B列=ID列で判定」の方針と異なるが、既存実装はこのまま維持する

Constants.getParam(key, defaultVal)000_infra/002_constants.js L147-167)

  • 03_sys_params シートを1回だけスキャンして _paramsCache にキャッシュし、以降はキー検索のみ。
  • 戻り値の型: defaultValnumber なら Number(val)、それ以外は String(val)。空文字・nullundefineddefaultVal をそのまま返す。
  • テンプレートID(CFG_TEMPLATE_ID_QUOTE 等)は文字列なので Constants.getParam('CFG_TEMPLATE_ID_QUOTE', '') で取得し、空なら処理中断する。

Contracts.InvoiceDTO の型定義(000_infra/003_contracts.js L38-67)

@property {string} 申請種別       - "請求書受領(AP)" | "経費申請(EX)" 等
@property {string} 請求ステータス - "未処理" | "承認済" | "却下"
@property {string} 収支区分       - "収入" | "支出"
  • 申請種別 の既存値(AR側)は 406_rpa_pipeline.js の L178 / L219 で 請求書発行(AR) を使用しており、本案件でも同じ値を再利用する(新値を追加しない)。
  • 請求ステータス は 3 値のいずれか。本案件は Human-in-the-Loop(HITL)で確定時に 未処理 をセットし、後段 Action BSubledgerService)で承認・消込する。

Constants.ID_PREFIX_MAP32_wrk_invoice エントリ(L96)

{ pattern: '32_wrk_invoice', prefix: 'INV_', digit: 4, isDate: true }
  • RpaCommon.generateInvId(dateStr, offset)INV_YYYYMMDD_NNNN 形式の ID を発番する。既存の 406_rpa_pipeline.js と同じ採番機構を再利用する。

Constants.SHEET_DEFAULTS21_bud_pipeline エントリ(L77)

{ pattern: '21_bud_pipeline', prefix: 'PIP_', defaults: {
  '契約形態': 'スポット(狩猟)', '売上科目': '売上高', '確度(ヨミ)': 'Aヨミ (確度80%)',
  '継続月数': 1, '取引先名': '', '決済手段': '', 'CF計上': '予算',
  '入金ラグ(月)': 1, 'スポット売上・初期費用': 0, '継続月額(MRR)': 0,
  _dynamic: { '計上開始年月': 'nextYm' }
} }
  • 本案件では defaults請求ステータス='未処理' を追加する。

setupAllSchemasBUD_PIPE DDL 定義(100_config/101_sys_config.js L662)

'BUD_PIPE': { headers: [
  "有効フラグ","管理ID","PJ・案件名","契約形態","売上科目","確度(ヨミ)",
  "計上開始年月","スポット売上・初期費用","継続月額(MRR)","継続月数",
  "取引先名","決済手段","CF計上","入金ラグ(月)","入金日","休日調整",
  "組織名","起票ターゲット月","最終起票年月日","備考"
], color: "#e69138" },
  • 現在の列数は 20列。末尾 備考直前に 5 列(請求ステータス, 発行済INV_ID, 見積書URL, 納品書URL, 請求書URL)を追加する方針。406_rpa_pipeline.jsh.indexOf(name) でヘッダー名参照のため、列挿入によるインデックスずれは発生しない。

onOpen() の実在メニュー構造(100_config/101_sys_config.js L299-308)

function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('🚀 BizLP')
    .addItem('操作パネルを開く', 'openOperationsSidebar')
    .addSeparator()
    .addItem('✅ 自動起動を有効化', 'installAutoOpenSidebarTrigger')
    .addItem('🚫 自動起動を無効化', 'uninstallAutoOpenSidebarTrigger')
    .addToUi();
}
  • 実在するメニューは 🚀 BizLP の単一トップレベルのみで、すべての操作は templates/operations_sidebar.html に集約されている(openOperationsSidebar() で起動)。本案件の「選択行から帳票を発行」ボタンは templates/operations_sidebar.html のセクションに新規追加する(onOpen() の直接変更ではない)。

400_domain/406_rpa_pipeline.js との干渉

  • 既存 generatePipelineInvoices()21_bud_pipeline確度(ヨミ) に「受注」を含む行のみを対象に、計上開始年月最終起票年月日 から INV を月次 RPA 自動生成する。
  • 本案件の手動発行 UI は 選択行 1 行を対象とし、発行済INV_ID 列が空の行のみ発行可能(二重発行防止)。
  • 406_rpa_pipeline.js が生成する INV は 申請種別='請求書発行(AR)' + 摘要 【RPA:PIPE】YYYY-MM PJ名 スポット/MRR Nヶ月目、本案件が生成する INV は 申請種別='請求書発行(AR)' + 摘要 【DOC:S-28】YYYY-MM-DD PJ名 発行摘要プレフィックスの違いで両者を識別する。
  • 同一案件に対して 406 RPA と本案件 UI の両方で INV が生成されないよう、本案件 UI で発行確定した行は 発行済INV_ID 列が埋まるため、406 RPA 側でその行をスキップするガードを入れる(修正方針 Step 1 の DDL 追加列を利用)。

修正方針

Step 1: 21_bud_pipeline DDL 変更(100_config/101_sys_config.js

setupAllSchemas'BUD_PIPE' エントリ(L662)の headers 配列の 備考 の直前に以下 5 列を挿入する:

列名役割
請求ステータスtext未処理 / 見積発行済 / 請求済 の3値。デフォルト 未処理
発行済INV_IDtext本案件 UI で発行確定した INV_YYYYMMDD_NNNN。二重発行防止キー
見積書URLtextDrive の PDF へのリンク
納品書URLtextDrive の PDF へのリンク
請求書URLtextDrive の PDF へのリンク

変更後の headers は 25 列となる。併せて:

  • Constants.SHEET_DEFAULTS21_bud_pipeline エントリに '請求ステータス': '未処理' を追加する(000_infra/002_constants.js L77)。
  • 406_rpa_pipeline.js 冒頭の ['有効フラグ', '管理ID', ...] の列名リスト(L27-30)に '発行済INV_ID' を追加し、その列が空文字でない行はRPA スキップする追加ガードを for ループ内(L83 付近)に挿入する(本案件 UI で発行済み案件の RPA 重複起票防止)。

Step 2: 400_domain/409_document_service.js 新規作成

ファイル番号選定理由: 400_domain/ で使用中の番号は 400/401/402/403/404/405/406/407/410/420 + 408(MAS-090 408_withholding_tax.js として仕様書完了済、未実装)。MAS-090 との番号衝突を避けるため 409 を採用する。

公開名前空間: DocumentServiceContracts / RpaCommon と同じ var + 関数マップ形式)。

主要関数:

関数引数返値役割
DocumentService.generateDrafts(pipeRowIdx)選択行の1始まり行番号{ quoteUrl, deliveryUrl, invoiceUrl, draftPayload }Stage 1: PDF 3 種を Drive 一時フォルダに生成しプレビュー URL を返す。32_wrk_invoice への書込は行わない
DocumentService.confirmAndRegister(draftPayload)Stage 1 の戻り値 draftPayload{ invId, pipeUpdated }Stage 2: InvoiceRepository.append([dto]) で INV 登録し、21_bud_pipeline の該当行に 発行済INV_ID / 請求ステータス='請求済' / 3種URL を書き戻す
DocumentService._nextInvoiceNumber_()顧客向け請求書番号CFG_INVOICE_NUMBER_PREFIX + CFG_INVOICE_NUMBER_LAST の連番。採番後は 03_sys_paramsCFG_INVOICE_NUMBER_LAST+1 して永続化(PropertiesService は使わず 03_sys_params シート直接更新)
DocumentService._renderPdf_(templateId, placeholders, fileName)テンプレID・プレースホルダー辞書・出力ファイル名Drive の PDF URLGoogle ドキュメントを DriveApp.getFileById().makeCopy()DocumentApp.openById() で各プレースホルダー {{会社名}} {{請求書番号}} {{金額_税込}} 等を body.replaceText() で置換 → file.getAs(MimeType.PDF) → 親フォルダにPDF保存 → 一時の Docs コピーは DriveApp.removeFile() で削除

プレースホルダー一覧(最小セット): {{会社名}} / {{請求先住所}} / {{請求書番号}} / {{発行日}} / {{PJ名}} / {{金額_税抜}} / {{消費税額}} / {{金額_税込}} / {{決済予定日}} / {{T番号}} / {{発行元社名}}

排他制御: confirmAndRegister() の先頭で LockService.getScriptLock().waitLock(10000) を実行し、取得失敗時はエラーダイアログ表示で中断する。

監査ログ: confirmAndRegister() の成功直後に Utils.auditLog('CREATE', '32_wrk_invoice', invId, '', 'DocumentService.confirmAndRegister', null, dto, '発行元=21_bud_pipeline 行=' + pipeRowIdx) を呼ぶ。

Step 3: UI サイドバー実装

新規 HTML: 300_ui/302_ui_document.html。単一モーダル的なダイアログではなく専用サイドバーとして実装し、以下の 2 段階フローを UI 側で制御する。

  • Stage 1(ドラフト生成): 「選択行から帳票を発行」ボタン → google.script.run.withSuccessHandler(...).withFailureHandler(...).documentServiceGenerateDrafts(activeRowIdx) を実行。成功時は 3 種 PDF のプレビューリンクをカード形式で表示。
  • Stage 2(確定登録): Stage 1 成功後に有効化される「確定して請求レコードを登録」ボタン → google.script.run.documentServiceConfirmAndRegister(draftPayload)。成功時は 32_wrk_invoiceINV_... ID と 21_bud_pipeline 更新行を結果領域に表示。

templates/operations_sidebar.html 側には既存の「📒 経理業務」セクションに 1行ボタンを追加し、押下で新サイドバーを開く:

<button class="btn" onclick="google.script.run.openDocumentGenerationSidebar()">📄 帳票発行(見積・納品・請求)</button>

サーバーサイド関数 openDocumentGenerationSidebar()101_sys_config.js に新設し、HtmlService.createHtmlOutputFromFile('300_ui/302_ui_document').setTitle('📄 帳票発行').setWidth(380) を返す。

Step 4: 32_wrk_invoice への収入INVレコード登録(409_document_service.js 内)

confirmAndRegister() 内で以下の InvoiceDTO を構築し InvoiceRepository.append([dto]) に渡す:

フィールド
有効フラグtrue
請求ID(INV)RpaCommon.generateInvId(dateStr, 0)INV_YYYYMMDD_NNNN を採番
親発注ID(ORD)Step 2-1 の draftPayload に含まれる既存 ORD_ID(31_wrk_order参照元ID=PIP_xxx で引き当て)。なければ空文字
起票日時new Date()
起票者Session.getActiveUser().getEmail() または '手動発行(S-28)'
申請種別'請求書発行(AR)' — 既存値再利用(406_rpa_pipeline.js と同一)
発生日(P/L計上日)21_bud_pipeline.計上開始年月 の月末日
決済日_計画入金ラグ(月) + 入金日 で算出(406_rpa_pipeline.jscalcSettleDate() と同じロジックを DocumentService 内のヘルパに切り出して再利用)
請求ステータス'未処理'
収支区分'収入'
取引先名21_bud_pipeline.取引先名
PJ名21_bud_pipeline.PJ・案件名(空なら InvoiceRepository.append 内で 指定なし_共通費など に自動補完)
科目名21_bud_pipeline.売上科目(デフォルト 売上高
税区分'対象外'(MAS-083 消費税対応がマージされるまで既存 RPA と同じ扱い)
通貨'JPY'
税抜金額_計画スポット売上・初期費用 + 継続月額(MRR) × 継続月数 を Stage 1 の draftPayload で算出した値
消費税額_計画0(MAS-083 対応前)
税込金額_計画税抜と同額
決済手段21_bud_pipeline.決済手段
組織名21_bud_pipeline.組織名
摘要'【DOC:S-28】' + YYYY-MM-DD + ' ' + PJ名 + ' 発行'
証憑URL請求書URL(Drive の請求書 PDF リンク)

排他制御: LockService.getScriptLock().waitLock(10000)監査ログ: Utils.auditLog('CREATE', '32_wrk_invoice', invId, '', 'DocumentService.confirmAndRegister', null, dto)

Step 5: 21_bud_pipeline のステータス更新

confirmAndRegister() の末尾で、選択行のヘッダーを sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0] で取得し、indexOf で列位置を都度特定して sheet.getRange(pipeRowIdx, col+1).setValue(...) で書き込む(列番号ハードコード禁止)。

書き込み対象: 発行済INV_ID=invId / 請求ステータス='請求済' / 見積書URL=quoteUrl / 納品書URL=deliveryUrl / 請求書URL=invoiceUrl

更新前後それぞれ Utils.auditLog('UPDATE', '21_bud_pipeline', pipeMgrId, '発行済INV_ID', 'DocumentService.confirmAndRegister', '', invId) を記録する。

影響範囲

  • 新規ファイル 2件: 400_domain/409_document_service.js(サーバーサイド)/ 300_ui/302_ui_document.html(クライアント)。
  • 既存ファイル変更 2件:
    • 100_config/101_sys_config.jssetupAllSchemas.BUD_PIPE に 5 列追加、openDocumentGenerationSidebar() 関数新設。
    • 400_domain/406_rpa_pipeline.js発行済INV_ID スキップガード 1 行追加。
    • 000_infra/002_constants.jsSHEET_DEFAULTS.21_bud_pipeline.defaults請求ステータス デフォルト追加。
  • 既存ファイル変更 1件(templates/operations_sidebar.html — 「📒 経理業務」セクションに「📄 帳票発行」ボタン 1 行追加。
  • InvoiceRepository.append() / Contracts.InvoiceDTO / Utils.auditLog()改変しない。既存 API を呼び出すのみ。

注意事項

  1. InvoiceRepository.append()dtos[i]['科目名']11_mst_account に登録済みであることが前提(AccountRepository.findAsMap() の戻り値でマッピング)。収入INVの 科目名21_bud_pipeline.売上科目 列の値をそのまま転記し、既定値は 売上高本実装前に 11_mst_account売上高有効フラグ=TRUE で登録されていることを MCP 経由で確認する(Phase 1-F の実データ検証項目)。
  2. InvoiceDTO.申請種別 の既存値はAP側(請求書受領(AP) / 経費申請(EX) 等)だが、AR側の値として 406_rpa_pipeline.js請求書発行(AR) を使用済み。本案件も同じ値 請求書発行(AR) を再利用し、新値は追加しない。
  3. appendDtosToSheet_(sheet, headers, dtos, 0) は A列(有効フラグ)で最終行を判定する。CLAUDE.md の「B列=ID列で判定」規約とは異なるが、InvoiceRepository.append() の既存実装はこの挙動なので本案件では変更しない(変更すると他のRPAにも影響)。
  4. Google ドキュメントの makeCopyopenByIdbody.replaceTextPDF エクスポート1 回あたり約 3〜8 秒を要する。3 種連続実行で合計 15〜25 秒が目安。GAS 実行時間上限(6分)までに余裕があるが、Stage 1 サイドバー側にプログレス表示(「見積書 生成中…」等)を実装してユーザーの不安を軽減すること。
  5. テンプレート ID(CFG_TEMPLATE_ID_QUOTE / CFG_TEMPLATE_ID_DELIVERY / CFG_TEMPLATE_ID_INVOICE)と請求書番号採番キー(CFG_INVOICE_NUMBER_PREFIX / CFG_INVOICE_NUMBER_LAST)が 03_sys_params に未設定または空文字の場合、処理を中断して SpreadsheetApp.getUi().alert('テンプレートIDを 03_sys_params に設定してください: CFG_TEMPLATE_ID_QUOTE 等', ui.ButtonSet.OK) を表示する(Stage 1 の最初でチェックし、PDF 生成に進まない)。
  6. Constants.getParam()最初の呼び出しでキャッシュされるCFG_INVOICE_NUMBER_LAST を連番更新した後も GAS の同一実行内ではキャッシュ値が使われるため、DocumentService._nextInvoiceNumber_()シート値を直接読む or Constants._paramsCache[key] = newVal でキャッシュを手動更新する(実装判断は実装プロンプト側に明記)。
  7. 発行済INV_ID 列が既に埋まっている行への再発行はエラーダイアログでブロックする(二重発行防止)。再発行が必要な場合は既存 INV の 有効フラグ=FALSE 化 + 21_bud_pipeline.発行済INV_ID クリア(手動)を経てから再実行させる運用とする。
  8. 406_rpa_pipeline.js の追加ガード(Step 1 後段)は、本案件 UI で発行済みの行を RPA が再度起票しないためのもの。RPA 実行時の 発行済INV_ID 列が非空なら、その行を continue でスキップする(確度(ヨミ).includes('受注') ガードと AND ではなく追加の OR 的スキップ条件)。

エッジケース

条件表示・動作理由
21_bud_pipeline 選択行が行なし(ヘッダー行選択、または空行)エラーダイアログ「発行対象の案件行を選択してください」表示・処理中断必須入力
選択行の 取引先名 が空、または スポット売上・初期費用継続月額(MRR) が両方 0 以下エラーダイアログ「取引先名と金額は必須です」表示・処理中断請求書の必須情報欠落
スポット売上・初期費用 + 継続月額(MRR) × 継続月数 ≤ 0エラーダイアログ「合計金額が 0 以下のため発行できません」表示・処理中断金額不正の請求書は発行不可
発行済INV_ID 列に既存の INV_YYYYMMDD_NNNN が入力済みエラーダイアログ「この行は既に INV_xxxx で請求書発行済みです。再発行は既存INVの無効化後に実施してください」表示・処理中断同一案件への二重INV登録防止
CFG_TEMPLATE_ID_QUOTE / _DELIVERY / _INVOICE のいずれかが 03_sys_params 未設定または空文字エラーダイアログ「テンプレートID未設定: CFG_TEMPLATE_ID_xxx を 03_sys_params に登録してください」表示・処理中断テンプレートなしでは PDF 生成不可
CFG_INVOICE_NUMBER_PREFIX / CFG_INVOICE_NUMBER_LAST が未設定エラーダイアログ「請求書番号採番設定が未登録です。CFG_INVOICE_NUMBER_PREFIX / CFG_INVOICE_NUMBER_LAST を 03_sys_params に登録してください」表示・処理中断顧客向け請求書番号の付与不可
11_mst_account21_bud_pipeline.売上科目 が未登録 or 有効フラグ=FALSEInvoiceRepository.append() 呼び出しAccountRepository.findAsMap() で事前バリデーションしブロック。エラーダイアログ「科目「xxx」が 11_mst_account に未登録 or 無効です」表示append 内部で科目マスタ必須のため
DriveApp.getFileById() / DocumentApp.openById() / makeCopy() が例外(ID 不正・権限なし・存在しない)Utils.logError('DocumentService.generateDrafts', e) で記録後、ユーザーダイアログ「PDF 生成に失敗しました。テンプレートID と Drive 権限を確認してください: <エラーメッセージ>」表示。既に生成された一時 Docs コピーがあれば DriveApp.getFileById(tmpId).setTrashed(true) でクリーンアップGAS 側エラーは API 層で握りつぶさず可視化
PDF 生成途中で例外(例: 1 種目成功・2 種目失敗)Stage 1 全体をロールバック: 生成済みの一時 PDF / Docs コピーをすべて setTrashed(true) し、サイドバーに失敗状態を表示。32_wrk_invoice 登録には進まない中途半端な Drive ファイル残留防止
GAS 実行時間が 5 分超過見込み(3 種連続生成で測定可能)Utils.logError() で記録し、タイムアウト前に中断。部分完了の状態をユーザーに通知。フォールバック(3 種分割実行・非同期キュー化等)はスコープ外(人間が検討すべき事項に記載)6 分上限への安全マージン確保
LockService.getScriptLock().waitLock(10000) が 10 秒以内にロック取得できないエラーダイアログ「他の処理が実行中です。30秒後に再試行してください」表示・処理中断同一ユーザーの連続クリック・多重 INV 採番の防止
Stage 1 成功後に Stage 2 未実行のままサイドバーを閉じるDrive 一時 PDF はそのまま残留。32_wrk_invoice 登録・21_bud_pipeline 更新は行われないStage 2 のみが確定操作。放置された PDF のクリーンアップポリシーは人間が検討すべき事項に記載
Stage 2 実行時に選択行番号が Stage 1 と異なる(ユーザーが別の行を選択した)draftPayload.pipeRowIdx と現在の選択行番号が一致しない場合はエラー「ドラフト生成時の行と選択行が異なります。再度 Stage 1 から実行してください」で中断意図しない行への INV 登録防止
AccountRepository.findAsMap() が例外(11_mst_account 破損等)Utils.logError() で記録し、エラーダイアログで中断下流の append でデータ不整合発生を防止

Human-in-the-Loop(2段階確定フロー)

  • Stage 1(自動生成): ユーザーが 21_bud_pipeline で案件行を選択 → サイドバー「帳票生成」ボタン押下 → DocumentService.generateDrafts() が見積書・納品書・請求書 PDF を Drive の一時フォルダに生成し、サイドバーに 3 種それぞれのプレビューリンクを表示する。この時点では 32_wrk_invoice への登録・21_bud_pipeline の更新は行わない
  • Stage 2(確定登録): ユーザーが PDF を目視確認し内容に問題がなければ「✅ 確定して請求レコードを登録」ボタンを押下。DocumentService.confirmAndRegister()InvoiceRepository.append([dto])(Step 4)と 21_bud_pipeline 更新(Step 5)を実行する。
  • Stage 1 のみで終了した場合: Drive 一時 PDF はそのまま保持される。自動クリーンアップは実装しない(人間が検討すべき事項に記載)。ユーザーが明示的に「キャンセル」ボタンを押した場合のみ DriveApp.setTrashed(true) で削除する。
  • Stage 2 のサイドバーには draftPayload(Stage 1 で生成された PDF URL・INV 予定データ・行番号)を隠しフィールドで保持し、ボタン押下時にサーバーに再送する。

実データ検証

本案件の MCP 実データ確認は実装直前に必ず実施すること。Phase 1 時点では以下を推定し、実装フェーズで確定する:

  • 03_sys_params のキー実在状況(要確認・未実装可能性大):

    • CFG_TEMPLATE_ID_QUOTE未登録の可能性が高い(過去の仕様書・コード検索でヒットなし)。実装時に初期値登録が必要(テンプレートの Google ドキュメント ID を手動で 03_sys_params に挿入)。
    • CFG_TEMPLATE_ID_DELIVERY — 同上、未登録の可能性大
    • CFG_TEMPLATE_ID_INVOICE — 同上、未登録の可能性大
    • CFG_INVOICE_NUMBER_PREFIX未登録の可能性大。実装時に 'INV-2026-' 等を初期投入。
    • CFG_INVOICE_NUMBER_LAST未登録の可能性大。実装時に 0 を初期投入し、採番ごとに +1。
    • 補完済みキー(参考): CONSUMPTION_TAX_RATE_STANDARD, CFG_BONUS_PROVISION_AMOUNT 等が既存。
  • 11_mst_account の収入科目名(要確認):

    • 売上高Constants.ACCOUNT_RULESkeywords: ['売上', '契約', '役務', '案件']account: '売上高' とマッピングされており、既存 RPA(406_rpa_pipeline.js)が 売上科目 のデフォルトとして 売上高 を使用中。実データに 有効フラグ=TRUE で登録済みである前提。未登録の場合は事前マスタ登録が必要。
  • 21_bud_pipeline 実シート列とDDL定義の乖離チェック:

    • DDL 定義(setupAllSchemas.BUD_PIPE)= 20 列。
    • 実シートに余分な列(手動追加の列)が存在する場合は、本案件の DDL 更新(5列追加 → 25列)実行時に setupAllSchemas() が列をリセット/追加する挙動を事前確認すること。
    • 実装前に MCP で 21_bud_pipeline実際のヘッダー行 1 行目を取得し、本仕様書の「列挿入位置 = 備考 の直前」が妥当か最終確認する。

関連ドキュメント

人間が検討すべき事項

TODO_future.md MAS-100 記載事項(原文転記)

  • テンプレートのデザイン — 見積書・納品書・請求書それぞれの Google ドキュメントテンプレート雛形の作成・ブランディング(ロゴ・社名・振込先等)。
  • 請求書番号の採番ルール — 連番方式(INV-2026-0001)か年度リセット方式(INV-2026-04-0001)か、または顧客別プレフィックス付き採番か。本仕様では CFG_INVOICE_NUMBER_PREFIX + CFG_INVOICE_NUMBER_LAST(+1) の単純連番を採用し、リセットロジックは実装しない。
  • メール送付の自動化要否 — 本仕様はスコープ外とし、生成 PDF の Drive URL からユーザーが手動でメール送付する運用。将来の GmailApp.sendEmail() 連携は別案件で検討する。

本仕様で決定した事項(再掲)

  • テンプレート形式: Google ドキュメントテンプレート(HTML→PDF変換ではなく DocumentApp.body.replaceText() + file.getAs(MimeType.PDF) 方式)。
  • 請求書番号採番: 03_sys_params 管理の単純連番(CFG_INVOICE_NUMBER_PREFIX + CFG_INVOICE_NUMBER_LAST、発行ごとに +1)。
  • メール送付: スコープ外。生成 PDF の Drive URL を手動共有。
  • T番号(適格請求書発行事業者登録番号): テンプレートの {{T番号}} プレースホルダーに Constants.getParam('CFG_INVOICE_REGISTRATION_NUMBER', '') で差し込む。事業者未登録なら空文字のまま(インボイス制度非対応の簡易請求書として出力)。

未決定・要検討の事項

  • Stage 2 未実行時の Drive 一時ファイルクリーンアップポリシー — 現状は残留するまま。週次で自動削除する GAS トリガー(cleanupOrphanedDraftPdfs() 等)を別途実装するか、手動削除運用とするか。
  • GAS 実行時間超過時のフォールバック — 3 種 PDF を逐次生成する現設計では通常 15〜25 秒で完了するが、Google Docs API の応答遅延で 5 分を超える可能性がある。フォールバック案: ①3 種分割実行(見積のみ→納品のみ→請求のみのボタン 3 つ)/ ②非同期キュー化(ScriptApp.newTrigger('...').timeBased().after(1).create() でバックグラウンド実行)。現段階では未実装。
  • PDF の Drive 保存先フォルダEnv.receiptFolderId()(領収書フォルダ)を流用するか、専用の「AR発行フォルダ」を新設するか。専用フォルダ推奨だが、03_sys_params.CFG_AR_OUTPUT_FOLDER_ID を新規キーとして追加する必要あり。
  • 将来のメール自動送付機能追加GmailApp.sendEmail() で請求書PDFを取引先にメール送付する機能は次フェーズ案件。取引先マスタ(12_mst_partner)に「請求書送付先メールアドレス」列を追加する DDL 変更が必要。
  • 再発行運用発行済INV_ID が既に埋まった行で修正再発行が必要になった場合のワークフロー。現状は「既存 INV を 有効フラグ=FALSE 化 + 発行済INV_ID 手動クリア」だが、UI に「再発行」ボタンを設けて上記を半自動化する余地あり。
  • 請求書の押印/電子署名 — 紙の押印は想定していないが、電子署名(クラウドサイン等)連携の要否。現状は Google Docs ベースの PDF 出力のみ。

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-100「請求書発行(見積書・納品書・請求書PDF生成)」を実装してください。

## 実行前タスク
- `000_infra/003_contracts.js` — InvoiceDTO の全フィールドと申請種別・請求ステータスの取りうる値を確認。申請種別は既存値 `請求書発行(AR)` を再利用(新値を追加しない)。請求ステータスは `未処理` / `承認済` / `却下` の3値のみ。
- `000_infra/002_constants.js` — `Constants.getParam(key, defaultVal)` は文字列デフォルトで `String(val)`、数値で `Number(val)` を返す。最初の呼び出しでシート全体をキャッシュする点に注意(`_paramsCache` の手動更新が必要)。`ID_PREFIX_MAP` の `32_wrk_invoice` は `prefix=INV_`, `digit=4`, `isDate=true`。`SHEET_DEFAULTS` の `21_bud_pipeline` エントリに `'請求ステータス': '未処理'` を追加する。
- `200_data/202_repository.js` — `InvoiceRepository.append(dtos)` は `AccountRepository.findAsMap()` で `科目名 → {stmt, cat}` を引き `諸表区分` / `大分類` を自動付与する。PJ名未指定時は `指定なし_共通費など` を設定。最終行判定は `lastRowCol=0`(A列=有効フラグ)。科目マスタ未登録の科目名は渡さないこと(事前に `AccountRepository.findAsMap()` で検証)。
- `100_config/101_sys_config.js` — `onOpen()` は `🚀 BizLP` メニュー1つのみ(サイドバーに全操作集約)。メニュー項目追加は `templates/operations_sidebar.html` の「📒 経理業務」セクションに `<button>` 1行を追加する方針。`setupAllSchemas` 内の `'BUD_PIPE'` エントリ(L662 付近)の `headers` 配列を 5 列拡張。`openDocumentGenerationSidebar()` 関数を新設し `HtmlService.createHtmlOutputFromFile('300_ui/302_ui_document').setTitle('📄 帳票発行').setWidth(380)` を返す。
- `400_domain/406_rpa_pipeline.js` — `generatePipelineInvoices()` が存在。21_bud_pipeline の `確度(ヨミ).includes('受注')` 行を対象に月次 RPA 自動起票する。本案件 UI と干渉しないよう、ループ内に「`発行済INV_ID` 列が非空なら `continue`」ガードを追加する。
- `300_ui/301_ui_assist.js` — onEdit ベースの入力アシスト(妖精)を実装。サイドバー HTML は存在しないため、サイドバーパターンは `templates/operations_sidebar.html` を参考に `HtmlService.createHtmlOutputFromFile()` + `google.script.run` 方式で実装する。
- MCP で `03_sys_params` の実データを確認: `CFG_TEMPLATE_ID_QUOTE` / `_DELIVERY` / `_INVOICE` / `CFG_INVOICE_NUMBER_PREFIX` / `CFG_INVOICE_NUMBER_LAST` / `CFG_INVOICE_REGISTRATION_NUMBER` / `CFG_AR_OUTPUT_FOLDER_ID` の 7 キーが登録済みかチェック。未登録なら初期値投入行を `03_sys_params` に追加する手順をユーザーに案内。
- MCP で `11_mst_account` の `売上高` 行の `有効フラグ` を確認。`FALSE` または未登録ならユーザーに登録依頼。

## 修正対象ファイル
- `100_config/101_sys_config.js` — `setupAllSchemas.BUD_PIPE.headers` を 5 列拡張(`備考` の直前に `請求ステータス` / `発行済INV_ID` / `見積書URL` / `納品書URL` / `請求書URL`)、`openDocumentGenerationSidebar()` を新設。
- `000_infra/002_constants.js` — `SHEET_DEFAULTS` の `21_bud_pipeline` エントリの `defaults` に `'請求ステータス': '未処理'` を追加。
- `400_domain/409_document_service.js` — 新規作成。`DocumentService.generateDrafts(pipeRowIdx)` / `DocumentService.confirmAndRegister(draftPayload)` / 内部 `_nextInvoiceNumber_()` / `_renderPdf_(templateId, placeholders, fileName)` を実装。採用ファイル番号は 409(408 は MAS-090 withholding_tax が仕様書完了済で予約)。
- `400_domain/406_rpa_pipeline.js` — ループ内に `発行済INV_ID` 非空スキップガードを 1 行追加(列名リストにも追加)。
- `300_ui/302_ui_document.html` — 新規作成。Stage 1 / Stage 2 ボタン・プレビューリンク表示・プログレス表示・エラー表示を持つサイドバー UI。
- `templates/operations_sidebar.html` — 「📒 経理業務」セクションに `<button class="btn" onclick="google.script.run.openDocumentGenerationSidebar()">📄 帳票発行(見積・納品・請求)</button>` を 1 行追加。

## 実装内容

### Step 1: `21_bud_pipeline` DDL 変更
`100_config/101_sys_config.js` L662 の `'BUD_PIPE'.headers` 配列を:
```js
'BUD_PIPE': { headers: [
  "有効フラグ","管理ID","PJ・案件名","契約形態","売上科目","確度(ヨミ)",
  "計上開始年月","スポット売上・初期費用","継続月額(MRR)","継続月数",
  "取引先名","決済手段","CF計上","入金ラグ(月)","入金日","休日調整",
  "組織名","起票ターゲット月","最終起票年月日",
  "請求ステータス","発行済INV_ID","見積書URL","納品書URL","請求書URL",
  "備考"
], color: "#e69138" },
```
併せて `000_infra/002_constants.js` L77 の `SHEET_DEFAULTS` に `'請求ステータス': '未処理'` を追加。

### Step 2: `400_domain/409_document_service.js` の骨格
```js
var DocumentService = {
  generateDrafts: function(pipeRowIdx) {
    var ss = getWebSpreadsheet_();
    var sheet = ss.getSheetByName('21_bud_pipeline');
    var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0];
    var row = sheet.getRange(pipeRowIdx, 1, 1, headers.length).getValues()[0];
    var get = function(col) { var i = headers.indexOf(col); return i === -1 ? '' : row[i]; };

    // 事前バリデーション
    var tmplQuote = Constants.getParam('CFG_TEMPLATE_ID_QUOTE', '');
    var tmplDelivery = Constants.getParam('CFG_TEMPLATE_ID_DELIVERY', '');
    var tmplInvoice = Constants.getParam('CFG_TEMPLATE_ID_INVOICE', '');
    if (!tmplQuote || !tmplDelivery || !tmplInvoice) {
      SpreadsheetApp.getUi().alert('テンプレートID未設定: CFG_TEMPLATE_ID_QUOTE / _DELIVERY / _INVOICE を 03_sys_params に登録してください');
      throw new Error('TEMPLATE_NOT_CONFIGURED');
    }
    if (!get('取引先名')) throw new Error('取引先名が未入力です');
    var spot = Number(get('スポット売上・初期費用')) || 0;
    var mrr = Number(get('継続月額(MRR)')) || 0;
    var dur = Number(get('継続月数')) || 0;
    var total = spot + mrr * dur;
    if (total <= 0) throw new Error('合計金額が 0 以下です');
    if (String(get('発行済INV_ID')).trim()) throw new Error('この行は既に請求書発行済みです');

    // 科目マスタ事前検証
    var acctName = String(get('売上科目') || '売上高').trim();
    var acctMap = AccountRepository.findAsMap();
    if (!acctMap[acctName]) throw new Error('科目「' + acctName + '」が 11_mst_account に未登録です');

    // 請求書番号採番
    var invNumber = DocumentService._nextInvoiceNumber_();

    // プレースホルダー構築
    var tz = Session.getScriptTimeZone();
    var placeholders = {
      '{{会社名}}': String(get('取引先名')),
      '{{請求書番号}}': invNumber,
      '{{発行日}}': Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd'),
      '{{PJ名}}': String(get('PJ・案件名')),
      '{{金額_税抜}}': total.toLocaleString(),
      '{{消費税額}}': '0',
      '{{金額_税込}}': total.toLocaleString(),
      '{{T番号}}': Constants.getParam('CFG_INVOICE_REGISTRATION_NUMBER', ''),
      '{{発行元社名}}': Constants.getParam('CFG_ISSUER_COMPANY_NAME', '')
    };

    // PDF 3 種生成
    var quoteUrl = DocumentService._renderPdf_(tmplQuote, placeholders, '見積書_' + invNumber);
    var deliveryUrl = DocumentService._renderPdf_(tmplDelivery, placeholders, '納品書_' + invNumber);
    var invoiceUrl = DocumentService._renderPdf_(tmplInvoice, placeholders, '請求書_' + invNumber);

    return {
      quoteUrl: quoteUrl, deliveryUrl: deliveryUrl, invoiceUrl: invoiceUrl,
      draftPayload: {
        pipeRowIdx: pipeRowIdx, invNumber: invNumber, total: total,
        取引先名: String(get('取引先名')), PJ名: String(get('PJ・案件名')),
        売上科目: acctName, 計上開始年月: get('計上開始年月'),
        決済手段: String(get('決済手段')), 組織名: String(get('組織名')),
        入金ラグ月: Number(get('入金ラグ(月)')) || 1,
        入金日: Number(get('入金日')) || 0,
        quoteUrl: quoteUrl, deliveryUrl: deliveryUrl, invoiceUrl: invoiceUrl
      }
    };
  },

  confirmAndRegister: function(draftPayload) {
    var lock = LockService.getScriptLock();
    if (!lock.tryLock(10000)) throw new Error('他の処理が実行中です。30秒後に再試行してください');
    try {
      var ss = getWebSpreadsheet_();
      var sheet = ss.getSheetByName('21_bud_pipeline');
      var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0];

      // 整合性チェック: 選択行の発行済INV_ID が空であること(再確認)
      var invColIdx = headers.indexOf('発行済INV_ID');
      var curInvId = sheet.getRange(draftPayload.pipeRowIdx, invColIdx + 1).getValue();
      if (String(curInvId).trim()) throw new Error('この行は既に請求書発行済みです(並行更新検知)');

      // InvoiceDTO 構築
      var now = new Date();
      var tz = Session.getScriptTimeZone();
      var dateStr = Utilities.formatDate(now, tz, 'yyyyMMdd');
      RpaCommon.resetIdCache();
      var invId = RpaCommon.generateInvId(dateStr, 0);
      var startYm = Utils.parseDateToYm(draftPayload.計上開始年月);
      var startParts = startYm.split('-').map(Number);
      var occurDate = new Date(startParts[0], startParts[1], 0); // 月末
      var settleDate = DocumentService._calcSettleDate_(startYm, draftPayload.入金ラグ月, draftPayload.入金日);

      var dto = {
        '有効フラグ': true,
        '請求ID(INV)': invId,
        '親発注ID(ORD)': DocumentService._findOrdId_(ss, draftPayload.pipeRowIdx, sheet, headers),
        '起票日時': now,
        '起票者': Session.getActiveUser().getEmail() || '手動発行(S-28)',
        '申請種別': '請求書発行(AR)',
        '発生日(P/L計上日)': occurDate,
        '決済日_計画': settleDate,
        '請求ステータス': '未処理',
        '収支区分': '収入',
        '取引先名': draftPayload.取引先名,
        'PJ名': draftPayload.PJ名 || '指定なし_共通費など',
        '組織名': draftPayload.組織名,
        '科目名': draftPayload.売上科目,
        '税区分': '対象外',
        '通貨': 'JPY',
        '税抜金額_計画': draftPayload.total,
        '消費税額_計画': 0,
        '税込金額_計画': draftPayload.total,
        '未決済残高(自動計算)': draftPayload.total,
        '決済手段': draftPayload.決済手段,
        '摘要': '【DOC:S-28】' + Utilities.formatDate(now, tz, 'yyyy-MM-dd') + ' ' + draftPayload.PJ名 + ' 発行',
        '証憑URL': draftPayload.invoiceUrl
      };

      InvoiceRepository.append([dto]);
      Utils.auditLog('CREATE', '32_wrk_invoice', invId, '', 'DocumentService.confirmAndRegister', null, dto, '発行元=21_bud_pipeline 行=' + draftPayload.pipeRowIdx);

      // 21_bud_pipeline のステータス更新(列参照はヘッダー名ベース)
      var updates = [
        { name: '発行済INV_ID', value: invId },
        { name: '請求ステータス', value: '請求済' },
        { name: '見積書URL', value: draftPayload.quoteUrl },
        { name: '納品書URL', value: draftPayload.deliveryUrl },
        { name: '請求書URL', value: draftPayload.invoiceUrl }
      ];
      updates.forEach(function(u) {
        var idx = headers.indexOf(u.name);
        if (idx === -1) throw new Error('列「' + u.name + '」が見つかりません。DDL を再適用してください');
        var before = sheet.getRange(draftPayload.pipeRowIdx, idx + 1).getValue();
        sheet.getRange(draftPayload.pipeRowIdx, idx + 1).setValue(u.value);
        Utils.auditLog('UPDATE', '21_bud_pipeline', '行' + draftPayload.pipeRowIdx, u.name, 'DocumentService.confirmAndRegister', before, u.value);
      });

      return { invId: invId, pipeRowUpdated: draftPayload.pipeRowIdx };
    } finally {
      lock.releaseLock();
    }
  },

  _nextInvoiceNumber_: function() {
    var prefix = Constants.getParam('CFG_INVOICE_NUMBER_PREFIX', '');
    if (!prefix) throw new Error('CFG_INVOICE_NUMBER_PREFIX が 03_sys_params に未登録です');
    var last = Number(Constants.getParam('CFG_INVOICE_NUMBER_LAST', 0)) || 0;
    var next = last + 1;
    // 03_sys_params シート直接更新(キャッシュも手動更新)
    var sheet = getWebSpreadsheet_().getSheetByName('03_sys_params');
    var data = sheet.getDataRange().getValues();
    for (var i = 1; i < data.length; i++) {
      if (String(data[i][0]).trim() === 'CFG_INVOICE_NUMBER_LAST') {
        sheet.getRange(i + 1, 2).setValue(next);
        Constants._paramsCache && (Constants._paramsCache['CFG_INVOICE_NUMBER_LAST'] = next);
        return prefix + String(next).padStart(4, '0');
      }
    }
    throw new Error('CFG_INVOICE_NUMBER_LAST 行が 03_sys_params に存在しません');
  },

  _renderPdf_: function(templateId, placeholders, fileName) {
    var tmpCopy = null;
    try {
      var outputFolderId = Constants.getParam('CFG_AR_OUTPUT_FOLDER_ID', '');
      var outputFolder = outputFolderId ? DriveApp.getFolderById(outputFolderId) : DriveApp.getRootFolder();
      tmpCopy = DriveApp.getFileById(templateId).makeCopy(fileName + '_tmp', outputFolder);
      var doc = DocumentApp.openById(tmpCopy.getId());
      var body = doc.getBody();
      Object.keys(placeholders).forEach(function(k) { body.replaceText(k, String(placeholders[k])); });
      doc.saveAndClose();
      var pdfBlob = DriveApp.getFileById(tmpCopy.getId()).getAs(MimeType.PDF).setName(fileName + '.pdf');
      var pdfFile = outputFolder.createFile(pdfBlob);
      return pdfFile.getUrl();
    } finally {
      if (tmpCopy) {
        try { DriveApp.getFileById(tmpCopy.getId()).setTrashed(true); } catch (e) { /* noop */ }
      }
    }
  },

  _calcSettleDate_: function(startYm, lag, payDay) {
    var sYm = Utils.addMonths(startYm, lag);
    var sp = sYm.split('-').map(Number);
    if (payDay > 0) {
      var maxDay = new Date(sp[0], sp[1], 0).getDate();
      return new Date(sp[0], sp[1] - 1, Math.min(payDay, maxDay));
    }
    return new Date(sp[0], sp[1], 0);
  },

  _findOrdId_: function(ss, pipeRowIdx, pipeSheet, pipeHeaders) {
    var mgrIdIdx = pipeHeaders.indexOf('管理ID');
    if (mgrIdIdx === -1) return '';
    var pipeMgrId = String(pipeSheet.getRange(pipeRowIdx, mgrIdIdx + 1).getValue()).trim();
    if (!pipeMgrId) return '';
    var ordSheet = ss.getSheetByName('31_wrk_order');
    if (!ordSheet) return '';
    var ordData = ordSheet.getDataRange().getValues();
    var ordHeaders = ordData[0];
    var refIdIdx = ordHeaders.indexOf('参照元ID');
    var ordIdIdx = ordHeaders.indexOf('発注ID(ORD)');
    for (var i = 1; i < ordData.length; i++) {
      if (String(ordData[i][refIdIdx]).trim() === pipeMgrId) return String(ordData[i][ordIdIdx]);
    }
    return '';
  }
};

// サイドバー側から呼ばれるトランポリン関数
function documentServiceGenerateDrafts(pipeRowIdx) { return DocumentService.generateDrafts(pipeRowIdx); }
function documentServiceConfirmAndRegister(draftPayload) { return DocumentService.confirmAndRegister(draftPayload); }
```

### Step 3: `300_ui/302_ui_document.html` の骨格
- Stage 1 ボタン押下で `google.script.run.withSuccessHandler(onDraftSuccess).withFailureHandler(onFailure).documentServiceGenerateDrafts(activeRow)` を呼ぶ。
- `activeRow` は `google.script.run.getActiveRowForDocumentSidebar()` で取得(`101_sys_config.js` に `function getActiveRowForDocumentSidebar() { return SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange().getRow(); }` を追加)。
- Stage 1 成功時に 3 PDF の `<a target="_blank">プレビュー</a>` を表示、`draftPayload` を `window._draftPayload` に保持。
- Stage 2 ボタンは Stage 1 成功後に有効化、押下で `documentServiceConfirmAndRegister(window._draftPayload)`。
- 失敗時は赤帯でエラーメッセージ表示。

### Step 4-5: Step 2 の `confirmAndRegister()` 内で `InvoiceRepository.append([dto])` 呼び出しと `21_bud_pipeline` 5 列更新を実装済み。

## 制約
- 列番号ハードコード禁止。列参照はヘッダー名ベース(`indexOf` / `buildHeaderIndex_`)。
- 有効フラグ=FALSE の行はすべての処理でスキップ(`DocumentService.generateDrafts` は選択行の有効フラグをチェックし `false` なら拒否)。
- `InvoiceRepository.append()` を改変しない(既存の科目マスタ自動付与ロジックを維持)。事前に `AccountRepository.findAsMap()` で科目名の存在検証を行う。
- 既存の 32_wrk_invoice レコードを上書きしない(`append` のみ使用。`save` や手動 `setValue` で既存行を書き換えない)。
- `LockService.getScriptLock().tryLock(10000)` で排他制御し、取得失敗時はエラーダイアログを表示して中断する。
- `Utils.auditLog()` で CREATE(32_wrk_invoice)と UPDATE(21_bud_pipeline の 5 列各々)を必ず記録する。
- `Constants.getParam()` の戻り型に注意: 文字列取得時は `String(val)`、数値取得時は `Number(val)`。
- PDF 生成の一時 Docs コピーは `try/finally` で必ず `setTrashed(true)` する。

## エッジケース
仕様書「## エッジケース」テーブル(13 行)をそのまま実装すること。特に次の 5 条件は Stage 1 の先頭で検証:
1. 選択行が空 or ヘッダー行
2. 取引先名 空 / 金額 ≤ 0
3. 発行済INV_ID 既入力
4. `CFG_TEMPLATE_ID_*` / `CFG_INVOICE_NUMBER_*` 未登録
5. 科目名が `11_mst_account` 未登録 or `有効フラグ=FALSE`

## 実データ検証
仕様書「## 実データ検証」の 3 項目(`03_sys_params` 7 キー / `11_mst_account.売上高` / `21_bud_pipeline` 実列)を実装前に MCP で必ず確認すること。未登録のキーは実装時に `03_sys_params` へ初期値投入する(ユーザーに Google ドキュメント ID の実値を確認する)。

## 動作確認
1. `npm run push:dev` でデプロイ
2. 開発スプレッドシートで `setupAllSchemas` を実行し `21_bud_pipeline` に新列 5 つが追加されたことを確認
3. `03_sys_params` に 7 キー(`CFG_TEMPLATE_ID_QUOTE` / `_DELIVERY` / `_INVOICE` / `CFG_INVOICE_NUMBER_PREFIX` / `CFG_INVOICE_NUMBER_LAST` / `CFG_INVOICE_REGISTRATION_NUMBER` / `CFG_AR_OUTPUT_FOLDER_ID`)を登録
4. `21_bud_pipeline` にテストデータを 1 行追加(取引先名・PJ名・計上開始年月・スポット売上 100000 円・入金ラグ 1ヶ月)
5. `🚀 BizLP` メニュー → 「操作パネルを開く」→ サイドバーの「📄 帳票発行(見積・納品・請求)」ボタン押下
6. 新サイドバーで「帳票生成」(Stage 1)押下 → 3 種 PDF のプレビューリンク表示を確認
7. 各 PDF を開き `{{会社名}}` 等のプレースホルダーが正しく置換されていることを確認
8. 「✅ 確定して請求レコードを登録」(Stage 2)押下
9. `32_wrk_invoice` に収入INV(`収支区分=収入` / `申請種別=請求書発行(AR)` / `摘要=【DOC:S-28】...`)が追記されたことを確認
10. `21_bud_pipeline` の該当行に `発行済INV_ID=INV_xxx` / `請求ステータス=請求済` / 3 種URL が更新されたことを確認
11. 同じ行で再度 Stage 1 を実行し「既に請求書発行済み」エラーダイアログが出ることを確認(二重発行ブロック)
12. `98_audit_log` に CREATE(32_wrk_invoice)と UPDATE(21_bud_pipeline × 5 列)のログが記録されたことを確認
13. `03_sys_params.CFG_INVOICE_NUMBER_LAST` が +1 されたことを確認
14. `generatePipelineInvoices()` を実行し、発行済み行はスキップされ新規 INV が生成されないことを確認(406 干渉ガード動作確認)

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|---|---|
| 実行前タスク(Read・MCP確認) | あり | ファイル構造・列名・関数名・科目名の確定 |
| 実装(Write/Edit) | なし | Phase 1 確定内容の書き下しに徹する |

推奨実行モデル

工程推奨モデル理由
Step 1: 21_bud_pipeline DDL 変更 + SHEET_DEFAULTS 更新Claude Haiku 4.5既存 DDL パターンの横展開。仕様書で列名・挿入位置が完全定義済み
Step 2: 400_domain/409_document_service.js 新規作成Claude Sonnet 4.6GAS DriveApp / DocumentApp / LockService / InvoiceRepository 連携の組み合わせ判断が必要。エラーハンドリングと一時ファイルクリーンアップの組立
Step 3: UI サイドバー(302_ui_document.html + openDocumentGenerationSidebar()Claude Sonnet 4.6HTML・google.script.run とサーバーサイド関数連携の設計判断が必要。Stage 1 / Stage 2 の UI 状態遷移設計
Step 4-5: INV 登録 + ステータス更新Claude Haiku 4.5InvoiceRepository.append() の再利用と indexOf 列参照による書込。仕様書で完全定義済み
406_rpa_pipeline.js への発行済INV_ID スキップガード追加Claude Haiku 4.5既存ループに 1 行追加のみ。仕様書で挿入位置を特定済み

変更履歴

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

仕様書作成プロンプト

展開して表示
【タイムアウト回避・実行原則(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(骨格 ~20行)/2-2(概要〜注意事項 ~300行)/2-3a(エッジケース〜人間検討事項 ~200行)/2-3b(実装プロンプト〜変更履歴 ~250行)/2-4(`<details>`プロンプト全文記録)に分割。1回のWrite/Editは約300行以内。
4. **各Stepで何を書くかを具体指示**: 各Stepの内容は下記に明記済み。出力時に設計判断を再考しない。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-28「請求書発行(見積書・納品書・請求書PDF生成)」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の適切なセクションに必ず追記してください。

---

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

以下のファイルを順にReadし、Phase 2で使用する固有名詞・行番号・データ構造をすべて確定させること。**名称を記憶や推測で書かず、必ずReadした内容から引用すること**(失敗パターン #18-#20 の直接対策)。

### 1-A: 案件要件の把握
- `docs/_internal/TODO_future.md` — S-28 の案件名・概要・人間が検討すべき事項をすべて取得する

### 1-B: プロジェクト規約
- `CLAUDE.md` — ファイル番号体系(400番台・300番台の空き番号確認)、コーディング規約(列参照ルール、有効フラグ処理、`Utils.auditLog` の使用義務)

### 1-C: コアコードの構造確認(Readで実態を確認し、名称はコードから引用すること)
- `000_infra/002_constants.js`
  - `Constants.getParam(key, defaultVal)` の引数・返値の型(数値デフォルトは `Number(val)` 返却)
  - `Constants.SHEET_DEFAULTS` 内の `21_bud_pipeline` エントリで現在定義されている列名(`defaults` フィールドの全キー)
  - `Constants.ID_PREFIX_MAP` 内の `32_wrk_invoice` エントリ(prefix=`INV_`, digit=4, isDate=true を確認)
- `000_infra/003_contracts.js`
  - `InvoiceDTO` の全プロパティ名と型コメント(特に `請求ステータス` の既存取りうる値: "未処理"|"承認済"|"却下"、`申請種別` の既存値: "請求書受領(AP)"等、`収支区分`: "収入"|"支出")
  - `21_bud_pipeline` に対応するDTOが定義されているか確認(存在しない場合はその旨をメモする)
- `000_infra/004_utils.js`
  - `Utils.auditLog(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)` の引数順と使用例
  - `Utils.logError(funcName, error, context)`、`Utils.toastResult(funcName, message, duration)` の引数確認
- `200_data/202_repository.js`
  - `InvoiceRepository.append(dtos)` の処理内容(科目マスタ自動付与ロジック、`appendDtosToSheet_` の `lastRowCol=0` による最終行判定)
  - `InvoiceRepository.findAll()` の返値構造 `{ headers, dtos }`
- `100_config/101_sys_config.js`
  - `onOpen()` のメニュー構造を全件確認 — **「請求管理」メニューが実在するか**。存在しない場合は新設が必要。実在するメニュー名をそのまま引用すること(存在しないメニュー名を造語しない)
  - `setupAllSchemas` 内の `21_bud_pipeline` DDL定義 — 現在の列一覧と順序を特定し、追加予定列(`請求ステータス`, `発行済INV_ID`, `見積書URL`, `納品書URL`, `請求書URL`)の挿入位置を確定する

### 1-D: 関連ドメインファイルの確認
- `400_domain/406_rpa_pipeline.js` — **ファイルの存在確認から実施**。存在する場合は `21_bud_pipeline` を操作する既存関数名と処理内容を把握し、新機能との干渉箇所を特定する(存在しない場合はその旨をメモする)
- `300_ui/301_ui_assist.js` — 既存UIサイドバーの実装パターン(`HtmlService.createHtmlOutputFromFile`, `google.script.run` の使い方)を把握する

### 1-E: 参考仕様書の読み込み
- `docs/dev/` 配下で最も近い案件(`dev_mas-080_pipeline_early_id.md` 等)を1件Readし、セクション構成・フォーマットを把握する

### 1-F: 実データのMCP確認(必須)
以下をMCPで実際のシートデータを参照して確認する:
- `21_bud_pipeline` の実シートの列一覧 — DDL定義と実態の乖離チェック。実際に存在する列名を確定する
- `03_sys_params` の実データ — `CFG_TEMPLATE_ID_QUOTE`, `CFG_TEMPLATE_ID_DELIVERY`, `CFG_TEMPLATE_ID_INVOICE`, `CFG_INVOICE_NUMBER_PREFIX`, `CFG_INVOICE_NUMBER_LAST` のキーが実在するか確認(未登録であれば「実装時に登録が必要」と仕様書に記載する)
- `11_mst_account` — `InvoiceRepository.append()` が科目マスタを必須とするため、収入側の科目名(`売上高` 等)が実際に登録済みか確認する

---

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

出力先: `docs/dev/dev_mas-100_document_generation.md`
**1回のツール呼び出しで全内容を出力しない。以下のStep順に分割実行すること。**

### Step 2-1: 骨格の作成(File Write / ~20行)

全セクション見出しのみ(本文空)を作成する。以下の見出しをすべて含めること:

`# S-28: 請求書発行(見積書・納品書・請求書PDF生成)`、`## 概要`、`## 目的`、`## 現在のコード`、`## 修正方針`、`## 影響範囲`、`## 注意事項`、`## エッジケース`、`## 実データ検証`、`## 関連ドキュメント`、`## 人間が検討すべき事項`、`## 実装プロンプト(Claude Code 用)`、`## 推奨実行モデル`、`## 変更履歴`、`## 仕様書作成プロンプト`

### Step 2-2: 前半セクションの追記(File Edit / ~300行)

Phase 1で確定した固有名詞のみで記述すること。推測した名称を使わない。

(中略 — 実際の指示は本ファイルの前半セクションに転記済み)

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

(中略 — 実際の指示は本ファイルの該当セクションに転記済み)

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

実装プロンプトは**バッククォートで囲まず、行頭スペース4つのインデント**で出力すること。
(以下、`## 実装プロンプト(Claude Code 用)` 配下に自己完結プロンプトを記載する指示。本仕様書の当該セクションに反映済み)

推奨実行モデルテーブル(`Claude Haiku 4.5` / `Claude Sonnet 4.6` の使い分け)・変更履歴テーブル(初版作成)を追記。

### Step 2-4: 仕様書作成プロンプトの記録(File Edit)

`## 仕様書作成プロンプト` セクションに `<details><summary>展開して表示</summary>` 形式でこの `<instruction>` 全文を貼り付ける。

---

## Phase 3: 後処理(テキスト報告禁止。即座にツール実行)

### 3-A: `_config.json` への追記
`docs/_config.json` の `nav` 配列 §E.6(パイプライン・RPA・外部連携)に以下を追加:
```json
{ "file": "dev/dev_mas-100_document_generation.md", "title": "E.6.X MAS-100 請求書発行(見積書・納品書・請求書PDF生成)" }
```
追記後にJSONの構文が正しいことを確認すること(末尾カンマ等の構文エラーを防ぐ)。

### 3-B: changelog への追記
`docs/_internal/changelog.md` の先頭(ヘッダー直後)に追記:
```
| 2026-04-19 | [dev_mas-100_document_generation.md](dev_mas-100_document_generation.md) | 初版作成。MAS-100 請求書発行(見積書・納品書・請求書PDF生成)の開発仕様書 |
```

### 3-C: コミット&プッシュ
```bash
git add docs/dev/dev_mas-100_document_generation.md docs/_internal/changelog.md docs/_config.json
git commit -m "docs: MAS-100 請求書発行(見積書・納品書・請求書PDF生成)の開発仕様書を作成

Googleドキュメントテンプレート方式でPDF3種を生成し、InvoiceRepository.append()で
収入INVレコードを登録する2段階確定フロー(Human-in-the-Loop)設計を定義。
DDL変更・新規ファイル2件・LockService排他制御・auditLog必須化を仕様に含む。

https://claude.ai/code/session_XXXXX"
git push -u origin <現在のブランチ名>
```