概要

項目内容
案件IDMAS-127
案件名発注書PDF発行(見積・注文・納品書)
カテゴリドキュメント生成
Phase実装
優先度P3(★)
所要時間4〜6時間
実装ステータス📝 仕様書段階・実装未着手 (2026-04-28 監査時点)
対象ファイル400_domain/408_document_generator.js(新規作成)/ 100_config/101_sys_config.jsConstants.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_MAP31_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_DEFAULTS31_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_DEFINITION000_infra/002_constants.js L206-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 のシステムキー→タブ名マッピングを使い、見つからなければ fallbackNamegetSheetByName する。本案件は Utils.getSheetByKey('WRK_ORDR', '31_wrk_order')31_wrk_order シートを取得する。

既存の PDF 生成コード(400_domain / 500_import への grep 結果)

  • DriveApp / DocumentApp を使うファイルは 500_import/502_receipt_reader.js500_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/ は既存レイヤー)
B700_doc/701_document_generator.js 等の新層必要(ファイル番号体系テーブルの追記)×

採用理由:

  • CLAUDE.md の現行ファイル番号体系(000/100/200/300/400/500/600/800/900)に 700_ は存在しない。新層追加は CLAUDE.mddocs/_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.js400/401/402/403/404/405/406/407/410/420)。408 は未使用のため衝突しない。

Step 1: 03_sys_params シートへのキー登録(手動作業)

実装着手前に、03_sys_params シートへ以下 4 キーを手動追加する(Phase 1 の grep でコード・過去仕様書にヒットしなかったため、未登録の可能性が極めて高い)。

キー値の例用途
TEMPLATE_ID_ESTIMATE1AbCdEf...xyz「見積依頼書」用 Googleドキュメントテンプレートの fileId
TEMPLATE_ID_ORDER1AbCdEf...xyz「発注書」用 Googleドキュメントテンプレートの fileId
TEMPLATE_ID_INSPECTION1AbCdEf...xyz「検収書」用 Googleドキュメントテンプレートの fileId
PDF_OUTPUT_FOLDER_ID1AbCdEf...xyz生成 PDF の保存先 Google ドライブフォルダ ID

登録が未完了の場合、実装コードは SpreadsheetApp.getUi().alert() でエラーダイアログを表示して処理を中断する(詳細はエッジケース参照)。

