MAS-180: AI-OCR連携による証憑自動起票
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-180 |
| カテゴリ | 外部連携 / AI (OCR 強化) |
| Phase | P2 |
| 優先度 | ★★ |
| 所要時間 | 2-3 時間 |
| 対象ファイル | 500_import/502_receipt_reader.js(関数追加)/ 100_config/101_sys_config.js(setupAllSchemas への WRK_OCRQ スキーマ追加・01_sys_config キー登録)/ 000_infra/002_constants.js(Constants.SHEET_DEFAULTS / Constants.ID_PREFIX_MAP / Constants.MENU_DEFINITION への追記) |
| 前提案件 | MAS-147(請求書 PDF→INV 自動起票・v2 仕様書完了)/ MAS-154(取引先略称自動生成・完了)/ MAS-157(写真 OCR・仕様書完了)/ MAS-216(Vertex AI 移行・仕様書完了) |
| 関連案件 | MAS-161(OCR 結果の手動補正アシスト)/ MAS-152(電帳法リネーム)/ MAS-203(プロンプトライブラリ版管理) |
本仕様書は TODO_future.md §5.1 No.11「MAS-180 OCR 精度強化」(docs/_internal/TODO_future.md L437)の実装仕様である。
目的
証憑画像(PDF/JPEG/PNG)から Gemini API(500_import/502_receipt_reader.js L10-14 の RECEIPT_GEMINI_PROMPT_)で抽出した OCR 結果を、信頼度判定を経て InvoiceRepository.append()(200_data/202_repository.js L185-207)経由で 32_wrk_invoice に自動起票するパイプラインを新設する。低信頼度レコードは新規シート 36_wrk_ocr_queue(システムキー WRK_OCRQ)に「要確認」ステータスで蓄積し、人間がレビュー・修正したうえで手動メニューから起票する Human-in-the-Loop(HitL) 設計とする。これにより現行 importReceiptPdfs()(L22-185)が 35_wrk_receipt への転記で止まっている情報フローを、INV 起票まで直結させ、手動 INV 作成工数を削減する。
現在のコード
現行の証憑 OCR 取込(500_import/502_receipt_reader.js)の主要関数と責務範囲は以下のとおり。Phase 1 で ls 500_import/ にて 501_cc_importer.js / 502_bank_importer.js / 502_receipt_reader.js / 504_invoice_importer.js の 4 ファイル実在を確認済み。502 番が重複しているが、本案件の拡張対象は 502_receipt_reader.js(領収書/請求書 PDF)である。
| 関数 | 行番号 | 役割 | 備考 |
|---|---|---|---|
importReceiptPdfs() | L22-185 | メニュー起点。Drive フォルダの PDF を 1 件ずつ Gemini API で抽出し、35_wrk_receipt シートに書込む | INV 起票は行わない |
callGeminiForReceipt_(apiKey, base64, fileName) | L194-269 | Google AI Studio 直叩きで Gemini 2.5-flash を呼び出し。429/503 Exponential Backoff(最大 4 回)実装済 | MAS-216 で Vertex AI 並行稼働化 |
parseGeminiReceiptText_(text) | L278-323 | Gemini レスポンスから JSON 配列を抽出。ブラケット不整合の自動補修機能あり | 共通ヘルパー |
postProcessReceiptData_(rcpSheet) | L330-403 | 同一取引先の T 番号・住所を多数決で補正(2 件以上の一致で採用) | 本案件と独立 |
callGeminiForReceiptOnVertex_(base64, fileName) | L416-521 | Vertex AI 経由版。asia-northeast1 → us-east5 フォールバック実装済 | MAS-216 並行稼働 |
出力先は 35_wrk_receipt(WRK_RCPT スキーマ:100_config/101_sys_config.js L898 で定義)のみで、Gemini の抽出結果は以下のフィールドで保持される:管理ID(RCP_NNNN)/処理日時/証憑種別/取引先名(Utils.normalizePartnerName() 適用済)/🏢住所/税込金額_決済/税抜金額_決済/消費税額_決済/源泉税額/T番号/帳票番号/発行日/発生日(P/L計上日)/決済日_実績/決済手段/摘要/ファイル名/証跡リンク。
本案件の課題: 35_wrk_receipt は「読み取ったデータの置き場」に留まり、INV 起票は人手でトリガされる(transferReceiptToAdhoc 等の別メニュー経由)。OCR 結果の信頼度判定ロジックも未実装で、金額不整合(税抜+消費税 ≠ 税込)や取引先マスタ未ヒットの判別も手動目視に依存している。
修正方針
本案件では 502_receipt_reader.js を拡張する形で、OCR → 信頼度判定 → 自動/確認キュー振り分け → INV 起票 の 3 Step を実装する。既存 importReceiptPdfs() は変更せず、新規関数を追加する方式で後方互換性を保つ。
Step 1: 36_wrk_ocr_queue シートの新設(確認キュー基盤)
対象ファイル: 100_config/101_sys_config.js(DDL)/000_infra/002_constants.js(定数)
100_config/101_sys_config.jsのsetupAllSchemas()にWRK_OCRQスキーマを追加schemasオブジェクト(L826-901)に以下のキーを追加:'WRK_OCRQ': { headers: [...], color: "#38761d" }- ヘッダー構成は「エッジケース」セクションの「
36_wrk_ocr_queueの列構成」に準拠(18 列)。 01_sys_configへの登録:L820-823 のif (!existKeys.includes('WRK_CARD')) confSheet.appendRow(...)パターンに倣い、WRK_OCRQキーをappendRow(['WRK_OCRQ', '', '36_wrk_ocr_queue', 'AI-OCR確認キュー'])で追加。
000_infra/002_constants.jsのID_PREFIX_MAP(L93-112)にエントリ追加- 既存の
{ pattern: '16_wrk_master', prefix: 'REQ_', digit: 4, isDate: true }パターンに倣い、以下を追加:{ pattern: '36_wrk_ocr_queue', prefix: 'OCQ_', digit: 4, isDate: true } - これにより
Utils.getIdPrefixConfig('36_wrk_ocr_queue')(004_utils.jsL482-487)がOCQ_YYYYMMDD_NNNN形式の ID を発番できる。
- 既存の
Constants.SHEET_DEFAULTS(L73-87)にエントリ追加- 既存 L85
{ pattern: '32_wrk_invoice', defaults: { '請求ステータス': '未処理', '収支区分': '支出', '通貨': 'JPY', '申請種別': '請求書受領(AP)' } }のオブジェクト型({ pattern, defaults }形式。prefixは ID 発番用で必須ではない)に倣い、以下を追加:{ pattern: '36_wrk_ocr_queue', defaults: { 'ステータス': '要確認' } } - 失敗パターン #18(MAS-003)の教訓:
SHEET_DEFAULTSは 3 要素配列ではなくオブジェクト配列である。必ず Read で確認してから追加すること。
- 既存 L85
Constants.MENU_DEFINITION(L206-324)への起票メニュー追加(Step 3 で詳述)
注: 36_wrk_bank_import(既存・WRK_BANK_IMPORT スキーマ、L822)とシート番号が重複するが、36_wrk_bank_import は銀行 CSV 取込専用で既に使用中のため、本案件のシートは物理名を 36_wrk_ocr_queue とする(両方とも 36_wrk_* 系で CSV/OCR 取込系の共通カテゴリ)。Utils の getSheetByKey 経由で参照するため物理名衝突のみが問題となるが、数値の重複自体は既存の 502_ 番号重複(502_bank_importer.js と 502_receipt_reader.js)と同様に許容されている運用。
Step 2: OCR パイプライン関数の実装
対象ファイル: 500_import/502_receipt_reader.js(既存関数は一切変更せず、末尾に関数追加)
処理フロー
[Drive フォルダの PDF/画像]
│
▼
┌─────────────────────────────┐
│ (1) LockService.waitLock(30000) │ ← 排他制御。失敗時は Toast で終了
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ (2) MD5 ハッシュ計算 │
│ Utilities.computeDigest( │
│ Utilities.DigestAlgorithm│
│ .MD5, │
│ blob.getBytes() │
│ ) │
│ → 既に 36_wrk_ocr_queue に │
│ ある → スキップ │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ (3) Gemini API 呼び出し │
│ callGeminiForReceipt_() を│
│ 再利用(既存関数) │
│ N-40 分岐は自動で効く │
│ 複数ページは「最終ページの │
│ 合計金額のみ」指定 │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ (4) 正規化 │
│ Utils.normalizePartnerName()│
│ Utils.aiSuggestAccount() │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ (5) 信頼度判定 │
│ 必須: T 番号・取引先・金額・日│
│ 付・科目名 が全て存在 │
│ 金額整合: 税抜 + 税 = 税込 │
│ マスタ: 取引先名が MST_PART に│
│ ヒット │
└─────────────────────────────┘
│
┌────┴────┐
│ │
▼ ▼
[高信頼度] [低信頼度]
│ │
▼ ▼
InvoiceRepo 36_wrk_ocr_queue
.append([dto]) に要確認で登録
│ │
▼ ▼
36_wrk_ocr_queue Toast で人間に確認依頼
に起票済で記録
実装詳細
- 新規公開関数:
importReceiptPdfsAutoPosting()(メニュー起点)。importReceiptPdfs()は温存。 - LockService:
var lock = LockService.getScriptLock(); try { lock.waitLock(30000); } catch(e) { SpreadsheetApp.getActive().toast('他の処理実行中のため中断', 'AI-OCR', 5); return; }finally { lock.releaseLock(); }を必ず付ける。
- MD5 ハッシュ:
var blob = file.getBlob(); var md5 = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, blob.getBytes()).map(function(b) { return ('0' + (b & 0xFF).toString(16)).slice(-2); }).join('');- 重複判定は
36_wrk_ocr_queueのファイルMD5列を全件読み取り、一致時はUtils.logInfo('importReceiptPdfsAutoPosting', '処理済みスキップ: ' + fileName)でログ出力してスキップ。
- Gemini 呼び出し: 既存
callGeminiForReceipt_(apiKey, base64, fileName)(L194)を再利用。MAS-216 の Vertex 分岐(Env.gcpProjectId()が設定済なら自動委譲・L196-198)もそのまま効く。 - 正規化:
- 取引先名:
Utils.normalizePartnerName(extracted.vendor)(004_utils.jsL657-680)。12_mst_partnerの略称列または取引先名_正式列に一致すれば略称を返し、未ヒットならgenerateLogicalAbbr()で論理略称を生成。 - 科目名:
Utils.aiSuggestAccount(extracted.description + ' ' + extracted.vendor)(004_utils.jsL467-475)で推論。該当なしなら空文字。
- 取引先名:
- 信頼度判定:
- 「高信頼度」の条件(すべて真):
- 必須フィールド(
totalAmount/accrualDate/ 正規化後取引先名 /invoiceNumberが T + 13 桁の形式 /aiSuggestAccountが科目名を返す)がすべて充足 - 金額整合性:
Math.abs((extracted.subtotal + extracted.tax) - extracted.totalAmount) <= 1(1 円誤差まで許容) - 推論科目が
11_mst_accountに登録済み(AccountRepository.findAsMap()(202_repository.jsL323-341)で照合)
- 必須フィールド(
- 「低信頼度」:上記いずれか不充足
- 「高信頼度」の条件(すべて真):
- InvoiceDTO への設定値(Phase 1 で
000_infra/003_contracts.jsL40-67 /100_config/101_sys_config.jsL847 /000_infra/002_constants.jsL85 を裏取り確定):
| フィールド | 設定値 | 根拠 |
|---|---|---|
申請種別 | "請求書受領(AP)" | Constants.SHEET_DEFAULTS L85 で 32_wrk_invoice の 申請種別 デフォルト値として定義(DDL 格納実データと一致) |
発生日(P/L計上日) | extracted.accrualDate(YYYY-MM-DD) | Gemini プロンプト L12「請求期間の終了日(最終日)」 |
決済日_計画 | extracted.paymentDate(空なら accrualDate + 30 日) | Gemini は paymentDate として抽出 |
請求ステータス | "未処理" | SHEET_DEFAULTS 既定値 |
収支区分 | "支出" | SHEET_DEFAULTS 既定値(領収書/請求書受領なので支出固定) |
取引先名 | Utils.normalizePartnerName(vendor) 結果 | MAS-154 略称マスタ準拠 |
科目名 | Utils.aiSuggestAccount(description) 結果 | InvoiceRepository.append() が AccountRepository.findAsMap() で諸表区分・大分類を自動付与(L192-199) |
税区分 | "課税"(T 番号ありかつ金額整合 OK の場合)/"対象外"(その他) | インボイス登録事業者判定の暫定ロジック |
通貨 | "JPY" | SHEET_DEFAULTS 既定値 |
税抜金額_計画 | extracted.subtotal | |
消費税額_計画 | extracted.tax | |
税込金額_計画 | extracted.totalAmount | |
摘要 | extracted.description | |
証憑URL | 'https://drive.google.com/file/d/' + file.getId() + '/view' | 既存 importReceiptPdfs L117 と同形式 |
PJ名 | 未設定(InvoiceRepository.append() が '指定なし_共通費など' をデフォルトで付与・L200-203) |
- 起票後処理:
36_wrk_ocr_queueにステータス: 起票済/起票済INV_ID: <払い出された INV_ID>を書き込む。INV_ID はappendDtosToSheet_戻り値経由では取得できないため、InvoiceRepository.findAll()の末尾行から取り直すか、既存appendDtosToSheet_のパターン踏襲で事前に ID を採番して DTO に詰める(推奨: 既存importReceiptPdfs方式の「シート最終行から最大連番取得 → padStart で整形」を踏襲)。
Step 3: 確認キューからの手動起票メニュー追加
対象ファイル: 000_infra/002_constants.js(メニュー定義)/500_import/502_receipt_reader.js(ハンドラ関数)
メニュー追加:
Constants.MENU_DEFINITION(L206-324)の📋 サイドバー: 🔍 消込・マッチングカテゴリ(L268-280、既存の領収書 OCR 3 項目と同カテゴリ)に以下 2 項目を追加:{ label: '🤖 AI-OCR 自動起票 (Drive)', funcName: 'importReceiptPdfsAutoPosting', description: 'Drive の PDF/画像を Gemini OCR で読み取り、32_wrk_invoice に自動起票(低信頼度は 36_wrk_ocr_queue)' }, { label: '✅ 確認キューから INV 起票', funcName: 'postApprovedOcrQueueRows', description: '36_wrk_ocr_queue の選択行(ステータス=修正済)を 32_wrk_invoice に起票' },造語禁止(失敗パターン #20):
onOpen()(101_sys_config.jsL323-350)はConstants.MENU_DEFINITIONを動的ループするだけなので、メニュー実体は002_constants.js側。既存{ label: '📄 領収書PDF読込 (Drive)', funcName: 'importReceiptPdfs' }(L274)を参考に文字列を命名。ハンドラ関数
postApprovedOcrQueueRows():- 選択範囲の行番号を
SpreadsheetApp.getActiveSheet().getActiveRange()で取得。 - 選択行のシート名が
36_wrk_ocr_queueでなければエラー表示して終了。 - 選択行を
ステータス === '修正済'のみで絞り込み、各行をInvoiceDTOに詰め替えてInvoiceRepository.append(dtos)を呼ぶ。 - 成功時は当該行の
ステータスを起票済・起票済INV_IDに払い出された INV_ID を書き込み、Toast で件数通知。 - バリデーション:
税抜 + 税 ≠ 税込なら起票を拒否してエラー表示(低信頼度判定と同一基準)。
- 選択範囲の行番号を
影響範囲
| 区分 | 対象 | 変更内容 |
|---|---|---|
| ファイル変更 | 500_import/502_receipt_reader.js | 既存関数は不変。末尾に importReceiptPdfsAutoPosting / postApprovedOcrQueueRows / 内部ヘルパー 2-3 本を追加(想定 200-300 行) |
| ファイル変更 | 100_config/101_sys_config.js | setupAllSchemas() の schemas オブジェクトに WRK_OCRQ 追加(1 行)/01_sys_config 登録 appendRow 追加(1 行) |
| ファイル変更 | 000_infra/002_constants.js | ID_PREFIX_MAP / SHEET_DEFAULTS / MENU_DEFINITION に各 1 エントリ追加(計 4 行程度) |
| 新規シート | 36_wrk_ocr_queue(WRK_OCRQ) | 18 列。DDL 管理。初期ステータス 要確認 |
| 新規システムキー | 01_sys_config の WRK_OCRQ 行 | setupAllSchemas が冪等追加 |
| 既存動作への影響 | なし | 既存 importReceiptPdfs() / 35_wrk_receipt / 501_cc_importer は完全不変 |
注意事項
- 科目マスタ未登録の
aiSuggestAccount結果は自動起票禁止:InvoiceRepository.append()(L185-207)は科目名をキーにAccountRepository.findAsMap()を引いて諸表区分・大分類を自動付与する(L192-199)。未登録科目だとマップヒットせず諸表区分・大分類が空のまま書き込まれ、後続の P/L・B/S マート集計で抜け落ちる。自動起票ルートではAccountRepository.findAsMap()[科目名]が存在する場合のみ高信頼度扱いとする。未登録なら低信頼度として確認キュー送り。 36_wrk_ocr_queueは DDL 管理の新規シート。スキーマ追加後は dev で📋 サイドバー: 🔧 開発・設定 → DDL 全更新 (Full)を実行して作成する必要がある。既存 Full DDL 経由で作成されるので、手動作成は不要。Utilities.computeDigest()の引数型:Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, bytes)のbytesはbyte[]。DriveApp.getFileById(id).getBlob().getBytes()で取得する。Stringを渡すとInvalid argumentになる。LockService.getScriptLock().waitLock(30000)は 30 秒タイムアウト。取得失敗時はErrorを throw するのでtry/catchで捕捉し、Toastで通知して即 return。finallyでreleaseLock()を必ず呼ぶ(内部例外でのロックリーク防止)。申請種別の実格納値は"請求書受領(AP)"(日本語表記):CLAUDE.md は「APL_xxx で統一」としているが、実データはInvoiceDTOコメント(003_contracts.jsL46)・Constants.SHEET_DEFAULTSL85・WRK_INVCDDL L847 すべてで"請求書受領(AP)"の日本語表記となっている。本案件は実データに合わせて日本語表記で設定する(DDL 定義値と格納実データの乖離はないと判断)。CLAUDE.md のAPL_xxx記述は将来の正規化方針(別案件)と解釈。- 複数ページ PDF の合算処理:現行
RECEIPT_GEMINI_PROMPT_(L10-14)は「ページごとに 1 つの JSON オブジェクトを返す」指示だが、本案件の自動起票ルートでは最終ページ(=合計金額が記載された請求書の末尾)のみを 1 件として扱うプロンプトを別途用意する(既存プロンプト定数は一字一句変更しないので、新規パイプライン用は別定数として定義)。中間ページの小計を誤採用するリスクを排除する。 - 冪等性キー:
36_wrk_ocr_queue.ファイルMD5列を一意制約として運用。同一ファイルの再実行は MD5 一致で即スキップ(Utils.logInfo に「処理済みスキップ」出力)。35_wrk_receipt 側のファイル名による重複判定とは独立。 35_wrk_receiptへの書き込み連携は行わない:本案件のパイプラインは36_wrk_ocr_queueと32_wrk_invoiceのみを操作する。既存35_wrk_receiptはimportReceiptPdfs専用のまま維持し、二重書き込みは避ける。35_wrk_receipt経由の消込ルート(importReceiptStatement・applyReceiptSettlement)とは責務を分ける。
エッジケース
信頼度判定と処理振分けテーブル
| 条件 | 挙動 | 理由 |
|---|---|---|
インボイス番号(T 番号)が読み取れない/形式不正(T + 13 桁 以外) | 確認キューに ステータス: 要確認 で登録。T 番号_OCR 生値 列を空または OCR 生値で保持し、人間に入力を促す | 形式不正のまま起票すると税区分判定(課税/非課税)が狂う。インボイス登録事業者以外との取引は別仕訳扱いになるため自動起票禁止 |
取引先名が読み取れない/Utils.normalizePartnerName() で 12_mst_partner に未ヒット(generateLogicalAbbr フォールバック) | 確認キューに登録。取引先名_OCR 生値 列に OCR 生値を保持し、マスタ登録(16_wrk_master 申請)を人間に促す | InvoiceRepository.append() は取引先名の存在を検証しないが、後続の消込(MAS-145 銀行 CSV マッチ・MAS-146 CC マッチ)で名寄せが崩れる。MAS-154 略称マスタとの整合性を必須化 |
金額不整合(subtotal + tax ≠ totalAmount、誤差 > 1 円) | 確認キューに ステータス: 要確認(信頼度「低」)。エラー内容 列に 差額: ¥NNN を記録 | 未決済残高(自動計算) の計算が狂う。自動起票禁止 |
| 税抜・消費税・税込のいずれかがゼロまたはマイナス | 確認キューに ステータス: エラー で登録。エラー内容 列に値を記録 | 会計上の異常値。ゼロ円・マイナス起票は監査観点で手動承認を必須化 |
| 複数ページ PDF | 自動起票ルートでは新設プロンプト(「最終ページの合計金額のみを 1 件で返す」指示)を使用。既存 RECEIPT_GEMINI_PROMPT_ は改変しない | 中間ページの小計を誤採用するリスクを排除(請求書末尾に合計記載のパターンが多い) |
| OCR 精度が著しく低い(必須フィールド 3 つ以上空、または Gemini が JSON を返せない) | 処理中断し確認キューに ステータス: エラー・エラー内容: OCR 精度不足 で登録。Toast で再撮影を促す | 低精度データの自動起票は消込・マートに連鎖影響 |
| 同一ファイルの再実行(MD5 ハッシュ一致) | 即スキップ。Utils.logInfo('importReceiptPdfsAutoPosting', '処理済みスキップ: ' + fileName) でログ出力 | 二重起票防止。36_wrk_ocr_queue.ファイルMD5 を一意制約として運用 |
LockService タイムアウト(別プロセスが実行中) | try/catch で Error を捕捉し、SpreadsheetApp.getActive().toast('他の処理実行中のため中断', 'AI-OCR', 5) で通知して終了。起票・キュー登録は行わない | 排他制御の破綻を防ぐ |
aiSuggestAccount() が空文字を返す(キーワード辞書未ヒット) | 確認キューに ステータス: 要確認・科目名_推論 列は空・エラー内容: 科目推論不可 | 科目未指定だと InvoiceRepository.append() が諸表区分・大分類を付与できず、マートで分類不能 |
aiSuggestAccount() が返した科目名が 11_mst_account に未登録 | 確認キューに ステータス: 要確認・エラー内容: 推論科目がマスタ未登録 | AccountRepository.findAsMap() ヒットしないまま起票すると諸表区分・大分類が空欄でマートから抜け落ちる |
| Gemini API 4 回リトライ後も 429/503 | 既存 callGeminiForReceipt_ が throw → try/catch で捕捉し、確認キューに ステータス: エラー・エラー内容: Gemini API retry 上限 | リトライは既存ロジックに委譲、本案件は例外を握りつぶさない |
| Drive フォルダに処理対象ファイルなし | Toast で 対象ファイルなし を通知して終了 | 既存 importReceiptPdfs L55 と同挙動 |
確認キュー起票メニュー実行時、ステータス !== '修正済' の行が混在 | 修正済 行のみ処理。他ステータスはスキップし、結果ダイアログに「スキップ件数」を表示 | 誤操作防止(人間が確認済フラグを立てていない行は起票しない) |
| 確認キュー起票メニュー実行時、選択範囲に複数行/単一行/範囲未選択 | 範囲未選択は Toast で 行を選択してください → 終了。単一・複数は同じロジックで処理 | 誤操作防止 |
36_wrk_ocr_queue シートが未作成(DDL 未実行) | Utils.getSheetByKey('WRK_OCRQ', '36_wrk_ocr_queue') が null → setupAllSchemas 実行を促す Toast/アラート表示 | 事前の DDL 実行漏れ対策 |
36_wrk_ocr_queue の列構成(DDL スキーマと完全一致)
| # | 列名 | 型 | 備考 |
|---|---|---|---|
| 1 | OCQ_ID | 文字列 | OCQ_YYYYMMDD_NNNN。ID_PREFIX_MAP 経由で自動発番 |
| 2 | ステータス | 文字列 | 要確認 / 修正済 / 起票済 / エラー の 4 値。デフォルト 要確認(SHEET_DEFAULTS) |
| 3 | 元ファイル名 | 文字列 | Drive 上のファイル名 |
| 4 | ファイルMD5 | 文字列 | 冪等性キー。32 桁 16 進 |
| 5 | 信頼度スコア | 文字列 | 高 / 低 の 2 値(将来数値化の余地あり) |
| 6 | OCR生データ_JSON | 文字列 | Gemini 応答原文(デバッグ用) |
| 7 | 取引先名_OCR生値 | 文字列 | 正規化前の vendor |
| 8 | 取引先名_正規化後 | 文字列 | normalizePartnerName 結果 |
| 9 | 発生日_OCR | 文字列 | YYYY-MM-DD |
| 10 | 税抜金額_OCR | 数値 | |
| 11 | 消費税額_OCR | 数値 | |
| 12 | 税込金額_OCR | 数値 | |
| 13 | インボイス番号_OCR | 文字列 | T + 13 桁形式を検証 |
| 14 | 科目名_推論 | 文字列 | aiSuggestAccount 結果 |
| 15 | 起票済INV_ID | 文字列 | 起票成功時に INV_YYYYMMDD_NNNN を書き込み |
| 16 | エラー内容 | 文字列 | 信頼度低・エラー時の理由 |
| 17 | 起票日時 | 日時 | yyyy-MM-dd HH:mm 形式 |
| 18 | 起票者 | 文字列 | Session.getActiveUser().getEmail() |
Human-in-the-Loop フロー
- 高信頼度(条件:必須フィールド全充足 + 金額誤差 ≤ 1 円 + 科目名が
11_mst_account登録済) →InvoiceRepository.append()で自動起票 → 同時に36_wrk_ocr_queueにステータス: 起票済・起票済INV_IDを書き込み(監査ログ兼用)。 - 低信頼度(上記以外) →
36_wrk_ocr_queueにステータス: 要確認・エラー内容: <具体的な理由>で登録 → Toast で件数通知。 - 人間が確認キューを修正 → 誤読箇所をシート上で直接編集 →
ステータスを修正済に手動変更。 - 確認キュー起票メニュー実行 → Step 3 の
postApprovedOcrQueueRows()が選択範囲のうち修正済行のみをInvoiceRepository.append()経由で起票 →ステータス: 起票済・起票済INV_IDを更新。 - エラー行の運用 →
ステータス: エラーの行はデータ修正後に修正済に変更 → 4 へ。修正不能なら手動で有効フラグ=FALSE相当(本シートではステータス: エラー固定)で運用。
実データ検証
実装前に以下を MCP または GAS ログで確認すること(失敗パターン #18-#20・DDL 定義値 vs 実データ乖離対策):
12_mst_partnerシート:略称/取引先名_正式/有効フラグ列の列名がUtils.normalizePartnerName()(004_utils.jsL665-676)で参照する'略称'/'取引先名_正式'/'有効フラグ'と完全一致するか確認。DDL L828 には略称_4文字/取引先名_正式/略称/銀行摘要名/UI用取引先名の複数略称列が存在するため、normalizePartnerNameが参照するのは略称(略称_4文字ではない)点を確認する。
11_mst_accountシート:Utils.aiSuggestAccount()が返す全account値(Constants.ACCOUNT_RULES:002_constants.jsL117-136)が11_mst_account.科目名に登録されているか確認。対象:新聞図書費/旅費交通費/交際費/通信費/支払手数料/消耗品費/地代家賃/水道光熱費/売上高/売上原価/租税公課/資本金/長期借入金/現金及び預金/預り金/法定福利費/給料手当/役員報酬。未登録があれば事前にD-01〜D-03 科目マスタ追加マイグレーションメニュー(migrationD01D03)で追加する。- 各科目の
諸表区分/大分類/有効フラグを確認(AccountRepository.findAsMap()が有効フラグ=FALSEをスキップ・202_repository.jsL329-330)。
32_wrk_invoiceシート:- 既存データで
申請種別列に格納されている実値を確認。"請求書受領(AP)"が実データと DDL 定義(SHEET_DEFAULTSL85)の両方で一致していることを目視確認。他の申請種別値(経費申請(EX)等)が混在している場合、本案件の AI-OCR は「請求書受領(AP)」固定で起票する方針を確定させる。
- 既存データで
01_sys_configシート:- 既存の
WRK_ORDR/WRK_INVC/WRK_BANK/WRK_RCPT/WRK_BANK_IMPORT等の登録行を確認し、WRK_OCRQ追加時の列構成(論理キー / シート ID / シート名 / 説明)を他エントリと揃える。
- 既存の
- Gemini API キー / GCP プロジェクト ID:
- dev 環境で
Env.geminiApiKey()とEnv.gcpProjectId()が設定済みかを確認。未設定時の挙動(MAS-216 並行稼働でgcpProjectId()空なら AI Studio 直叩きにフォールバック)が自動起票でも期待どおりか動作検証する。
- dev 環境で
関連ドキュメント
| 仕様書・定義 | 関連箇所 |
|---|---|
docs/dev/dev_mas-147_invoice_ocr_auto_posting.md | 請求書 PDF → INV 自動起票 v2(Document AI ハイブリッド)。本案件は領収書/請求書共通の簡易版。重複する設計観点(T 番号正規化・取引先照合・HitL)を参照 |
docs/dev/dev_mas-157_photo_ocr.md | 写真(JPEG/PNG)対応 OCR。本案件は画像対応後も同パイプラインで動作する想定 |
docs/dev/dev_mas-161_ocr_manual_correction_assist.md | OCR 結果の手動補正アシスト。本案件の確認キュー UI 設計と親和 |
docs/dev/dev_mas-216_vertex_ai_migration.md | Vertex AI 並行稼働。本案件の callGeminiForReceipt_ 呼び出しが MAS-216 で自動切替される前提 |
docs/dev/dev_mas-154_partner_logical_abbr.md | 取引先略称自動生成。Utils.normalizePartnerName の実装根拠 |
000_infra/003_contracts.js L40-67 | InvoiceDTO フィールド定義 |
000_infra/004_utils.js L467-475 / L657-680 | aiSuggestAccount / normalizePartnerName |
200_data/202_repository.js L149-207 / L304-341 | InvoiceRepository.append / AccountRepository.findAsMap の科目マスタ自動付与ロジック |
100_config/101_sys_config.js L820-823 / L826-901 | 01_sys_config 登録パターン / schemas 定義パターン |
000_infra/002_constants.js L73-87 / L93-112 / L206-324 | SHEET_DEFAULTS / ID_PREFIX_MAP / MENU_DEFINITION |
docs/_internal/failure_patterns.md #18-#20 | コード未読による固有名詞誤記の防止(MAS-003 起因) |
人間が検討すべき事項
TODO_future.md MAS-180 行(L437)から転記:
- Gemini API の利用料・レート制限。自動起票では OCR 呼出回数が増えるため上限超過リスクあり。MAS-216 で Vertex 移行時は別コスト体系。
- プロンプトチューニングの工数。本案件の「最終ページの合計金額のみ返す」プロンプトと既存「ページごと 1 JSON」プロンプトの 2 系統併存による保守負荷。
本調査で追加判明した事項:
- 確認キュー
36_wrk_ocr_queueのレビュー UI 設計:シート直接編集(本案件のデフォルト)/サイドバー UI(MAS-161 連携)/Google Form の 3 案。シート直接編集は最小工数だが、T 番号・金額のバリデーションを onEdit で組み込むか検討要(MAS-128 承認ワークフロー参照)。 - 証憑ファイルの保存先フォルダ管理:
Env.receiptFolderId()(001_env.js)の運用方針。自動起票は既存フォルダと統合か、専用フォルダを分離するか。MAS-152(電帳法リネーム)と統合時のフォルダ構造(processed/YYYY-MM/型)との整合。 - インボイス番号(T 番号)の連携:
32_wrk_invoice.税区分は現状"課税"/"非課税"/"対象外"の 3 値だが、T 番号の有無で「適格請求書」判定を42_trn_journal.税区分コードに連携するか。MAS-114(インボイス検証)との連携スコープ。 - 処理済みハッシュの永続化先:本案件は
36_wrk_ocr_queue.ファイルMD5列を唯一ソースとするが、シートが肥大化するため03_sys_paramsor99_cache_*シートへのキャッシュ化も将来検討。MAS-133(年度跨ぎアーカイブ)との整合。 - 自動起票ルートと既存
35_wrk_receiptルートの責務分離:importReceiptPdfsは35_wrk_receipt経由の消込ルート、本案件は32_wrk_invoice経由の INV 起票ルート。両方が同一 Drive フォルダを処理する場合の重複実行制御(MD5 の相互参照要否)は、将来パイプライン統合時に判断。 申請種別のAPL_xxx正規化案件との切り分け:CLAUDE.md はAPL_xxxコード統一を将来方針として記述しているが、本案件は現行実データ"請求書受領(AP)"に合わせる。別案件としてAPL_xxxマイグレーションが立ち上がる場合、本案件の固定値もその時点で移行する。- MAS-203(AI プロンプトライブラリの版管理)との連携:本案件で追加する「最終ページ合計のみ返せ」プロンプトを
RECEIPT_GEMINI_PROMPT_とは別定数とするが、MAS-203 実装時には外部管理(シート ordocs/prompts/)に移行する前提で、ファイル内定数として命名規則RECEIPT_AUTO_POSTING_GEMINI_PROMPT_V1_等で版管理の余地を残す。 - 低信頼度キューの自動レビュー強化:現状は単純な必須フィールド充足+金額整合+マスタ照合の 3 条件だが、将来的には Gemini の
confidenceスコア取得(プロンプト側で要求)や Document AI 併用(MAS-147 v2 参照)への拡張余地。
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-180「AI-OCR連携による証憑自動起票」を以下の Step に分けて実装してください。
## 実行前タスク
- `500_import/502_receipt_reader.js` L22-185 の `importReceiptPdfs()`、L194-269 の `callGeminiForReceipt_()`、L278-323 の `parseGeminiReceiptText_()`、L416-521 の `callGeminiForReceiptOnVertex_()` を Read で確認(既存関数は変更しない)
- `100_config/101_sys_config.js` L323-350 の `onOpen()`(`Constants.MENU_DEFINITION` を動的ループする構造)、L820-823 の `01_sys_config` `appendRow` 登録パターン、L826-901 の `schemas` オブジェクト定義パターンを Read で確認(メニュー実体は `000_infra/002_constants.js` 側)
- `000_infra/002_constants.js` L73-87 の `SHEET_DEFAULTS`(オブジェクト配列 `{ pattern, prefix?, defaults, _dynamic? }`)、L93-112 の `ID_PREFIX_MAP`(`{ pattern, prefix, digit, isDate }`)、L206-324 の `MENU_DEFINITION`(特に L268-280 の「📋 サイドバー: 🔍 消込・マッチング」カテゴリ)を Read で確認してからエントリを追加する(失敗パターン #18・#20 参照)
- `000_infra/003_contracts.js` L40-67 の `InvoiceDTO` 全フィールド定義を Read で確認
- `200_data/202_repository.js` L149-207 の `InvoiceRepository.append()` 内部処理(`AccountRepository.findAsMap()` で科目マスタから `諸表区分`・`大分類` を自動付与、`PJ名` 未指定時に `'指定なし_共通費など'` をデフォルト)、L304-341 の `AccountRepository.findAsMap()` を Read で確認
- `000_infra/004_utils.js` L467-475 の `aiSuggestAccount(text)`、L657-680 の `normalizePartnerName(rawName)`、L482-487 の `getIdPrefixConfig(sheetName)` を Read で確認
- `32_wrk_invoice` の `申請種別` 列の実データ値を GAS ログ(または MCP で実シート値)で確認し、`"請求書受領(AP)"` が実データでも DDL 定義でも一致していることを確定させる
## 修正対象ファイル
- `500_import/502_receipt_reader.js`(末尾に関数追加のみ。既存関数は一切変更しない)
- `100_config/101_sys_config.js`(`setupAllSchemas()` 内の `schemas` オブジェクトに `WRK_OCRQ` キーを追加、同関数内の `01_sys_config` 登録ブロックに `appendRow` を追加。それ以外は変更しない)
- `000_infra/002_constants.js`(`SHEET_DEFAULTS` / `ID_PREFIX_MAP` / `MENU_DEFINITION` の 3 配列への追記のみ)
## Step 1: 36_wrk_ocr_queue シートの新設
- `101_sys_config.js` L820-823 パターンに倣い `01_sys_config` へ `WRK_OCRQ` システムキーを登録: `if (!existKeys.includes('WRK_OCRQ')) confSheet.appendRow(['WRK_OCRQ', '', '36_wrk_ocr_queue', 'AI-OCR確認キュー']);`
- `101_sys_config.js` L826-901 の `schemas` オブジェクトに `'WRK_OCRQ': { headers: [...], color: "#38761d" }` を追加。列構成は仕様書「`36_wrk_ocr_queue` の列構成」に従う(18 列:OCQ_ID / ステータス / 元ファイル名 / ファイルMD5 / 信頼度スコア / OCR生データ_JSON / 取引先名_OCR生値 / 取引先名_正規化後 / 発生日_OCR / 税抜金額_OCR / 消費税額_OCR / 税込金額_OCR / インボイス番号_OCR / 科目名_推論 / 起票済INV_ID / エラー内容 / 起票日時 / 起票者)
- `002_constants.js` L93-112 の `ID_PREFIX_MAP` に `{ pattern: '36_wrk_ocr_queue', prefix: 'OCQ_', digit: 4, isDate: true }` を追加
- `002_constants.js` L73-87 の `SHEET_DEFAULTS` に `{ pattern: '36_wrk_ocr_queue', defaults: { 'ステータス': '要確認' } }` を追加(L85 `32_wrk_invoice` エントリと同じ `{ pattern, defaults }` オブジェクト型)
- DDL を dev で実行して `36_wrk_ocr_queue` シートが 18 列で作成されることを確認
## Step 2: OCR パイプライン関数の実装
- `502_receipt_reader.js` の末尾に以下の関数を追加(既存関数は一切変更しない):
- `importReceiptPdfsAutoPosting()` — メニュー起点
- 内部ヘルパー: `computeFileMd5_(blob)` / `isOcrQueueDuplicate_(md5)` / `evaluateConfidence_(extracted, normalizedVendor, suggestedAcct, accountMap)` / `buildInvoiceDtoFromOcr_(extracted, normalizedVendor, suggestedAcct, driveLink)` / `appendToOcrQueue_(sheet, ocqId, row)`
- `LockService.getScriptLock().waitLock(30000)` で排他制御。`try/catch` で捕捉し `SpreadsheetApp.getActive().toast('他の処理実行中のため中断', 'AI-OCR', 5)` で終了。`finally { lock.releaseLock(); }` を必ず付ける
- MD5 計算: `Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, blob.getBytes())` の戻り値 `byte[]` を 16 進文字列化(既存 `35_wrk_receipt` 処理にはない新規ヘルパー)
- 重複判定: `36_wrk_ocr_queue.ファイルMD5` 列の全件読み取り → 一致時は `Utils.logInfo('importReceiptPdfsAutoPosting', '処理済みスキップ: ' + fileName)` でログしてスキップ
- Gemini 呼び出し: 既存 `callGeminiForReceipt_(apiKey, base64, fileName)` を再利用(MAS-216 の Vertex 分岐が自動で効く)。ただし本自動起票ルートでは「最終ページの合計金額のみを 1 件で返す」新規プロンプト(例: `RECEIPT_AUTO_POSTING_GEMINI_PROMPT_V1_`)を別定数として定義し、既存 `callGeminiForReceipt_` を呼び出す前に引数で差し替える形にはせず、**新規内部関数 `callGeminiForAutoPosting_(apiKey, base64, fileName)` を別途実装**する(既存関数の一字一句不変を担保)。実装は `callGeminiForReceipt_` を参考に、プロンプト部分のみ差し替え・Backoff ロジックは同一ロジックで複製
- 正規化: `Utils.normalizePartnerName(extracted.vendor)` / `Utils.aiSuggestAccount((extracted.description || '') + ' ' + (extracted.vendor || ''))`
- 信頼度判定「高」: (1) `totalAmount > 0` かつ `subtotal > 0` かつ `tax >= 0`、(2) `accrualDate` が YYYY-MM-DD 形式、(3) `invoiceNumber` が `/^T\d{13}$/` に一致、(4) `normalizedVendor` が空でない、(5) `suggestedAcct` が空でなくかつ `AccountRepository.findAsMap()[suggestedAcct]` が存在、(6) `Math.abs((subtotal + tax) - totalAmount) <= 1`
- 高信頼度 → `buildInvoiceDtoFromOcr_` で `InvoiceDTO` を構築し `InvoiceRepository.append([dto])` で起票。同時に `36_wrk_ocr_queue` へ `ステータス: 起票済`・`起票済INV_ID: <新 INV_ID>` で記録
- 低信頼度 → `36_wrk_ocr_queue` へ `ステータス: 要確認`・`エラー内容: <具体理由>` で登録
- `InvoiceDTO` の `申請種別` は文字列リテラル `"請求書受領(AP)"` を直接指定する(`SHEET_DEFAULTS` のデフォルトに依存しない。明示指定で将来のデフォルト変更にも堅牢)
## Step 3: 確認キューからの手動起票メニュー追加
- `002_constants.js` L268-280 の「📋 サイドバー: 🔍 消込・マッチング」カテゴリの `items` 配列末尾(既存 `importReceiptPdfs` / `importReceiptStatement` / `applyReceiptSettlement` の後)に以下 2 項目を追加:
- `{ label: '🤖 AI-OCR 自動起票 (Drive)', funcName: 'importReceiptPdfsAutoPosting', description: 'Drive の PDF/画像を Gemini OCR で読み取り、32_wrk_invoice に自動起票(低信頼度は 36_wrk_ocr_queue)' }`
- `{ label: '✅ 確認キューから INV 起票', funcName: 'postApprovedOcrQueueRows', description: '36_wrk_ocr_queue の選択行(ステータス=修正済)を 32_wrk_invoice に起票' }`
- `502_receipt_reader.js` の末尾に `postApprovedOcrQueueRows()` を追加:
- `SpreadsheetApp.getActiveSheet().getName()` が `36_wrk_ocr_queue` でなければ `ui.alert('シート違い', '36_wrk_ocr_queue シートで実行してください', ...)` で終了
- `SpreadsheetApp.getActiveRange()` の行範囲を取得し、`ステータス === '修正済'` のみを対象化
- 各行から `InvoiceDTO` を構築し、金額整合性(`税抜 + 税 = 税込`)を再検証。不整合は起票拒否しエラー表示
- `InvoiceRepository.append(dtos)` で起票 → 当該行の `ステータス` を `起票済`・`起票済INV_ID` に新 INV_ID を書き込み
- Toast で件数(起票成功・スキップ・エラーの内訳)通知
## 制約
- 既存の `importReceiptPdfs` / `callGeminiForReceipt_` / `parseGeminiReceiptText_` / `postProcessReceiptData_` / `callGeminiForReceiptOnVertex_` は**一字一句変更しない**(MAS-216 並行稼働の既存挙動を守るため)
- `RECEIPT_GEMINI_PROMPT_` 定数(L10-14)は変更禁止。本案件用のプロンプトは別定数 `RECEIPT_AUTO_POSTING_GEMINI_PROMPT_V1_` として追加
- `501_cc_importer.js` / `502_bank_importer.js` / `504_invoice_importer.js` は変更対象外
- 科目名には必ず `AccountRepository.findAsMap()[name]` が存在するものだけを渡す。未登録科目の自動起票は禁止(低信頼度で確認キュー送り)
- `SHEET_DEFAULTS` と `ID_PREFIX_MAP` の型は必ず Read で確認してから追加する(#18 失敗パターン参照)
- `MENU_DEFINITION` に追加するメニュー文字列・カテゴリ名は既存エントリ(L274 `'📄 領収書PDF読込 (Drive)'` 等)の表記を参照し造語しない(#20 失敗パターン参照)
- `onOpen()` 本体(`101_sys_config.js` L323-350)は変更しない。メニュー実体の更新は `MENU_DEFINITION` 側のみ
## エッジケース
仕様書「エッジケース」セクションのテーブルに従って実装すること。特に以下を必ず実装する:
- 金額不整合(`|税抜 + 税 - 税込| > 1`)→ 自動起票せず `36_wrk_ocr_queue` に `要確認` で登録
- 同一ファイル MD5 一致 → 即スキップ + ログ出力
- `LockService.waitLock` タイムアウト → Toast 通知して終了・キュー登録なし
- T 番号未検出(`/^T\d{13}$/` 不一致)→ 低信頼度扱い
- 科目推論結果が `11_mst_account` 未登録 → 低信頼度扱い
- `36_wrk_ocr_queue` シート未作成 → `getSheetByKey` が null → `setupAllSchemas` 実行を促すアラート
## 実データ検証
実装前に仕様書「実データ検証」セクションの 5 項目を MCP または GAS ログで全て確認すること。
特に以下 2 点は自動起票の失敗を未然に防ぐため必須:
- `Utils.aiSuggestAccount` が返す全 18 科目(`Constants.ACCOUNT_RULES` L117-136)が `11_mst_account` に登録済みか(未登録があれば `migrationD01D03` で追加)
- `32_wrk_invoice.申請種別` 列に `"請求書受領(AP)"` の実データが存在し、DDL 定義と一致していること
## 動作確認
1. `npm run push:dev` でデプロイ
2. `📋 サイドバー: 🔧 開発・設定 → DDL 全更新 (Full)` を実行し `36_wrk_ocr_queue` が 18 列で作成されることを確認
3. `01_sys_config` に `WRK_OCRQ` 行が追加されていることを確認
4. dev の `Env.receiptFolderId()` 配下にテスト証憑(以下 4 パターン)を投入:
- A: 鮮明な請求書(T 番号あり・金額整合・科目推論可能)
- B: 不鮮明な請求書(T 番号なし)
- C: 金額不整合(`subtotal + tax ≠ totalAmount`)
- D: A と同一ファイル(再実行による MD5 一致テスト)
5. `🤖 AI-OCR 自動起票 (Drive)` メニューを実行
6. 期待結果:
- A: `32_wrk_invoice` に 1 行追加(`諸表区分`・`大分類` が科目マスタから自動付与、`申請種別=請求書受領(AP)`)、`36_wrk_ocr_queue` に `ステータス: 起票済`・`起票済INV_ID` が記録
- B: `36_wrk_ocr_queue` に `ステータス: 要確認`・`エラー内容: T 番号形式不正`、`32_wrk_invoice` 無変化
- C: `36_wrk_ocr_queue` に `ステータス: 要確認`・`エラー内容: 金額不整合`、`32_wrk_invoice` 無変化
- D: ログに `処理済みスキップ: <fileName>`、`36_wrk_ocr_queue` 行追加なし
7. B の行の誤読項目を手動修正 → `ステータス` を `修正済` に変更 → 該当行を選択
8. `✅ 確認キューから INV 起票` メニューを実行 → `32_wrk_invoice` に 1 行追加、`36_wrk_ocr_queue` の `ステータス: 起票済`・`起票済INV_ID` が更新されることを確認
9. `LockService` 動作確認: 2 タブで同時にメニュー実行 → 後発側が `他の処理実行中のため中断` の Toast で終了することを確認
10. `901_test_runner.js` に MAS-180 の結合テストケース(高信頼度自動起票・低信頼度キュー登録・MD5 重複スキップ・確認キュー起票)を追加して実行
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(調査・設計) | あり | ファイル名(`502_receipt_reader.js` と `502_bank_importer.js` の重複解消)・`申請種別` 実データ値確認・`36_wrk_ocr_queue` 列構成確定に使用 |
| Phase 2(実装) | 最小限 | Phase 1 で確定した内容の実装に徹する |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
Step 1: WRK_OCRQ シート新設 + 定数追加 | Claude Haiku 4.5 | SHEET_DEFAULTS / ID_PREFIX_MAP / DDL schemas のパターン追加のみ。既存 WRK_CARD / WRK_RCPT エントリを参考に機械的に挿入できる |
| Step 2: OCR パイプライン関数実装 | Claude Opus 4.6 | Gemini API 呼び出し(新規プロンプト定数定義+Backoff ロジック複製)・信頼度判定ロジック(5 条件の AND)・InvoiceDTO 構築・LockService 排他制御・Utilities.computeDigest の byte→hex 変換など、複数ファイル横断の設計判断と HitL フローの整合性確認が必要 |
| Step 3: メニュー追加 + 確認キュー起票ハンドラ | Claude Sonnet 4.6 | MENU_DEFINITION への追記は機械的だが、postApprovedOcrQueueRows は選択範囲処理・ステータスフィルタ・金額再検証・ステータス更新と複数の判断要素あり |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-22 | 初版作成 |
仕様書作成プロンプト(再現性・監査性のため必ず記録)
展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計・調査)では拡張思考をフル活用し、ファイル名・行番号・固有名詞(関数名/シート名/列名)を完全に確定させる。Phase 2(清書)では各 Step 内の拡張思考を最小限に抑え、Phase 1 で確定した内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: Step 2-1(骨格 ~20行) / 2-2(概要〜注意事項 ~300行) / 2-3a(エッジケース〜人間検討事項 ~200行) / 2-3b(実装プロンプト〜変更履歴 ~250行) / 2-4(<details>プロンプト全文記録) に分割。1 回の Write/Edit は 300 行以内を目安にする。
4. **各 Step で何を書くかを具体指示**: 設計判断は Phase 1 で完全に確定させ、Phase 2 では判断を持ち込まない。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 N-04「AI-OCR連携による証憑自動起票」の開発仕様書を作成してください。
作成完了後は `docs/_config.json` の `nav` 配列の適切なセクションに必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
**Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で裏取りすること(失敗パターン #18-#20 の直接対策)。推測した瞬間に手を止めて Read する。**
### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` を検索し、`MAS-180` の「案件名」「概要」「期待される効果」「人間が検討すべき事項」を取得する。
### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` を Read し、コーディング規約・ファイル番号体系・マイグレーション運用ガイドライン(800_ops/ への配置ルール、冪等性要件)・`申請種別コード(APL_xxx)`の仕様を把握する。
### 1-C: 既存仕様書テンプレートの読み込み
- `docs/_internal/dev_spec_prompt_template.md` の「Phase 2: 仕様書の作成」セクション構成と「実装プロンプトのフォーマット」を Read し、出力する仕様書がこの構成に完全準拠するようにする。
- `docs/dev/` 配下から外部連携・インポート系の既存仕様書を 1 件 Grep で探して Read し、フォーマットを把握する。
### 1-D: 関連コードの調査(以下を全て Read/Bash で裏取りすること)
**① 既存 OCR ファイルの特定(ファイル名確定が最優先)**
```bash
ls 500_import/
を実行し、実在するファイル名を確認する。CLAUDE.md のファイル番号体系には 501_cc_importer・502_bank_importer・502_receipt_reader と記載されているが、502 番が重複しており実際のファイル名(502_receipt_reader.js か 503_receipt_reader.js か)は不明。Bash の出力結果を以降の記述の唯一の根拠とする。
確認後、当該ファイルを Read して以下を把握する:
- 主要な公開関数名と処理範囲
- Gemini API の呼び出しパターン(モデル名・プロンプト構造)
- Google Drive との連携方法(
Env.receiptFolderId()の使用有無)
② 再利用ユーティリティの確認
000_infra/004_utils.js を Read し以下を確認する:
Utils.normalizePartnerName(rawName)の引数・戻り値・内部で参照するシート名('12_mst_partner'またはUtils.getSheetNameByKey('MST_PART'))Utils.aiSuggestAccount(text)の引数・戻り値・Constants.ACCOUNT_RULESとの関係
※ GAS ビルトイン Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, bytes) を重複チェックのファイルハッシュ計算に使用する(blob.getBytes() を渡す)。
③ InvoiceDTO とリポジトリの確認
000_infra/003_contracts.jsを Read し、InvoiceDTOの全フィールドを把握する。OCR から設定が必要な必須フィールド(申請種別・発生日(P/L計上日)・収支区分・取引先名・科目名・税区分・通貨・税抜金額_計画・消費税額_計画・税込金額_計画・請求ステータス)を記録する。200_data/202_repository.jsを Read し、InvoiceRepository.append()の内部処理(科目マスタからの諸表区分・大分類自動付与ロジック、AccountRepository.findAsMap()の呼び出し)を確認する。申請種別に設定すべき値を確認する。CLAUDE.mdには「APL_xxx で統一」とあるが、InvoiceDTOの定義では"請求書受領(AP)"等の日本語表記が示されている。32_wrk_invoiceの実データまたは DDL 定義を MCP/GAS で確認し、実際に格納されている値を特定すること(DDL 定義値 vs 実データの乖離チェック)。
④ 新規シート設計の前提確認
100_config/101_sys_config.jsを Read し以下を確認する:setupAllSchemas(DDL)への新規シートスキーマ追加パターン(引数の型・フィールド構造)onOpen()の実在するメニュー名・メニュー構造の文字列(造語禁止)
000_infra/002_constants.jsを Read し以下を確認する:Constants.SHEET_DEFAULTSの各エントリの型({ pattern, prefix, defaults, _dynamic }のうち何が必須か)Constants.ID_PREFIX_MAPの各エントリの型({ pattern, prefix, digit, isDate }で確定)- 新規
36_wrk_ocr_queueに追加するエントリの形式を確定させる
Constants.CONFIG_SHEET(='01_sys_config')へのシステムキー登録パターンを101_sys_config.jsのsetupAllSchemas内で確認し、36_wrk_ocr_queueに割り当てるキー名(例:WRK_OCRQ)を決定する。
⑤ 既存テストの確認
900_test/901_test_runner.js を Grep し、receipt または ocr に関連するテストケースの有無を確認する。
Phase 2: 仕様書の分割作成
出力先: docs/dev/dev_mas-180_ai_ocr_enhancement.md
(※ ISSUE_ID は大文字 MAS-180 を使用)
1 回のツール呼び出しで全内容を出力しない。以下の Step に厳密に分割して実行すること。
Step 2-1: 骨格の作成(Write, ~20行)
dev_spec_prompt_template.md に定義された全セクション見出しのみを持つファイルを Write で作成する(本文空でよい)。セクション順序は以下を厳守:
# MAS-180: AI-OCR連携による証憑自動起票
## 概要
## 目的
## 現在のコード
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト(再現性・監査性のため必ず記録)
Step 2-2: 前半セクションの追記(Edit または Bash heredoc, ~300行)
「概要」〜「注意事項」を記述する。Phase 1 で確定した実ファイル名・関数名・行番号のみを使用すること(推測禁止)。
含める内容:
概要テーブル: 案件ID=N-04, カテゴリ=外部連携/AI, Phase・優先度・所要時間は
TODO_future.mdから転記, 対象ファイル=500_import/[Phase 1 で確定した実ファイル名]と100_config/101_sys_config.jsと000_infra/002_constants.js目的: 証憑画像から Gemini API で請求情報を自動抽出し、
InvoiceRepository.append()経由で32_wrk_invoiceへ起票するパイプラインを構築する。低信頼度の OCR 結果は新設する36_wrk_ocr_queueシートで人間が確認・修正してから起票する Human-in-the-Loop 設計とする。現在のコード:
500_import/[実ファイル名]の主要関数・行番号・処理範囲を Read 結果から正確に転記する修正方針: 以下の 3 Step に分割して記述する:
Step 1:
36_wrk_ocr_queueシートの新設100_config/101_sys_config.jsのsetupAllSchemasにスキーマを追加(列構成は Step 2-3a で定義した列を使用)000_infra/002_constants.jsのConstants.ID_PREFIX_MAPに{ pattern: '36_wrk_ocr_queue', prefix: 'OCQ_', digit: 4, isDate: true }を追加Constants.SHEET_DEFAULTSに{ pattern: '36_wrk_ocr_queue', defaults: { 'ステータス': '要確認' } }を追加(Phase 1 で確認した実際の型に従う)01_sys_configシートへシステムキーWRK_OCRQ(または Phase 1 で決定したキー名)を登録し、Utils.getSheetByKey('WRK_OCRQ', '36_wrk_ocr_queue')で参照できるようにする
Step 2: OCR パイプライン関数の実装
500_import/[実ファイル名]に関数を追加(既存関数は変更しない)- 処理フロー(仕様書に図示すること):
- Drive からファイル取得 →
Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, blob.getBytes())でハッシュ計算 → 処理済みなら即リターン LockService.getScriptLock()で排他ロック取得(取得失敗時は Toast で通知して終了)- Gemini API 呼び出し(複数ページ PDF は「合計金額のみを返せ」とプロンプト指定)
- 結果パース →
Utils.normalizePartnerName(rawVendorName)で取引先正規化 →Utils.aiSuggestAccount(description)で科目推論 - 信頼度判定(必須フィールド充足度 + 金額整合性チェック)
- 高信頼度 →
InvoiceRepository.append([invoiceDto])で自動起票 →36_wrk_ocr_queueにステータス: 起票済で記録 - 低信頼度 →
36_wrk_ocr_queueにステータス: 要確認で記録 → Toast で確認を促す
- Drive からファイル取得 →
InvoiceDTOの各フィールドへの設定値を仕様書に明記する。特に申請種別は Phase 1 で確認した実際の格納値を使用する(DDL 定義値 vs 実データの乖離に注意)
Step 3: 確認キューからの手動起票メニュー追加
100_config/101_sys_config.jsのonOpen()に確認キュー起票メニューを追加(Phase 1 で Read した実在するメニュー構造・文字列に倣う。メニュー名を造語しない)- 確認キューの選択行を
InvoiceRepository.append()で起票し、ステータスを起票済・起票済INV_IDに INV_ID を書き込む関数を追加
影響範囲: 変更ファイルリスト(
500_import/[実ファイル名]・100_config/101_sys_config.js・000_infra/002_constants.js)、新規シート36_wrk_ocr_queue、01_sys_configへのシステムキー追加注意事項:
InvoiceRepository.append()はAccountRepository.findAsMap()で科目マスタを参照するため、科目名に11_mst_account未登録の値を渡すと諸表区分が空になる。aiSuggestAccount()の返す科目名がマスタに登録されているかを実装前に検証すること(→ 実データ検証セクション)36_wrk_ocr_queueは DDL(setupAllSchemas)管理の新規シート。スキーマ変更時は dev で全シートのスキーマとUIを最新化(DDL)を実行してから動作確認することUtilities.computeDigest()はbyte[]を受け取る。DriveApp.getFileById(id).getBlob().getBytes()で渡すことLockService.getScriptLock()は取得タイムアウト(waitLock(30000)等)を設定し、タイムアウト時は例外を catch して Toast 通知してから終了すること申請種別の値は Phase 1 で確認した実際の格納値を使用すること。CLAUDE.md記載のAPL_xxxコードとInvoiceDTO定義の日本語表記が乖離している可能性があるため、実データを最優先とする
Step 2-3a: エッジケース〜人間が検討すべき事項の追記(Edit または Bash, ~200行)
エッジケーステーブル(必須。以下の観点を全て網羅すること):
| 条件 | 挙動 | 理由 |
|---|---|---|
| インボイス番号(T番号)が読み取れない / 形式不正(T + 13桁以外) | 確認キューに登録(ステータス: 要確認)。T番号フィールドを空で保持し、人間に入力を促す | 形式不正のまま起票すると税区分判定処理に影響する |
取引先名が読み取れない / normalizePartnerName() で 12_mst_partner に未ヒット | 確認キューに登録。取引先名_OCR生値 列に OCR 生値を保持し、マスタ登録を人間に促す | InvoiceRepository.append() は取引先名を検証しないが、後続の消込・マート処理で名寄せが崩れる |
| 金額不整合(税抜金額 + 消費税額 ≠ 税込金額) | 確認キューに登録(信頼度「低」)。エラー内容列に差額を記録 | 未決済残高 の計算が狂う。自動起票禁止 |
| 金額がゼロ / マイナス値 | 確認キューに登録(ステータス: エラー)。エラー内容列に値を記録 | 会計上の異常値。ゼロ円・マイナス起票は手動承認を必須とする |
| 複数ページ PDF | Gemini API プロンプトで「最終ページの合計金額のみを返すこと」と明示指定する | 中間ページの小計を誤採用するリスクを排除 |
| OCR 精度が著しく低い(必須フィールド 3 つ以上が空 / Gemini が値を返せない) | 処理中断し確認キューに ステータス: エラー で登録。Toast で再撮影を促す | 低精度データの自動起票は消込・マート処理全体に悪影響 |
| 同一ファイルの再実行(MD5 ハッシュ一致) | 即スキップ。Utils.logInfo() に「処理済みスキップ: {ファイル名}」を出力 | 二重起票防止 |
LockService タイムアウト(別プロセスが実行中) | 例外を catch し Toast で通知して終了。起票・キュー登録は行わない | 排他制御の破綻を防ぐ |
36_wrk_ocr_queue シートの列構成(DDL スキーマと完全一致させること):
OCQ_ID / ステータス(要確認/修正済/起票済/エラー)/ 元ファイル名 / ファイルMD5 / 信頼度スコア / OCR生データ_JSON / 取引先名_OCR生値 / 取引先名_正規化後 / 発生日_OCR / 税抜金額_OCR / 消費税額_OCR / 税込金額_OCR / インボイス番号_OCR / 科目名_推論 / 起票済INV_ID / エラー内容 / 起票日時 / 起票者
Human-in-the-Loop フロー(信頼度判定基準を定義すること):
- 高信頼度の条件(例: 必須フィールド全充足 + 金額整合性 OK)→
InvoiceRepository.append()で自動起票 - 低信頼度の条件(上記以外)→
36_wrk_ocr_queueに要確認で登録 - 人間が確認キューを修正 → Step 3 の「確認キューから起票」メニューを実行 →
InvoiceRepository.append()で起票 →ステータス: 起票済に更新
実データ検証(実装前に MCP または GAS ログで確認すべき項目):
12_mst_partnerシートの「略称」「取引先名_正式」「有効フラグ」列がnormalizePartnerName()の内部参照列名と一致するか確認11_mst_accountシートにUtils.aiSuggestAccount()が返す全科目名(新聞図書費・旅費交通費・交際費等Constants.ACCOUNT_RULESの全account値)が登録されているか確認32_wrk_invoiceの申請種別列に既存データで使われている実際の値("請求書受領(AP)"等)を確認し、DDL 定義値との乖離チェック
関連ドキュメント:
| 仕様書・定義 | 関連箇所 |
|---|---|
000_infra/003_contracts.js | InvoiceDTO フィールド定義 |
000_infra/004_utils.js | normalizePartnerName() / aiSuggestAccount() |
200_data/202_repository.js | InvoiceRepository.append() の科目マスタ自動付与ロジック |
docs/_internal/failure_patterns.md #18-#20 | コード未読による固有名詞誤記の防止 |
人間が検討すべき事項(TODO_future.md の N-04 から転記 + 以下を追記):
- 確認キュー
36_wrk_ocr_queueのレビュー UI 設計(サイドバー vs シート直接編集 vs Google Form) - Gemini API の利用料・レート制限(
UrlFetchAppの呼び出し上限)への対処方針 - 証憑ファイルの保存先フォルダ管理(
Env.receiptFolderId()の活用可否・フォルダ権限設計) - インボイス番号(T番号)の
42_trn_journal課税区分判定への連携可否 - 処理済みハッシュの永続化先(
03_sys_paramsシートへの書き込みか、36_wrk_ocr_queue自体を参照するか)
Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit または Bash, ~250行)
実装プロンプトはバッククォートで囲まず、行頭4スペースインデントで記述すること(本文省略・仕様書本体参照)。
推奨実行モデル:
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| Step 1: シート新設 + 定数追加 | Claude Sonnet 4.6 | Constants の型確認・DDL パターン適用の判断が必要 |
| Step 2: OCR パイプライン実装 | Claude Opus 4.6 | Gemini API 統合・複数ファイル横断・Human-in-the-Loop 設計判断が必要 |
| Step 3: メニュー追加 | Claude Haiku 4.5 | 101_sys_config.js の既存パターンへの追記のみ |
変更履歴:
| 日付 | 変更内容 |
|---|---|
| 2026-04-20 | 初版作成 |
Step 2-4: 仕様書作成プロンプト全文の記録(Edit または Bash)
仕様書末尾の「## 仕様書作成プロンプト(再現性・監査性のため必ず記録)」セクションに以下の形式で追記する:
<details><summary>展開して表示</summary>
(この <instruction> プロンプトの全文をそのまま貼り付ける)
</details>
Phase 3: 保存・登録・コミット
3-A: _config.json への登録(必須)
docs/_config.json の nav 配列「§E.6 パイプライン・RPA・外部連携」セクションに追加:
{ "file": "dev/dev_mas-180_ai_ocr_enhancement.md", "title": "E.6.X MAS-180 AI-OCR連携による証憑自動起票" }
追記前に必ず git pull origin main で最新を取得し、JSON 構文エラーがないことを確認すること。
3-B: changelog への追記
docs/_internal/changelog.md のヘッダー直後に追記。
3-C: コミット&プッシュ
docs: N-04 AI-OCR連携による証憑自動起票の開発仕様書を作成 のメッセージで commit & push。
</details>