MAS-127: 発注書PDF発行(見積・注文・納品書)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-127 |
| 案件名 | 発注書PDF発行(見積・注文・納品書) |
| カテゴリ | ドキュメント生成 |
| Phase | 実装 |
| 優先度 | P3(★) |
| 所要時間 | 4〜6時間 |
| 実装ステータス | 📝 仕様書段階・実装未着手 (2026-04-28 監査時点) |
| 対象ファイル | 400_domain/408_document_generator.js(新規作成)/ 100_config/101_sys_config.js(Constants.MENU_DEFINITION へのメニュー追加のみ)/ 03_sys_params シート(TEMPLATE_ID_ESTIMATE / TEMPLATE_ID_ORDER / TEMPLATE_ID_INSPECTION / PDF_OUTPUT_FOLDER_ID の 4 キーを手動登録) |
| 前提案件 | なし(MAS-100 請求書PDF発行は兄弟機能として存在するが、本案件と独立に実装可能。実装時は 400_domain/409_document_service.js(MAS-100側)と競合しないよう 408_ を採番) |
目的
GASメニューから 31_wrk_order(発注ワークテーブル)の選択行に対し、OrderDTO.発注ステータス の値("見積中" / "発注済" / "検収済")に応じた PDF 帳票(見積依頼書/発注書/検収書)を Googleドキュメントテンプレートから自動生成し、生成 PDF の Google ドライブ URL を 31_wrk_order.証憑URL 列に自動反映させるフローを実装する。
現状は 31_wrk_order に発注データを手入力した後、見積書・発注書・検収書を個別に手作業で作成しているため、①取引先向けの書類発行に時間がかかる、②発注データと帳票内容の不整合が発生しやすい、③発行漏れ・再発行の履歴が追えない、という運用課題がある。本案件で書類発行を自動化し、売上サイクル(MAS-100 請求書発行)と並ぶ「支出サイクルの書類発行フェーズ」を整備する。
現在のコード(関連する既存実装)
OrderDTO の型定義(000_infra/003_contracts.js L13-36)
/**
* 31_wrk_order — 発注レコード
* @typedef {Object} OrderDTO
* @property {boolean} 有効フラグ
* @property {string} 発注ID(ORD) - "ORD_YYYYMMDD_NNNN"
* ...
* @property {number} 税抜金額_発注
* @property {number} 消費税額_発注
* @property {number} 税込金額_発注
* ...
* @property {string} 発注ステータス - "見積中" | "発注済" | "検収済"
* @property {string} 参照元区分 - "SUB" | "HC" | "CAPEX" 等
* @property {string} 参照元ID
* @property {string} 証憑URL
*/
発注ステータスの取りうる値は"見積中"/"発注済"/"検収済"の 3 値のみ。元プロンプトで言及された"納品完了"は 定義に存在しないため使用禁止。- 本案件で書き込み対象となる列は
証憑URLのみ。
OrderRepository の実装(200_data/202_repository.js L104-146)
var OrderRepository = {
_getSheet: function() {
return Utils.getSheetByKey('WRK_ORDR', '31_wrk_order');
},
findAll: function() {
return readSheetAsDtos_(OrderRepository._getSheet());
},
save: function(dtos) {
var sheet = OrderRepository._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: string[], dtos: OrderDTO[] }形式を返す(正確にはreadSheetAsDtos_由来の{ headers, dtos }。本案件はこれを利用)。save(dtos)はwriteDtosToSheet_による全行置換であり、複数ユーザー並行操作時に他ユーザーの編集内容を巻き戻すリスクがある。 本案件では対象行の証憑URL列のみ更新したいので、OrderRepository.save()は使わず、シート取得後にsheet.getRange(rowNum, colIdx).setValue(url)で直接書き込む。- シート取得キーは
'WRK_ORDR'(fallback シート名'31_wrk_order')。
Constants.getParam(key, defaultVal)(000_infra/002_constants.js L147-167)
getParam: function(key, defaultVal) {
if (!this._paramsCache) {
this._paramsCache = {};
try {
var ss = SpreadsheetApp.getActiveSpreadsheet() || ...;
if (ss) {
var sheet = ss.getSheetByName('03_sys_params');
if (sheet) {
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
var k = String(data[i][0]).trim();
if (k) this._paramsCache[k] = data[i][1];
}
}
}
} catch(e) { }
}
var val = this._paramsCache[key];
if (val === undefined || val === null || val === '') return defaultVal;
return (typeof defaultVal === 'number') ? Number(val) : String(val);
},
- 最初の呼び出し時のみ
03_sys_paramsシート全体をスキャンしてキャッシュし、以降はキー検索のみ。 - 戻り値の型:
defaultValが数値ならNumber(val)、それ以外はString(val)。未登録・空文字はdefaultValを返す。 - 本案件のテンプレートIDは文字列なので
Constants.getParam('TEMPLATE_ID_ESTIMATE', '')で取得し、空なら処理中断する。
Constants.ID_PREFIX_MAP の 31_wrk_order エントリ(000_infra/002_constants.js L95)
{ pattern: '31_wrk_order', prefix: 'ORD_', digit: 4, isDate: true },
- 発注ID は
ORD_YYYYMMDD_NNNN形式(RpaCommon.generateOrderId相当)。本案件は 既存発注IDに紐付く PDF 生成のみで、新規採番は行わない。
Constants.SHEET_DEFAULTS の 31_wrk_order エントリ(000_infra/002_constants.js L84)
{ pattern: '31_wrk_order', defaults: { '発注ステータス': '見積中', '契約形態': 'スポット' } },
- 行追加時のデフォルトは
発注ステータス='見積中'、契約形態='スポット'。本案件でSHEET_DEFAULTSは変更しない。
onOpen() / メニュー動的生成(100_config/101_sys_config.js L323-350)
function onOpen() {
var ui = SpreadsheetApp.getUi();
// N-38: Constants.MENU_DEFINITION をループしてメニューを動的生成
try {
Constants.MENU_DEFINITION.forEach(function(catDef) {
if (catDef.privileged && !isPrivilegedUser_()) return;
var menu = ui.createMenu(catDef.category);
catDef.items.forEach(function(item) {
if (item.separator) menu.addSeparator();
else if (item.items) { ... sub menu ... }
else menu.addItem(item.label, item.funcName);
});
menu.addToUi();
});
} catch (e) {}
}
- メニュー追加は
onOpen()を直接編集せず、Constants.MENU_DEFINITION(000_infra/002_constants.jsL206-324)への宣言的登録で行う。本案件では「📋 サイドバー: 📝 費用登録」カテゴリと並ぶ新規カテゴリ、または既存カテゴリへのサブメニュー追加として実装する(詳細は修正方針参照)。
Utils.getSheetByKey(key, fallbackName)(000_infra/004_utils.js L302-312)
getSheetByKey: function(key, fallbackName) {
const ss = getWebSpreadsheet_();
try {
const name = Utils.getSheetNameByKey(key);
if (name) {
const s = ss.getSheetByName(name);
if (s) return s;
}
} catch (e) {}
return ss.getSheetByName(fallbackName);
},
01_sys_configのシステムキー→タブ名マッピングを使い、見つからなければfallbackNameでgetSheetByNameする。本案件はUtils.getSheetByKey('WRK_ORDR', '31_wrk_order')で31_wrk_orderシートを取得する。
既存の PDF 生成コード(400_domain / 500_import への grep 結果)
DriveApp/DocumentAppを使うファイルは500_import/502_receipt_reader.jsと500_import/504_invoice_importer.jsのみ(いずれも Drive からの読み取り用途。PDF 生成・ドキュメントコピー用途のコードは存在しない)。- 兄弟機能 MAS-100(請求書PDF発行)は
400_domain/409_document_service.jsを新設予定(docs/dev/dev_mas-100_document_generation.md参照)。MAS-100 未実装の段階で MAS-127 が先行する場合も想定し、本案件は408_document_generator.jsとして独立採番する。将来 MAS-100 実装後に共通ヘルパー(createPdfFromTemplate_)を400_rpa_common.jsまたは000_infra/004_utils.jsに切り出すリファクタリングは別案件とする。
修正方針
新規ファイルの配置先(Option A 採用)
Phase 1 で以下 2 案を比較検討した結果、Option A を採用する。
| 選択肢 | 配置先 | CLAUDE.md 変更 | 判定 |
|---|---|---|---|
| A(採用) | 400_domain/408_document_generator.js | 不要(400_domain/ は既存レイヤー) | ○ |
| B | 700_doc/701_document_generator.js 等の新層 | 必要(ファイル番号体系テーブルの追記) | × |
採用理由:
- CLAUDE.md の現行ファイル番号体系(000/100/200/300/400/500/600/800/900)に
700_は存在しない。新層追加はCLAUDE.mdとdocs/_config.json双方の更新が必要となり、スコープが拡大する。 - 兄弟機能 MAS-100 は
400_domain/409_document_service.jsを採番予定(docs/dev/dev_mas-100_document_generation.md§Step 2)。「ドキュメント生成」は400_domain/配下で一貫させる方針に沿う。 - 現状
400_domain/の最大番号は420_project_profitability.js(400/401/402/403/404/405/406/407/410/420)。408は未使用のため衝突しない。
Step 1: 03_sys_params シートへのキー登録(手動作業)
実装着手前に、03_sys_params シートへ以下 4 キーを手動追加する(Phase 1 の grep でコード・過去仕様書にヒットしなかったため、未登録の可能性が極めて高い)。
| キー | 値の例 | 用途 |
|---|---|---|
TEMPLATE_ID_ESTIMATE | 1AbCdEf...xyz | 「見積依頼書」用 Googleドキュメントテンプレートの fileId |
TEMPLATE_ID_ORDER | 1AbCdEf...xyz | 「発注書」用 Googleドキュメントテンプレートの fileId |
TEMPLATE_ID_INSPECTION | 1AbCdEf...xyz | 「検収書」用 Googleドキュメントテンプレートの fileId |
PDF_OUTPUT_FOLDER_ID | 1AbCdEf...xyz | 生成 PDF の保存先 Google ドライブフォルダ ID |
登録が未完了の場合、実装コードは SpreadsheetApp.getUi().alert() でエラーダイアログを表示して処理を中断する(詳細はエッジケース参照)。
Step 2: PDF 生成ヘルパーの新設(400_domain/408_document_generator.js)
新規ファイルを作成し、以下の公開関数 1 本+内部ヘルパー 1 本を配置する。
公開関数(メニュー登録対象):
generateOrderDocument()- 選択行(複数行可)の各
OrderDTOに対して、発注ステータスに応じた PDF を生成する。 - 実行フロー:
LockService.getScriptLock().tryLock(30000)で二重実行防止(Phase 1 で確定したCacheServiceではなくLockServiceを採用)。Constants.getParam('TEMPLATE_ID_ESTIMATE', '')等 4 キーを取得。1 つでも空ならSpreadsheetApp.getUi().alert()で中断。SpreadsheetApp.getActiveSheet().getActiveRangeList()またはgetSelection()で選択範囲を取得し、対象行番号の配列を作る。OrderRepository.findAll()で{ headers, dtos }を取得し、選択行インデックスに対応する DTO のみ処理対象とする。- DTO ごとにループ: 必須項目チェック →
発注ステータスで帳票種別決定 → テンプレートコピー → プレースホルダー置換 → PDF 変換 → 保存 →証憑URL書き込み。1 件エラーでも後続継続。 - 完了後、
SpreadsheetApp.getUi().alert()で「成功 N 件 / 失敗 M 件(失敗発注ID: ORD_..., ORD_...)」サマリーを表示。 finally節でlock.releaseLock()。
- 選択行(複数行可)の各
内部ヘルパー:
createPdfFromTemplate_(templateId, replacements, outputFileName, outputFolder)- 引数: テンプレート fileId、
{ '{{placeholder}}': '値' }形式の置換マップ、出力ファイル名(拡張子.pdf除く)、保存先フォルダ (GoogleAppsScript.Drive.Folder)。 - 実装順序(PDF 変換後は編集不可のため この順序を厳守):
var tempCopy = DriveApp.getFileById(templateId).makeCopy(outputFileName + '_tmp', outputFolder);var doc = DocumentApp.openById(tempCopy.getId());var body = doc.getBody();→body.replaceText(key, value)でプレースホルダー置換(複数回)。doc.saveAndClose();var pdfBlob = tempCopy.getAs('application/pdf').setName(outputFileName + '.pdf');var pdfFile = outputFolder.createFile(pdfBlob);tempCopy.setTrashed(true);(中間 Google ドキュメントをゴミ箱に移動し蓄積防止)。- return
pdfFile.getUrl();
- 引数: テンプレート fileId、
Step 3: 帳票切り替えロジック
発注ステータス の値で以下のように分岐する(Phase 1 の Read で確認した OrderDTO 定義の 3 値のみを使用):
発注ステータス | 使用テンプレート | 出力ファイル名(例) |
|---|---|---|
"見積中" | TEMPLATE_ID_ESTIMATE | 見積依頼書_{取引先名}_{発注ID} |
"発注済" | TEMPLATE_ID_ORDER | 発注書_{取引先名}_{発注ID} |
"検収済" | TEMPLATE_ID_INSPECTION | 検収書_{取引先名}_{発注ID} |
| その他(空・未定義値) | — | エラー、当該行スキップ・失敗サマリーに発注IDを記録 |
プレースホルダー設計例(Googleドキュメントテンプレート側で予め埋め込む):
{{発注ID}} / {{取引先名}} / {{契約・件名}} / {{起票日時}} / {{税抜金額}} / {{消費税額}} / {{税込金額}} / {{摘要}} / {{開始年月}} / {{終了年月}} / {{組織名}} / {{PJ名}} / {{発行日}}(= 本日)
Step 4: YYYY-MM サブフォルダの動的作成
生成 PDF は PDF_OUTPUT_FOLDER_ID 直下ではなく、OrderDTO.起票日時 の年月(YYYY-MM)サブフォルダに保存する。
var ym = Utilities.formatDate(new Date(dto['起票日時']), 'JST', 'yyyy-MM');
var rootFolder = DriveApp.getFolderById(rootFolderId);
var iter = rootFolder.getFoldersByName(ym);
var ymFolder = iter.hasNext() ? iter.next() : rootFolder.createFolder(ym);
getFoldersByName(ym)で既存サブフォルダを再利用し、なければcreateFolder(ym)で新規作成。起票日時が空・不正値のときは当日日時でYYYY-MMを算出(エッジケース参照)。
Step 5: 証憑URL 列の更新(OrderRepository.save() 非使用)
生成 PDF の URL は OrderRepository.save(dtos) では書き込まない。理由:
save()はwriteDtosToSheet_によりシート全体を全行置換する。複数ユーザーが同時に31_wrk_orderを編集している場合、本案件の処理中にユーザーが加えた編集内容を巻き戻す。- そのため、対象行の
証憑URL列セルのみをRange.setValue()で直接書き込む方式を採用する(推奨)。
var sheet = Utils.getSheetByKey('WRK_ORDR', '31_wrk_order');
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var evidenceColIdx = headers.indexOf('証憑URL') + 1; // 1-based
// targetRow は選択範囲から取得した 1-based のシート行番号
sheet.getRange(targetRow, evidenceColIdx).setValue(pdfUrl);
Step 6: メニュー登録(100_config/101_sys_config.js)
Constants.MENU_DEFINITION(000_infra/002_constants.js L206-324)のサイドバー項目末尾「📋 サイドバー: 📝 費用登録」カテゴリの直後、または「📋 サイドバー: 📒 経理業務 (RPA / Action)」カテゴリ内に、以下のメニュー項目を追加する(最終決定は実装時の既存メニュー並び順を確認した上で行う):
{ label: '📄 発注書PDF発行(選択行)', funcName: 'generateOrderDocument', description: '31_wrk_order の選択行から発注ステータスに応じた PDF 帳票を生成し、証憑URL 列を更新' }
編集対象は Constants.MENU_DEFINITION のみ。onOpen() 本体は変更しない。
Step 7: 並行実行防止・二重発行対応
LockService.getScriptLock().tryLock(30000)(30 秒待機)で同時実行をブロック。取得失敗時はSpreadsheetApp.getUi().alert('他の発行処理が実行中です。完了後に再実行してください')を表示して早期 return。- 同一
発注IDで再発行した場合:DriveApp.getFilesByName(expectedName + '.pdf')で YYYY-MM サブフォルダ内の既存ファイルを検索し、存在すれば_v2/_v3…のサフィックスを付与した別名で新規ファイルを作成する(既存ファイルは削除せず履歴として残す)。証憑URL列は最新版 URL で上書き。
影響範囲
コード変更
| ファイル | 変更内容 |
|---|---|
400_domain/408_document_generator.js | 新規作成。generateOrderDocument() 公開関数 + createPdfFromTemplate_() 内部ヘルパー |
000_infra/002_constants.js | Constants.MENU_DEFINITION へのメニュー項目追加 1 行のみ(対象カテゴリは Step 6 参照) |
データ変更
| シート・列 | 変更内容 |
|---|---|
03_sys_params | TEMPLATE_ID_ESTIMATE / TEMPLATE_ID_ORDER / TEMPLATE_ID_INSPECTION / PDF_OUTPUT_FOLDER_ID の 4 行を手動追加 |
31_wrk_order.証憑URL | 実行時に対象行の PDF URL を書き込み(他列は変更しない) |
Drive リソース
PDF_OUTPUT_FOLDER_IDで指定したフォルダ配下にYYYY-MMサブフォルダを自動生成- 生成 PDF ファイル、およびテンプレート中間コピー(処理後即ゴミ箱行き)
影響を受けない範囲
42_trn_journal(仕訳帳)、32_wrk_invoice(請求レコード)、33_wrk_bank(決済レコード)など会計本体のデータは一切変更しない。本案件は書類発行のみであり会計計上は行わない。
注意事項
発注ステータスの有効値は"見積中"/"発注済"/"検収済"の 3 値のみ(OrderDTOの JSDoc 定義に準拠)。元プロンプトに記載されていた"納品完了"は存在しない値のため使用禁止。これら以外の値を持つ行は処理をスキップし、失敗サマリーに記録する。Googleドキュメントの処理順序厳守: テンプレートコピー →
DocumentApp.openById()でプレースホルダー置換 →doc.saveAndClose()→getAs('application/pdf')→ 保存 → 中間コピーsetTrashed(true)。PDF 変換後は内容を編集できないため、置換は必ず PDF 変換前に完了させる。並行実行防止は
LockService.getScriptLock()を使用。CacheServiceはキャッシュ用途の API であり、プロセスロックには GAS の正式機構であるLockServiceが適切。tryLock(30000)の戻り値(boolean)を必ずチェックし、取得失敗時はユーザー通知して早期 return する。OrderRepository.save(dtos)は全行置換であり複数ユーザー並行操作時の競合リスクが大きい。対象行の証憑URLセルのみ更新する本案件ではsheet.getRange(rowNum, colIdx).setValue(url)を直接使用する方が安全。テンプレート中間コピーは処理成功・失敗によらず
setTrashed(true)で Trash に移動する(try/finally で確実にクリーンアップ)。放置するとユーザーの Google ドライブにゴミファイルが蓄積する。テンプレートファイルの共有権限: テンプレート Google ドキュメントおよび
PDF_OUTPUT_FOLDER_IDの Drive フォルダは、GAS 実行ユーザー(通常はスプレッドシート所有者)が編集権限を持つ必要がある。権限不足時はDriveApp.getFileById()/makeCopy()が例外を投げるため、呼び出し元で catch して「テンプレートまたは出力フォルダの共有設定を確認してください」の alert を表示する。Constants.getParam()はキャッシュ参照。同一実行内で03_sys_paramsの値を更新してもキャッシュが優先されるため、本案件の範囲では設定値を実行中に書き換える操作は行わない(実装上は読み取りのみ)。
エッジケース
| 条件 | 動作 | 理由 |
|---|---|---|
選択行に該当 OrderDTO なし(選択がヘッダー行のみ等) | SpreadsheetApp.getUi().alert('対象の発注行を選択してください') を表示し処理中断 | ユーザーの選択操作誤りを即時フィードバック |
必須項目(取引先名 / 契約・件名 / 税抜金額_発注)が空 | 当該行をスキップし、失敗サマリーに ORD_xxx: 必須項目欠落(取引先名, ...) を記録して後続継続 | テンプレート差し込みに必要な情報が不足。他の行の処理は阻害しない |
税抜金額_発注 が負値 | 当該行をスキップし、失敗サマリーに記録 | 負の発注残高は会計上無効。誤入力の可能性が高い |
税抜金額_発注 が 0 | 許容(PDF 生成に進む) | 無償契約・覚書等の合法ケース |
発注ステータス が "見積中" / "発注済" / "検収済" 以外(空・旧値・誤入力) | 当該行をスキップし、失敗サマリーに ORD_xxx: 発注ステータス不正(実値: "xxx") を記録 | OrderDTO 定義外の値での帳票生成は不可 |
起票日時 が空・不正な日付 | 当日日時(new Date())で YYYY-MM サブフォルダを算出して処理継続 | 既存データのクリーンアップ不要。運用互換性を優先 |
同一 発注ID で 2 回目以降の発行 | YYYY-MM サブフォルダ内で既存同名ファイルを検索し、_v2 / _v3 … サフィックスを付与した別名で新規作成。証憑URL は最新版 URL で上書き | 再発行履歴を保持しつつ最新版への参照を維持 |
03_sys_params のテンプレートID(3種)またはフォルダIDのいずれかが空・未登録 | 処理全体を中断し、SpreadsheetApp.getUi().alert('テンプレートIDまたは出力フォルダIDが未設定です。03_sys_params に TEMPLATE_ID_ESTIMATE / TEMPLATE_ID_ORDER / TEMPLATE_ID_INSPECTION / PDF_OUTPUT_FOLDER_ID を登録してください') を表示 | 設定不備時は明確なエラーメッセージで初期設定手順を案内 |
| テンプレート Googleドキュメント / 出力フォルダへの編集権限なし | DriveApp.getFileById() / makeCopy() の例外を catch し、SpreadsheetApp.getUi().alert('テンプレート/出力フォルダの共有設定を確認してください。実行ユーザーに編集権限が必要です') を表示 | Drive API の権限エラーは業務運用上起きやすいため明示的にハンドル |
LockService.getScriptLock().tryLock(30000) が false を返す | SpreadsheetApp.getUi().alert('他の発行処理が実行中です。完了後に再実行してください') を表示して早期 return | 二重実行防止。並行発行による重複 PDF 生成を回避 |
複数行選択で途中 1 件が makeCopy 失敗(例: Drive クォータ超過) | 当該行のみ失敗扱いで記録し、中間コピーがあれば setTrashed(true)。後続行は処理継続 | 1 件の失敗で全体停止すると運用に支障。try/catch で個別ハンドル |
| 処理完了時(成功 0 件 / 失敗 > 0) | SpreadsheetApp.getUi().alert('発行に失敗しました。失敗発注ID: ORD_..., ORD_...\n詳細はログを確認してください') を表示 | 異常系でもユーザーに明確に通知 |
| 処理完了時(成功 > 0 / 失敗 = 0) | SpreadsheetApp.getUi().alert('発注書 N 件を発行しました。証憑URL列を確認してください') を表示 | 正常系の完了通知 |
実データ検証
実装着手前に以下を MCP(Google Sheets API)またはスプレッドシートの目視で確認すること。
31_wrk_order.発注ステータス列の実値- 実際に格納されている値が
"見積中"/"発注済"/"検収済"のみか確認(旧仕様の残存値や表記揺れがないか)。 - もし
"納品完了"などの異値が残っていれば、マイグレーションで修正するか、本案件では「スキップ対象」として扱う方針を確定する。
- 実際に格納されている値が
31_wrk_order.証憑URL列の存在・列位置OrderDTO定義上は末尾フィールドだが、実シートのヘッダーが同じ順序で存在するか確認。存在しない場合はsetupAllSchemasで DDL 再適用が必要。
03_sys_paramsのキー登録状況- Phase 1 の grep では以下 4 キーのヒットなし。未登録前提で実装し、実装完了後に手動登録手順をユーザーに案内する:
TEMPLATE_ID_ESTIMATETEMPLATE_ID_ORDERTEMPLATE_ID_INSPECTIONPDF_OUTPUT_FOLDER_ID
- 命名整合性: MAS-100(請求書PDF発行)では
CFG_TEMPLATE_ID_QUOTE/CFG_TEMPLATE_ID_DELIVERY/CFG_TEMPLATE_ID_INVOICEのようにCFG_プレフィックスを採用している。本案件は元プロンプト指定どおりTEMPLATE_ID_*で進めるが、MAS-100 との命名統一が必要と判断された場合は実装時にCFG_TEMPLATE_ID_ESTIMATE等へ変更してもよい(仕様書・実装プロンプト双方で置換するだけで対応可能)。
- Phase 1 の grep では以下 4 キーのヒットなし。未登録前提で実装し、実装完了後に手動登録手順をユーザーに案内する:
- 対象 Googleドキュメントテンプレートの事前準備
- 「見積依頼書」「発注書」「検収書」の各テンプレートを Google ドキュメントで作成し、
{{発注ID}}等のプレースホルダーをテキストとして埋め込む必要がある。実装完了後の動作確認ステップに含める。
- 「見積依頼書」「発注書」「検収書」の各テンプレートを Google ドキュメントで作成し、
関連ドキュメント
docs/dev/dev_mas-100_document_generation.md— 兄弟機能(請求書PDF発行)。テンプレート差し込み・PDF 生成の実装パターンは MAS-100 と揃える。命名規則(CFG_プレフィックス採否)は両案件で統一すべき検討事項。CLAUDE.md§「GAS ファイル番号体系 (Modular Monolith)」 — Option A を選択した根拠(400_domain/の連番採番)。docs/_internal/TODO_future.mdMAS-127 行 — 原案件メモ(「MAS-100 と共通化すべきコード範囲」「帳票番号採番ルール」の論点提示)。
人間が検討すべき事項
TODO_future.md MAS-127 記載事項(原文転記)
- テンプレートデザイン — 「見積依頼書 / 発注書 / 検収書」それぞれのレイアウト・ロゴ配置・必須項目をユーザー側で確定する必要がある。
- 帳票番号の採番ルール — 見積書番号 / 発注書番号 / 検収書番号を別体系で採番するか、
発注ID(ORD_YYYYMMDD_NNNN)を流用するか。本仕様書は後者(発注ID流用)を前提とするが、取引先向け書類として別採番が必要な場合は追加機能となる。 - MAS-100 と共通化すべきコード範囲 —
createPdfFromTemplate_()を共通ヘルパーに切り出すか。MAS-100 が未実装の現時点では各ファイルで実装し、MAS-100 実装後にリファクタリングする方針。
本仕様で決定した事項(再掲)
- ①新規ファイル配置先: Option A(
400_domain/408_document_generator.js)。CLAUDE.md 変更不要、既存400_domain/レイヤーに統一。 - ②
発注ステータス分岐値:OrderDTO定義に従い"見積中"/"発注済"/"検収済"のみ。"納品完了"は不使用。 - ③並行実行防止:
LockService.getScriptLock().tryLock(30000)。CacheServiceは採用しない。 - ④
証憑URL更新方式:OrderRepository.save()ではなくsheet.getRange().setValue()で対象セル直接書き込み。
未決定・要検討の事項
- テンプレートファイルの作成・管理担当: エンジニアがサンプルテンプレートを作成するか、経理担当がレイアウト込みで作成するか。プレースホルダー命名規則(
{{発注ID}}等)は実装仕様として本書に定義済み。 - メール送付機能のスコープ: TODO_future.md では「取引先へのメール送付機能(オプション)」と記載されているが、本案件のスコープ外として扱う(将来 MAS-127 派生案件または I-系案件として別立てる)。
- インボイス制度対応: MAS-100 では T 番号(適格請求書発行事業者登録番号)の差し込みが論点となるが、発注書は受領側書類ではないため本案件では対応不要。ただし「発行元社名」「発行元住所」をテンプレート側で固定値として記載するか、
03_sys_paramsのキー(例:CFG_ISSUER_COMPANY_NAME)から動的差し込みするかは実装時に判断。 CFG_プレフィックス統一: 実データ検証 §3 参照。MAS-100 と命名揃えが必要なら実装時に置換。
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件MAS-127「発注書PDF発行(見積・注文・納品書)」を実装してください。
## 実行前タスク
- `000_infra/003_contracts.js` を Read し、`OrderDTO` の全フィールド名と `発注ステータス` の取りうる値が `"見積中"` / `"発注済"` / `"検収済"` の 3 値のみであることを確認する。`証憑URL` フィールドの存在も確認する。
- `000_infra/002_constants.js` を Read し、`Constants.getParam(key, defaultVal)` のシグネチャ(L147-167)、`Constants.MENU_DEFINITION`(L206-324)のカテゴリ構造、`SHEET_DEFAULTS` の `31_wrk_order` エントリ(L84)を確認する。
- `200_data/202_repository.js` を Read し、`OrderRepository.findAll()` の戻り値 `{ headers, dtos }` と `save()` が `writeDtosToSheet_` による全行置換であることを確認する。`_getSheet()` が `Utils.getSheetByKey('WRK_ORDR', '31_wrk_order')` を呼ぶことも確認する。
- `000_infra/004_utils.js` L302-312 を Read し、`Utils.getSheetByKey(key, fallbackName)` のシグネチャを確認する。
- `100_config/101_sys_config.js` の `onOpen()`(L323-350)を Read し、メニュー追加は `Constants.MENU_DEFINITION` への宣言的登録で行う方式であることを確認する(`onOpen()` 本体は編集しない)。
- `03_sys_params` シートに `TEMPLATE_ID_ESTIMATE` / `TEMPLATE_ID_ORDER` / `TEMPLATE_ID_INSPECTION` / `PDF_OUTPUT_FOLDER_ID` の 4 キーが登録済みかユーザーに確認依頼する(**未登録前提**で実装を進め、登録案内を動作確認ステップに含める)。
## 修正対象ファイル
- `400_domain/408_document_generator.js`(**新規作成**。このファイルのみ新規)
- `000_infra/002_constants.js`(`Constants.MENU_DEFINITION` へのメニュー項目追加 1 行のみ。`onOpen()` 本体は編集しない)
## 実装内容
### A. `400_domain/408_document_generator.js` 新規作成
1. ファイル先頭にモジュールヘッダーコメント(案件番号 MAS-127、役割「発注書PDF発行」)を記載。
2. 公開関数 `generateOrderDocument()` を実装:
- `LockService.getScriptLock().tryLock(30000)` で二重実行防止。取得失敗時は `SpreadsheetApp.getUi().alert('他の発行処理が実行中です。完了後に再実行してください')` を表示し早期 return。
- `Constants.getParam('TEMPLATE_ID_ESTIMATE', '')` / `Constants.getParam('TEMPLATE_ID_ORDER', '')` / `Constants.getParam('TEMPLATE_ID_INSPECTION', '')` / `Constants.getParam('PDF_OUTPUT_FOLDER_ID', '')` の 4 値を取得。1 つでも空なら alert で中断。
- `SpreadsheetApp.getActiveSheet()` で現在のアクティブシートを取得し、タブ名が `31_wrk_order` でなければ alert で中断(または `Utils.getSheetByKey('WRK_ORDR', '31_wrk_order')` 直接参照でアクティブシート制約を緩めても可)。
- `SpreadsheetApp.getActiveRangeList()` から選択行番号(1-based シート行番号)配列を抽出。ヘッダー行(`row === 1`)は除外。
- `OrderRepository.findAll()` を呼び `{ headers, dtos }` を取得。シート行 `rowNum` に対する DTO は `dtos[rowNum - 2]`(ヘッダー 1 行分を引く)。
- 選択行 DTO ごとに内部関数 `processOrderRow_(dto, rowNum, headers, sheet, templates, rootFolderId)` を呼ぶ。try/catch で個別エラーを吸収し、`successIds` / `failedEntries` 配列に集計。
- 全件処理後、完了サマリーを `SpreadsheetApp.getUi().alert()` で表示(成功件数 / 失敗件数 / 失敗発注ID 一覧)。
- `finally` 節で `lock.releaseLock()`。
3. 内部関数 `processOrderRow_(dto, rowNum, headers, sheet, templates, rootFolderId)`:
- 必須項目(`取引先名` / `契約・件名` / `税抜金額_発注`)が空なら throw。
- `税抜金額_発注` が負値なら throw。
- `発注ステータス` で帳票種別・テンプレートID・出力ファイル名プレフィックスを決定(`"見積中"` → `TEMPLATE_ID_ESTIMATE` / `見積依頼書`、`"発注済"` → `TEMPLATE_ID_ORDER` / `発注書`、`"検収済"` → `TEMPLATE_ID_INSPECTION` / `検収書`、その他 → throw)。
- `起票日時` から `YYYY-MM` を算出(空・不正値なら `new Date()` フォールバック)。`DriveApp.getFolderById(rootFolderId).getFoldersByName(ym)` → 既存なら再利用、なければ `createFolder(ym)`。
- 出力ファイル名: `{帳票プレフィックス}_{取引先名}_{発注ID}`。同名ファイルが既存なら `_v2` / `_v3` … サフィックス付与。
- 内部ヘルパー `createPdfFromTemplate_(templateId, replacements, outputFileName, outputFolder)` を呼び PDF URL を取得。
- `var evidenceColIdx = headers.indexOf('証憑URL') + 1;` で列番号取得 → `sheet.getRange(rowNum, evidenceColIdx).setValue(pdfUrl);` で直接書き込み(`OrderRepository.save()` は使わない)。
4. 内部関数 `createPdfFromTemplate_(templateId, replacements, outputFileName, outputFolder)`:
- `var tempCopy = DriveApp.getFileById(templateId).makeCopy(outputFileName + '_tmp', outputFolder);`
- `var doc = DocumentApp.openById(tempCopy.getId());`
- `var body = doc.getBody();` → `replacements` の各キーに対し `body.replaceText(key, String(value || ''));`
- `doc.saveAndClose();`
- `var pdfBlob = tempCopy.getAs('application/pdf').setName(outputFileName + '.pdf');`
- `var pdfFile = outputFolder.createFile(pdfBlob);`
- try/finally で `tempCopy.setTrashed(true);`(失敗時もゴミ箱に移動)。
- return `pdfFile.getUrl();`
5. プレースホルダー置換マップ例(`replacements`):
```js
var replacements = {
'{{発注ID}}': dto['発注ID(ORD)'] || '',
'{{取引先名}}': dto['取引先名'] || '',
'{{契約・件名}}': dto['契約・件名'] || '',
'{{起票日時}}': Utilities.formatDate(new Date(dto['起票日時']), 'JST', 'yyyy-MM-dd'),
'{{税抜金額}}': Number(dto['税抜金額_発注'] || 0).toLocaleString(),
'{{消費税額}}': Number(dto['消費税額_発注'] || 0).toLocaleString(),
'{{税込金額}}': Number(dto['税込金額_発注'] || 0).toLocaleString(),
'{{摘要}}': dto['摘要'] || '',
'{{開始年月}}': dto['開始年月'] || '',
'{{終了年月}}': dto['終了年月'] || '',
'{{組織名}}': dto['組織名'] || '',
'{{PJ名}}': dto['PJ名'] || '',
'{{発行日}}': Utilities.formatDate(new Date(), 'JST', 'yyyy-MM-dd')
};
```
### B. `000_infra/002_constants.js` メニュー項目追加
`Constants.MENU_DEFINITION` の「📋 サイドバー: 📒 経理業務 (RPA / Action)」または既存カテゴリの末尾に以下を追加(最終配置は既存並びを見て決定):
```js
{ label: '📄 発注書PDF発行(選択行)', funcName: 'generateOrderDocument', description: '31_wrk_order の選択行から発注ステータスに応じた PDF 帳票を生成し、証憑URL 列を更新' }
```
## 制約
- **`発注ステータス` の分岐値は `OrderDTO` 定義の 3 値(`"見積中"` / `"発注済"` / `"検収済"`)のみ使用**。`"納品完了"` 等は使用禁止。
- **`OrderRepository.save(dtos)` は使用しない**。対象セルへの `sheet.getRange().setValue()` 直接書き込みで `証憑URL` を更新する。
- **並行実行防止は `LockService.getScriptLock().tryLock(30000)` を使う**。`CacheService` は使用しない。
- シート直接アクセスが必要な場合は `Utils.getSheetByKey('WRK_ORDR', '31_wrk_order')` 経由で取得すること。
- `onOpen()` 本体は編集しない。メニュー追加は `Constants.MENU_DEFINITION` への宣言的登録で行う。
- テンプレート中間コピーは try/finally で必ず `setTrashed(true)` する(ゴミファイル蓄積防止)。
- 既存 `Constants.MENU_DEFINITION` の他項目を削除・改変しない。
- 列参照はヘッダー名ベース(`headers.indexOf('証憑URL')`)。列番号ハードコード禁止(CLAUDE.md コーディング規約)。
## エッジケース
(仕様書 §エッジケース のテーブルをそのまま遵守。必須項目欠落・負値・無効ステータス・同一発注ID再発行・テンプレートID未設定・権限不足・ロック取得失敗等は仕様書記載の動作で実装する)
## 実データ検証
(仕様書 §実データ検証 の 4 項目を実装着手前に確認。特に `03_sys_params` の 4 キー未登録前提で実装を進め、登録手順を動作確認ステップでユーザー案内する)
## 動作確認
1. `03_sys_params` に `TEMPLATE_ID_ESTIMATE` / `TEMPLATE_ID_ORDER` / `TEMPLATE_ID_INSPECTION` / `PDF_OUTPUT_FOLDER_ID` の 4 キーを手動登録(テンプレート 3 種を事前に Google ドキュメントで作成し、fileId を登録)。
2. `npm run push:dev` で開発環境にデプロイ。
3. スプレッドシートを開き直し、メニュー「📋 サイドバー: 📒 経理業務 (RPA / Action)」(または追加先カテゴリ)に「📄 発注書PDF発行(選択行)」が表示されることを確認。
4. `31_wrk_order` に `発注ステータス='見積中'` のテストデータを 1 行作成し、当該行を選択してメニュー実行。`PDF_OUTPUT_FOLDER_ID` 配下の `YYYY-MM` サブフォルダに「見積依頼書_{取引先名}_{発注ID}.pdf」が生成され、`証憑URL` 列に URL が書き込まれることを確認。
5. 同じ行で `発注ステータス='発注済'` に変更してメニュー再実行。「発注書_..._v2.pdf」が別名で生成され、`証憑URL` が v2 の URL に上書きされることを確認。
6. さらに `発注ステータス='検収済'` に変更してメニュー再実行。「検収書_..._v3.pdf」が生成されることを確認。
7. `発注ステータス` を不正値(例: `"納品完了"`)に変更して実行し、失敗サマリーに発注IDが記録されることを確認。
8. 必須項目(例: `取引先名`)を空にして実行し、スキップ+失敗サマリー記録を確認。
9. `03_sys_params` のいずれかのテンプレートIDを一時的に削除して実行し、未設定案内の alert が表示され処理中断することを確認。
10. 複数行選択で実行し、一部エラー行があっても成功行が処理され、完了サマリーに成功・失敗件数が正しく表示されることを確認。
11. **`42_trn_journal` / `32_wrk_invoice` / `33_wrk_bank` に新規レコードが追加されないこと**(本案件は書類発行のみで会計計上は行わない)を確認。
12. 動作検証完了後、`npm run push:prod` で本番デプロイ。本番の `03_sys_params` にも 4 キー登録が必要。
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read) | あり | ファイル構造・行番号・メニュー登録方式の確定 |
| 実装(Write/Edit) | なし | Phase 1 確定済み内容の書き下し |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 実行前タスク・ファイル配置決定 | Claude Sonnet | 複数ファイル横断の確認と設計判断(Option A 採用の再確認・Constants.MENU_DEFINITION への追加箇所決定)が必要 |
PDF 生成ヘルパー新設(408_document_generator.js) | Claude Sonnet | DriveApp / DocumentApp API と既存 Repository パターンの理解、エラーハンドリング設計が必要 |
Constants.MENU_DEFINITION へのメニュー項目追加 | Claude Haiku | 挿入位置特定済み・1 行追加のみで判断要素なし |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-22 | 初版作成(Claude Opus 4.7) |
仕様書作成プロンプト
展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。Phase 2(清書)の各Step内では拡張思考を最小限に抑え、Phase 1で確定済みの内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等のtextのみでtool_useなしにturnを終了しない。説明は1文以内。直ちにtoolを呼ぶ。
3. **4-5分割のWrite/Edit実行**: 骨格(2-1)/概要〜注意事項(2-2)/エッジケース〜人間検討事項(2-3a)/実装プロンプト〜変更履歴(2-3b)/<details>記録(2-4)に分割。1回のWrite/Editは約300行以内。
4. **各Stepで何を書くかを具体指示**: 設計判断をPhase 2実行時に持ち込まない。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
CLIエージェント「Claude Code」として、案件S-55「発注書PDF発行(見積・注文・納品書)」の開発仕様書を作成してください。
開発仕様書を新規作成した後は、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記すること。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
Phase 2の清書中に設計判断を再考しないよう、以下を全てPhase 1で確定させること。**Grepはファイルの発見までに留め、型・フィールド名・メニュー文字列等の「どう書くか」の判断は必ずReadで裏取りする。**
### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` を検索し、S-55の案件名・概要・優先度・人間が検討すべき事項を取得する。あわせてS-28「請求書PDF発行」が存在するか確認し、存在すれば概要を把握する(共通ヘルパー設計の前提判断に使う)。
### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` を読み込み、ファイル番号体系(百の位=レイヤー一覧)・コーディング規約・デプロイフローを確認する。
### 1-C: 既存仕様書テンプレートの読み込み
- `docs/dev/` 配下のUI/メニュー追加系仕様書を1件読み込み、フォーマットを把握する(例: `dev_mas-094_boundary_month_selector.md`)。
### 1-D: 関連GASコードの読み込み(以下を必ずReadすること)
| ファイル | 確認ポイント |
|---------|------------|
| `000_infra/003_contracts.js` | `OrderDTO`の全フィールド名と型。特に`発注ステータス`の取りうる値(コメントに`"見積中" | "発注済" | "検収済"`と定義されているか)、`証憑URL`フィールドの存在を実際のコードで確認 |
| `000_infra/002_constants.js` | `Constants.getParam(key, defaultVal)`のシグネチャと実装(L何行目か)、`SHEET_DEFAULTS`の`31_wrk_order`エントリのフィールド名、`ID_PREFIX_MAP`の`31_wrk_order`エントリ(プレフィックス`ORD_`確認) |
| `200_data/202_repository.js` | `OrderRepository.findAll()`の戻り値形式`{ headers: string[], dtos: OrderDTO[] }`、`OrderRepository.save(dtos)`の動作(全行置換であることに注意)、`_getSheet()`が使うキー`'WRK_ORDR'` |
| `000_infra/004_utils.js` | `Utils.getSheetByKey(key, fallbackName)`のシグネチャ |
| `100_config/101_sys_config.js` | `onOpen()`のメニュー構造全体を確認し、帳票発行メニューを追加すべき親メニュー名の正確な文字列を特定する。既存のPDF生成・ドキュメント操作系メニューの有無も確認 |
| `400_domain/` 配下 | 既存のPDF生成・GoogleドキュメントAPI操作コードの有無をgrepで確認 |
| `500_import/` 配下 | 同上(DriveApp/DocumentApp操作パターンの参考) |
### 1-E: Phase 1で確定すべき設計判断事項
**① 新規ファイルの配置先(要確定)**
`700_lib/701_document_generator.js`はCLAUDE.mdのファイル番号体系(000/100/200/300/400/500/600/800/900)に存在しないレイヤーであり、このまま使用するとCLAUDE.md更新が必要になる。以下の選択肢から調査の上どちらかに決定し、仕様書に明記すること:
- **Option A**: `400_domain/` 配下に追加(`401〜420`の最大番号を確認して次番を採番。例: `408_document_generator.js`)— CLAUDE.md変更不要
- **Option B**: `700_doc/701_document_generator.js`等の新層を追加 — CLAUDE.mdのファイル番号体系テーブルへの追記が必要
**② 帳票切り替えロジックで使う`発注ステータス`の値**
`003_contracts.js`のReadで確認した実際の値のみを使う。**Geminiが生成した元プロンプトに記載の`"納品完了"`は`OrderDTO`の取りうる値に存在しない可能性が高い(定義は`"見積中" | "発注済" | "検収済"`)**。必ずReadで確認してから仕様書に記載すること。
**③ 並行実行防止機構**
GASには`LockService.getScriptLock().tryLock(ms)`という正式なロック機構が存在する。元プロンプトの`CacheService`によるロックはキャッシュ用途のAPIであり、プロセスロックには`LockService`が適切。仕様書では`LockService.getScriptLock()`を第一候補として記載する。
**④ `03_sys_params`のキー登録状況**
`Constants.getParam()`が参照する`03_sys_params`シートに、`TEMPLATE_ID_ESTIMATE` / `TEMPLATE_ID_ORDER` / `TEMPLATE_ID_INSPECTION` / `PDF_OUTPUT_FOLDER_ID`のキーが既登録か否かを確認(MCPまたはReadで)。未登録の場合は実装後に手動登録が必要な旨を仕様書の「注意事項」と「動作確認」に明記する。
---
## Phase 2: 仕様書の分割作成(Step 2-1 骨格 / 2-2 概要〜注意事項 / 2-3a エッジケース〜人間検討事項 / 2-3b 実装プロンプト〜変更履歴 / 2-4 仕様書作成プロンプト記録)
出力先: `docs/dev/dev_mas-127_order_pdf_generation.md`
(中略。詳細は本仕様書の各セクション構成を参照)
---
## Phase 3: `_config.json`への追記・changelog更新・コミット
1. `docs/_config.json`をReadし、`§E.6 パイプライン・RPA・外部連携`(または案件性質に最も近いセクション)のnav配列に以下を追加:
`{ "file": "dev/dev_mas-127_order_pdf_generation.md", "title": "E.6.X S-55 発注書PDF発行" }`
2. `Bash: node -e "require('./docs/_config.json')"` でJSON構文が壊れていないことを確認
3. `docs/_internal/changelog.md`の先頭行(ヘッダー直後)に追記
4. `git add` → `git commit` → `git push`
</instruction>