Step 2: PDF 生成ヘルパーの新設(400_domain/408_document_generator.js

新規ファイルを作成し、以下の公開関数 1 本+内部ヘルパー 1 本を配置する。

  • 公開関数(メニュー登録対象): generateOrderDocument()

    • 選択行(複数行可)の各 OrderDTO に対して、発注ステータス に応じた PDF を生成する。
    • 実行フロー:
      1. LockService.getScriptLock().tryLock(30000) で二重実行防止(Phase 1 で確定した CacheService ではなく LockService を採用)。
      2. Constants.getParam('TEMPLATE_ID_ESTIMATE', '') 等 4 キーを取得。1 つでも空なら SpreadsheetApp.getUi().alert() で中断。
      3. SpreadsheetApp.getActiveSheet().getActiveRangeList() または getSelection() で選択範囲を取得し、対象行番号の配列を作る。
      4. OrderRepository.findAll(){ headers, dtos } を取得し、選択行インデックスに対応する DTO のみ処理対象とする。
      5. DTO ごとにループ: 必須項目チェック → 発注ステータス で帳票種別決定 → テンプレートコピー → プレースホルダー置換 → PDF 変換 → 保存 → 証憑URL 書き込み。1 件エラーでも後続継続。
      6. 完了後、SpreadsheetApp.getUi().alert() で「成功 N 件 / 失敗 M 件(失敗発注ID: ORD_..., ORD_...)」サマリーを表示。
      7. finally 節で lock.releaseLock()
  • 内部ヘルパー: createPdfFromTemplate_(templateId, replacements, outputFileName, outputFolder)

    • 引数: テンプレート fileId、{ '{{placeholder}}': '値' } 形式の置換マップ、出力ファイル名(拡張子 .pdf 除く)、保存先フォルダ (GoogleAppsScript.Drive.Folder)。
    • 実装順序(PDF 変換後は編集不可のため この順序を厳守):
      1. var tempCopy = DriveApp.getFileById(templateId).makeCopy(outputFileName + '_tmp', outputFolder);
      2. var doc = DocumentApp.openById(tempCopy.getId());
      3. var body = doc.getBody();body.replaceText(key, value) でプレースホルダー置換(複数回)。
      4. doc.saveAndClose();
      5. var pdfBlob = tempCopy.getAs('application/pdf').setName(outputFileName + '.pdf');
      6. var pdfFile = outputFolder.createFile(pdfBlob);
      7. tempCopy.setTrashed(true);(中間 Google ドキュメントをゴミ箱に移動し蓄積防止)。
      8. return pdfFile.getUrl();

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_DEFINITION000_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.jsConstants.MENU_DEFINITION へのメニュー項目追加 1 行のみ(対象カテゴリは Step 6 参照)

データ変更

シート・列変更内容
03_sys_paramsTEMPLATE_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(決済レコード)など会計本体のデータは一切変更しない。本案件は書類発行のみであり会計計上は行わない。

注意事項

  1. 発注ステータス の有効値は "見積中" / "発注済" / "検収済" の 3 値のみOrderDTO の JSDoc 定義に準拠)。元プロンプトに記載されていた "納品完了" は存在しない値のため使用禁止。これら以外の値を持つ行は処理をスキップし、失敗サマリーに記録する。

  2. Googleドキュメントの処理順序厳守: テンプレートコピー → DocumentApp.openById() でプレースホルダー置換 → doc.saveAndClose()getAs('application/pdf') → 保存 → 中間コピー setTrashed(true)PDF 変換後は内容を編集できないため、置換は必ず PDF 変換前に完了させる。

  3. 並行実行防止は LockService.getScriptLock() を使用CacheService はキャッシュ用途の API であり、プロセスロックには GAS の正式機構である LockService が適切。tryLock(30000) の戻り値(boolean)を必ずチェックし、取得失敗時はユーザー通知して早期 return する。

  4. OrderRepository.save(dtos) は全行置換であり複数ユーザー並行操作時の競合リスクが大きい。対象行の 証憑URL セルのみ更新する本案件では sheet.getRange(rowNum, colIdx).setValue(url) を直接使用する方が安全。

  5. テンプレート中間コピーは処理成功・失敗によらず setTrashed(true) で Trash に移動する(try/finally で確実にクリーンアップ)。放置するとユーザーの Google ドライブにゴミファイルが蓄積する。

  6. テンプレートファイルの共有権限: テンプレート Google ドキュメントおよび PDF_OUTPUT_FOLDER_ID の Drive フォルダは、GAS 実行ユーザー(通常はスプレッドシート所有者)が編集権限を持つ必要がある。権限不足時は DriveApp.getFileById() / makeCopy() が例外を投げるため、呼び出し元で catch して「テンプレートまたは出力フォルダの共有設定を確認してください」の alert を表示する。

  7. 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)またはスプレッドシートの目視で確認すること。

  1. 31_wrk_order.発注ステータス 列の実値
    • 実際に格納されている値が "見積中" / "発注済" / "検収済" のみか確認(旧仕様の残存値や表記揺れがないか)。
    • もし "納品完了" などの異値が残っていれば、マイグレーションで修正するか、本案件では「スキップ対象」として扱う方針を確定する。
  2. 31_wrk_order.証憑URL 列の存在・列位置
    • OrderDTO 定義上は末尾フィールドだが、実シートのヘッダーが同じ順序で存在するか確認。存在しない場合は setupAllSchemasDDL 再適用が必要。
  3. 03_sys_params のキー登録状況
    • Phase 1 の grep では以下 4 キーのヒットなし。未登録前提で実装し、実装完了後に手動登録手順をユーザーに案内する:
      • TEMPLATE_ID_ESTIMATE
      • TEMPLATE_ID_ORDER
      • TEMPLATE_ID_INSPECTION
      • PDF_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 等へ変更してもよい(仕様書・実装プロンプト双方で置換するだけで対応可能)。
  4. 対象 Googleドキュメントテンプレートの事前準備
    • 「見積依頼書」「発注書」「検収書」の各テンプレートを Google ドキュメントで作成し、{{発注ID}} 等のプレースホルダーをテキストとして埋め込む必要がある。実装完了後の動作確認ステップに含める。

関連ドキュメント

  • 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.md MAS-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.jsClaude SonnetDriveApp / 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>