MAS-100: 請求書発行(見積書・納品書・請求書PDF生成)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-100 |
| 案件名 | 請求書発行(見積書・納品書・請求書PDF生成) |
| カテゴリ | 請求管理(新機能) |
| Phase | 実装 |
| 優先度 | P2(★★) |
| 実装ステータス | 📝 仕様書段階・実装未着手 (2026-04-28 監査時点) |
| 対象ファイル | 100_config/101_sys_config.js(setupAllSchemas の BUD_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.js の generatePipelineInvoices() が 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にキャッシュし、以降はキー検索のみ。- 戻り値の型:
defaultValがnumberならNumber(val)、それ以外はString(val)。空文字・null・undefinedはdefaultValをそのまま返す。 - テンプレート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 B(SubledgerService)で承認・消込する。
Constants.ID_PREFIX_MAP の 32_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_DEFAULTS の 21_bud_pipeline エントリ(L77)
{ pattern: '21_bud_pipeline', prefix: 'PIP_', defaults: {
'契約形態': 'スポット(狩猟)', '売上科目': '売上高', '確度(ヨミ)': 'Aヨミ (確度80%)',
'継続月数': 1, '取引先名': '', '決済手段': '', 'CF計上': '予算',
'入金ラグ(月)': 1, 'スポット売上・初期費用': 0, '継続月額(MRR)': 0,
_dynamic: { '計上開始年月': 'nextYm' }
} }
- 本案件では
defaultsに 請求ステータス='未処理' を追加する。
setupAllSchemas の BUD_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.jsはh.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_ID | text | 本案件 UI で発行確定した INV_YYYYMMDD_NNNN。二重発行防止キー |
見積書URL | text | Drive の PDF へのリンク |
納品書URL | text | Drive の PDF へのリンク |
請求書URL | text | Drive の PDF へのリンク |
変更後の headers は 25 列となる。併せて:
Constants.SHEET_DEFAULTSの21_bud_pipelineエントリに'請求ステータス': '未処理'を追加する(000_infra/002_constants.jsL77)。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 を採用する。
公開名前空間: DocumentService(Contracts / 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_params の CFG_INVOICE_NUMBER_LAST を +1 して永続化(PropertiesService は使わず 03_sys_params シート直接更新) |
DocumentService._renderPdf_(templateId, placeholders, fileName) | テンプレID・プレースホルダー辞書・出力ファイル名 | Drive の PDF URL | Google ドキュメントを 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_invoiceのINV_...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.js の calcSettleDate() と同じロジックを 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.js—setupAllSchemas.BUD_PIPEに 5 列追加、openDocumentGenerationSidebar()関数新設。400_domain/406_rpa_pipeline.js—発行済INV_IDスキップガード 1 行追加。000_infra/002_constants.js—SHEET_DEFAULTS.21_bud_pipeline.defaultsに請求ステータスデフォルト追加。
- 既存ファイル変更 1件(
templates/operations_sidebar.html) — 「📒 経理業務」セクションに「📄 帳票発行」ボタン 1 行追加。 InvoiceRepository.append()/Contracts.InvoiceDTO/Utils.auditLog()は改変しない。既存 API を呼び出すのみ。
注意事項
InvoiceRepository.append()はdtos[i]['科目名']が11_mst_accountに登録済みであることが前提(AccountRepository.findAsMap()の戻り値でマッピング)。収入INVの科目名は21_bud_pipeline.売上科目列の値をそのまま転記し、既定値は売上高。本実装前に11_mst_accountに売上高が有効フラグ=TRUEで登録されていることを MCP 経由で確認する(Phase 1-F の実データ検証項目)。InvoiceDTO.申請種別の既存値はAP側(請求書受領(AP)/経費申請(EX)等)だが、AR側の値として406_rpa_pipeline.jsが請求書発行(AR)を使用済み。本案件も同じ値請求書発行(AR)を再利用し、新値は追加しない。appendDtosToSheet_(sheet, headers, dtos, 0)は A列(有効フラグ)で最終行を判定する。CLAUDE.md の「B列=ID列で判定」規約とは異なるが、InvoiceRepository.append()の既存実装はこの挙動なので本案件では変更しない(変更すると他のRPAにも影響)。- Google ドキュメントの
makeCopy→openById→body.replaceText→PDF エクスポートは 1 回あたり約 3〜8 秒を要する。3 種連続実行で合計 15〜25 秒が目安。GAS 実行時間上限(6分)までに余裕があるが、Stage 1 サイドバー側にプログレス表示(「見積書 生成中…」等)を実装してユーザーの不安を軽減すること。 - テンプレート 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 生成に進まない)。 Constants.getParam()は最初の呼び出しでキャッシュされる。CFG_INVOICE_NUMBER_LASTを連番更新した後も GAS の同一実行内ではキャッシュ値が使われるため、DocumentService._nextInvoiceNumber_()はシート値を直接読む orConstants._paramsCache[key] = newValでキャッシュを手動更新する(実装判断は実装プロンプト側に明記)。発行済INV_ID列が既に埋まっている行への再発行はエラーダイアログでブロックする(二重発行防止)。再発行が必要な場合は既存 INV の有効フラグ=FALSE化 +21_bud_pipeline.発行済INV_IDクリア(手動)を経てから再実行させる運用とする。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_account に 21_bud_pipeline.売上科目 が未登録 or 有効フラグ=FALSE | InvoiceRepository.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_RULESのkeywords: ['売上', '契約', '役務', '案件']で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 行目を取得し、本仕様書の「列挿入位置 =備考の直前」が妥当か最終確認する。
- DDL 定義(
関連ドキュメント
- dev_mas-080 21タブ管理IDの早期採番 —
21_bud_pipelineの管理ID(PIP_)早期採番に関する既存仕様。本案件は管理IDが採番済みの行を前提とする。 - dev_mas-083 パイプライン売上の消費税対応 —
21_bud_pipelineへの税区分/消費税額列追加。本案件は MAS-083 マージ前後で税区分='対象外'固定か21タブ.税区分参照かを実装時に再確認する。 - dev_mas-078 84タブにパイプライン売上を合流 — パイプラインから 84 タブ(CF 日次計画)への合流。本案件で INV 登録された行は
31_wrk_order.参照元ID=PIP_xxx経由で MAS-078 の除外対象となる。 - dev_S-55(TODO_future.md 参照)発注書PDF発行 — 本案件の兄弟機能(発注書・注文書・納品書)。実装時にテンプレート置換・採番等の共通基盤化を検討する余地あり(スコープ外)。
人間が検討すべき事項
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.6 | GAS DriveApp / DocumentApp / LockService / InvoiceRepository 連携の組み合わせ判断が必要。エラーハンドリングと一時ファイルクリーンアップの組立 |
Step 3: UI サイドバー(302_ui_document.html + openDocumentGenerationSidebar()) | Claude Sonnet 4.6 | HTML・google.script.run とサーバーサイド関数連携の設計判断が必要。Stage 1 / Stage 2 の UI 状態遷移設計 |
| Step 4-5: INV 登録 + ステータス更新 | Claude Haiku 4.5 | InvoiceRepository.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 <現在のブランチ名>
```