MAS-086: 領収書→経費タブ登録時の税区分自動判定
概要
| 項目 | 値 |
|---|---|
| 案件ID | MAS-086 |
| カテゴリ | バリデーション追加 / ユーティリティ拡張 |
| Phase | Phase 1.5(自動入力パイプライン 即効) |
| 優先度 | P1 / ★★ |
| 所要時間目安 | 約1時間 |
| 対象ファイル | 000_infra/004_utils.js(主) |
| 関連ファイル | 500_import/502_receipt_reader.js(呼び出し元・要確認)、100_config/101_sys_config.js(03_sys_params 初期化) |
| 前提案件 | なし |
【重要】「26タブ」について 案件タイトルの「26タブ」は
000_infra/002_constants.jsのSHEET_DEFAULTS/ID_PREFIX_MAPにも存在しない。 実際の税区分登録対象タブは32_wrk_expense(EXP_、経費タブ) である(SHEET_DEFAULTSで'税区分': '対象外'の既定値を持つ唯一の経費系シート)。 関連する33_wrk_finance(FIN_、財務タブ)にも同じ既定値があり、将来的には共通ユーティリティとして両方で利用可能。 本仕様書では対象シートを「経費タブ (32_wrk_expense)」に統一して扱う。
目的
OCR 等で読み取った金額情報(税込・税抜・税額の一部または全部)から消費税の税区分(課税10%・課税8%・非課税・対象外)を自動判定・提案し、経費タブ (32_wrk_expense) 登録時の手動選択工数を削減する。
背景
- 現状、経費タブの
税区分列はSHEET_DEFAULTSで既定値'対象外'が設定されるのみで、課税取引の場合は常に手動修正が必要。 500_import/502_receipt_reader.jsによる領収書 OCR は「税込金額_決済」「税抜金額_決済」「消費税額_決済」の 3 値を raw データとして取得できるが、税区分は一切設定していない。- 課税事業者(TODO_future.md の Section 5 I-系案件群)への移行を見据え、OCR 金額の比率から税区分を機械的に推定できる共通ユーティリティが必要。
- 判定ロジックは「税額 / 税抜 ≒ 10% なら課税10%、≒ 8% なら課税8%」のシンプルな演算で済み、
Utils.aiSuggestAccount()と同じく 提案 に徹する(Human-in-the-Loop 原則)。
現在のコード
税区分の既定値 (000_infra/002_constants.js L81-82)
{ pattern: '32_wrk_expense', prefix: 'EXP_', defaults: { '承認ステータス': '未申請', '収支区分': '支出', '税区分': '対象外', '通貨': 'JPY', '外貨金額': 0, '日本円金額': 0, '消費税額': 0, _dynamic: { '発生日(P/L計上日)': 'now' } } },
{ pattern: '33_wrk_finance', prefix: 'FIN_', defaults: { 'ステータス': '未申請', '収支区分': '支出', '税区分': '対象外', '通貨': 'JPY', '外貨金額': 0, '日本円金額': 0, '消費税額': 0, _dynamic: { '発生日(P/L計上日)': 'now' } } },
→ 新規行 append 時の 税区分 は常に '対象外'。課税取引でも OCR 結果を参照せず、人間が毎回書き換える運用。
InvoiceDTO の型定義 (000_infra/003_contracts.js L57)
* @property {string} 税区分 - "課税" | "非課税" | "対象外"
→ DTO 層で許容される値は "課税" | "非課税" | "対象外" の 3 値のみ。 '課税10%' 等の詳細値は DTO に直接セットできない。
OCR による金額取得 (500_import/502_receipt_reader.js L125-145)
var rowData = [
rcpId, // 管理ID
now, // 処理日時
extracted.docType || '領収書', // 証憑種別
Utils.normalizePartnerName(extracted.vendor || ''), // 取引先名
extracted.address || '', // 🏢住所
extracted.totalAmount || 0, // 税込金額_決済
extracted.subtotal || 0, // 税抜金額_決済
extracted.tax || 0, // 消費税額_決済
// ... (税区分の設定は一切なし)
];
→ 領収書 OCR は receipt タブへ raw データを書き込むのみで、32_wrk_expense へのインポート時に 税区分 を判定する箇所が存在しない。
Utils.aiSuggestAccount() (000_infra/004_utils.js L200-213)
/**
* 摘要テキストから勘定科目を推論 (Constants.ACCOUNT_RULES ベース)
*/
aiSuggestAccount: function(text) {
text = String(text).toLowerCase();
for (const rule of Constants.ACCOUNT_RULES) {
for (const kw of rule.keywords) {
if (text.includes(kw)) return rule.account;
}
}
return '';
},
→ 「提案を返す」同類の共通ユーティリティ。本仕様で追加する Utils.suggestTaxCategory() はこの直後(L214 付近)に挿入する。
Constants.getParam() (000_infra/002_constants.js L147-167)
getParam: function(key, defaultVal) {
if (!this._paramsCache) {
this._paramsCache = {};
try {
var ss = SpreadsheetApp.getActiveSpreadsheet() || (typeof getWebSpreadsheet_ === 'function' ? getWebSpreadsheet_() : null);
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);
},
→ 第2引数が number なら Number(val) で型変換する実装。税率パラメータ(0.10 / 0.08 / 0.005)も number 型で取得可能。
03_sys_params の既存登録状況
101_sys_config.jsではCFG_DDL_VERSION等の管理のみで、CONSUMPTION_TAX_RATE_STANDARD/_REDUCED/_TOLERANCEの登録ロジックは未実装。- 実装時に以下のいずれかで追加する:
03_sys_paramsシートに手動で 3 行追加(最小変更)101_sys_config.jsの初期化ロジックに追加(推奨・再現性あり)
修正方針
新規関数 Utils.suggestTaxCategory() の追加
000_infra/004_utils.js の Utils.aiSuggestAccount() 直後(L214 付近、}, の次)に以下を追加する。
/**
* 金額情報から消費税の税区分を自動判定する (S-14)
* @param {{ taxInclusive: number|null, taxExclusive: number|null, tax: number|null }} amounts
* @returns {'課税10%'|'課税8%'|'非課税'|'対象外'|'判定不能'} 判定結果
*/
suggestTaxCategory: function(amounts) { ... }
コアロジック(仕様)
amounts.taxInclusive/amounts.taxExclusive/amounts.taxについて、null/ 空文字 / 非数値 (isNaN) を「欠損値」と判定する(0 は有効値)。- 有効な値が 2 つ以上揃っているか確認。1 つ以下なら
'判定不能'を即返す(補完計算不可)。 - 欠損値を他 2 値から補完計算する:
taxExclusive欠損 →taxInclusive - taxtax欠損 →taxInclusive - taxExclusivetaxInclusive欠損 →taxExclusive + tax
- 返品等でマイナス値になる場合は
Math.abs()で絶対値に変換(税率の正負は不変)。 taxExclusive <= 0(税抜ゼロ・マイナス)の場合はゼロ除算回避(失敗パターン #2 対策):tax === 0→'非課税'tax !== 0→'対象外'(税抜ゼロなのに税だけ付く異常ケース)
rate = tax / taxExclusiveを計算。- パラメータ参照:
Constants.getParam('CONSUMPTION_TAX_RATE_STANDARD', 0.10)— 標準税率Constants.getParam('CONSUMPTION_TAX_RATE_REDUCED', 0.08)— 軽減税率Constants.getParam('CONSUMPTION_TAX_RATE_TOLERANCE', 0.005)— 判定許容誤差
- 判定:
|rate - std| <= tol→'課税10%'|rate - red| <= tol→'課税8%'rate === 0→'非課税'- それ以外 →
'判定不能'
DTOへの反映マップ(呼び出し元で適用)
suggestTaxCategory の戻り値 | DTO 税区分 セット値 |
|---|---|
'課税10%' | '課税' |
'課税8%' | '課税' |
'非課税' | '非課税' |
'対象外' | '対象外' |
'判定不能' | 既存値を維持(上書きしない) |
重要:
InvoiceDTO.税区分の有効値は"課税" | "非課税" | "対象外"の 3 値(003_contracts.jsL57 の型定義)。'課税10%'等の内部戻り値を DTO に直接セットしないこと。マッピング辞書を呼び出し元で必ず適用する。
パラメータ管理
本仕様で新規に 03_sys_params シートへ以下 3 行を登録する必要がある:
| キー | 既定値 | 説明 |
|---|---|---|
CONSUMPTION_TAX_RATE_STANDARD | 0.10 | 消費税 標準税率(10%) |
CONSUMPTION_TAX_RATE_REDUCED | 0.08 | 消費税 軽減税率(8%) |
CONSUMPTION_TAX_RATE_TOLERANCE | 0.005 | 判定許容誤差(±0.5%)。端数処理差異を吸収 |
未登録の場合も Constants.getParam() のデフォルト値フォールバックで動作するが、本番運用前に必ず登録する。
影響範囲
| 種別 | ファイル | 内容 |
|---|---|---|
| 変更(追記のみ) | 000_infra/004_utils.js | Utils.suggestTaxCategory() を新規追加(既存関数は無変更) |
| 呼び出し元(要追加) | 500_import/502_receipt_reader.js 等 | OCR 結果から DTO 構築時に suggestTaxCategory() を呼び出し、戻り値をマッピング辞書で変換してセット |
| データ(要追加) | 03_sys_params シート | 消費税率 3 パラメータの新規登録 |
| 無変更 | 000_infra/003_contracts.js | DTO 型定義は既存の "課税" | "非課税" | "対象外" のまま使用 |
| 無変更 | 200_data/201_data_validator.js | 本仕様ではバリデーション追加なし(「提案」のみ) |
注意事項
- 既存関数を変更しない —
Utils.aiSuggestAccount()等を巻き込んだリファクタは禁止。追記のみとする。 - 税率のハードコード禁止 — 必ず
Constants.getParam()経由で取得すること(失敗パターン #3「DDLコード値 vs 実データ乖離」対策。将来の軽減税率改定にも対応)。 - ゼロ除算を必ず回避 —
taxExclusive <= 0の早期リターンは削除しないこと(失敗パターン #2「ゼロ除算フォールバック」対策)。 - 有効値カウントでは 0 を有効と扱う —
税額 0 円 & 税抜 1000 円 & 税込 1000 円の非課税ケースを正しく判定するため。if (!val)は使わず、v !== null && v !== '' && !isNaN(Number(v))で判定する。 - DTO への代入値の厳格さ — 必ずマッピング辞書
{'課税10%':'課税', '課税8%':'課税', '非課税':'非課税', '対象外':'対象外'}を経由し、'課税10%'等をそのまま書き込まない。 - Human-in-the-Loop 遵守 — 本関数は「提案」を返すのみ。最終確定は人間が UI 上で承認する運用(プロダクトポリシー準拠)。呼び出し元 UI では自動提案値の視覚通知(セル背景色等)を検討する(本仕様書のスコープ外)。
- キャッシュの副作用 —
Constants._paramsCacheは起動時に一度だけロードされる。スクリプトエディタで03_sys_paramsを更新した場合、GASプロセス再実行まで反映されない点に留意。
エッジケース
| 条件 | 戻り値 | 理由 |
|---|---|---|
| 有効な金額情報が 1 つ以下 | '判定不能' | 補完計算不可 |
taxExclusive <= 0 かつ tax === 0 | '非課税' | ゼロ除算回避。税抜0・税0は非課税扱い |
taxExclusive <= 0 かつ tax !== 0 | '対象外' | ゼロ除算回避。税抜0で税だけ付く異常ケースは対象外 |
tax === 0 かつ taxExclusive > 0 | '非課税' | 税率 0% = 非課税扱い |
税抜 + 税額 ≠ 税込(1円以上の不整合) | 欠損値は補完、全値揃っている場合はそのまま税額/税抜で判定 | 端数処理の差異は tol = 0.005 で吸収 |
| 返品等で金額がマイナス | 絶対値に変換して判定 | 税率の正負は変わらない |
03_sys_params にパラメータ未登録 | Constants.getParam() のデフォルト値(0.10 / 0.08 / 0.005)で動作 | フォールバック定義済み |
| 税込1100円・税抜1000円・税額100円 | '課税10%' | 100/1000 = 0.10、標準税率と一致 |
| 税込1080円・税抜1000円・税額80円 | '課税8%' | 80/1000 = 0.08、軽減税率と一致 |
| 税込1100円・税抜900円・税額100円 | '判定不能' | 100/900 ≒ 0.111、どちらの税率にも該当しない(許容誤差超過) |
全て null | '判定不能' | 情報なし |
amounts === undefined | TypeError(引数必須) | 呼び出し元で引数チェックを行う責務 |
実データ検証
実装前に以下を MCP / スプレッドシートで確認する:
03_sys_paramsシートの既存キー確認:CONSUMPTION_TAX_RATE_STANDARD/CONSUMPTION_TAX_RATE_REDUCED/CONSUMPTION_TAX_RATE_TOLERANCEの 3 キーが既に登録されているか。- 未登録なら以下を A列・B列に追加(C列「説明」にコメント任意):
CONSUMPTION_TAX_RATE_STANDARD/0.10CONSUMPTION_TAX_RATE_REDUCED/0.08CONSUMPTION_TAX_RATE_TOLERANCE/0.005
32_wrk_expenseの税区分プルダウン確認:- 列の入力規則(データ検証)が
"課税" | "非課税" | "対象外"の 3 値に設定されているか。 InvoiceDTO型定義と一致しない値(例:'課税10%'等)が誤ってセットされないか確認。
- 列の入力規則(データ検証)が
- OCR 読み取りサンプルの入力欠損パターン確認:
502_receipt_reader.jsが書き込むreceiptタブの既存行で、税込金額_決済/税抜金額_決済/消費税額_決済が揃っているか、欠損パターンがあるかを目視確認。- 欠損が頻出するなら「有効値 2 つ以上必須」の設計が実運用に合うか再検討する材料となる。
- 既存経費レコードの
税区分分布確認:32_wrk_expenseで税区分が'対象外'以外('課税'/'非課税')となっているレコード数を集計し、手動設定の割合を把握。
関連ドキュメント
| 種別 | 参照先 | 関連内容 |
|---|---|---|
| プロジェクト規約 | CLAUDE.md | データアクセス規約(列参照はヘッダー名ベース)、会計ロジック規約(科目マスタ未登録のキーワード推測禁止) |
| プロダクトポリシー | PRD プロダクトポリシー | Human-in-the-Loop(提案→承認→確定) |
| 失敗パターン | failure_patterns.md | #2 ゼロ除算フォールバック、#3 DDLコード値 vs 実データ乖離、#21-24 数式設計の落とし穴 |
| 関連仕様 | dev_mas-089_consumption_tax_exclusive_method.md | 消費税 税抜方式への切替え(中長期) |
| 関連仕様 | dev_mas-083_pipeline_tax.md | パイプライン売上の消費税対応(課税/免税の切替) |
| 関連仕様 | dev_mas-161_ocr_manual_correction_assist.md | OCR手動補正支援(本仕様の UX 連携検討対象) |
| 類似ユーティリティ | 000_infra/004_utils.js Utils.aiSuggestAccount() L200-213 | 同じ「提案」系ユーティリティの実装パターン |
人間が検討すべき事項
TODO_future.md からの転記事項
- 税区分の判定ルール: 税率からの逆算精度、軽減税率 8% と標準税率 10% の判別について(TODO_future.md Section 5 より転記)。
- → 本仕様では
tol = 0.005(±0.5%)を許容誤差として採用。端数処理の差異を吸収しつつ、10%/8% を明確に判別可能。
- → 本仕様では
- 課税事業者移行との整合: 免税事業者期間中は税区分が常に
'対象外'となる運用との整合性をどう取るか。- → 本関数は「金額比率からの判定」であり、免税期間中でも
rate === 0なら'非課税'を返す。運用判断として「免税事業者期間中は呼び出し元でスキップ」する設計もあり得る(本仕様書ではスコープ外、将来検討)。
- → 本関数は「金額比率からの判定」であり、免税期間中でも
本仕様で追加で検討すべき事項
- Human-in-the-Loop 必須: 本関数はあくまで「提案」を返す。呼び出し元 UI では提案値セルの背景色変更等で「自動提案値」であることを視覚的に通知し、最終確認は人間が行うフローとする(プロダクトポリシー準拠)。UI 層の具体設計は本仕様書のスコープ外だが、後続案件として UX 設計を推奨。
'判定不能'時の UX: セルのハイライトや入力促進トースト通知の検討(本仕様書のスコープ外だが後続案件 MAS-161 と合わせて検討)。'非課税'vs'対象外'の分離: 軽減税率品目(食品等)・非課税取引(保険等)・不課税取引(給与等)の会計上の厳密な区別を、単純な金額比率だけで自動判定するのは不可能。現ロジックは「税額 0 円 → 非課税」と「税抜0円 & 税非0円 → 対象外」の雑な区別に留まる。運用上は「自動提案値は課税10%/課税8%のみ信頼、それ以外は人間判断」とする方針が現実的。- 軽減税率改定の影響: 消費税率が将来改定された場合、
03_sys_paramsの値を書き換えるだけで追随可能(パラメータ化の利点)。ただし「新旧税率の混在期間」の扱いは要仕様追加(例: 発生日に応じた税率選択)。本仕様では発生日を参照しない単純な比率判定のみ。 - 外貨取引の扱い: 本関数は JPY 前提。外貨(USD/EUR等)の場合は為替レート換算後の税率判定となるが、本仕様では扱わない(
amounts引数は JPY 換算後の数値である前提)。 - テスト自動化:
901_test_runner.jsに以下のケースを追加することを推奨(実装プロンプトでも言及):- 10%標準税率の判定 / 8%軽減税率の判定 / 非課税 / 対象外 / 判定不能(情報不足)/ マイナス値(返品)/ ゼロ除算回避
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
CLIエージェントである「Claude Code」として、以下の指示に従い、
案件 MAS-086「領収書→経費タブ登録時の税区分自動判定」を実装してください。
## 実行前タスク(コンテキストの読み込み)
実装前に必ず以下のファイルを Read で確認し、行番号・シグネチャを正確に把握してください:
1. `000_infra/004_utils.js` — `Utils.aiSuggestAccount()` の末尾位置(現在 L213 付近の `},`)。
`Utils.suggestTaxCategory()` はその直後に挿入する。`Utils.parseAmt()` の挙動 (L191-198) も確認。
2. `000_infra/003_contracts.js` — `InvoiceDTO.税区分` の型定義 L57 (`"課税" | "非課税" | "対象外"`) を確認。
DTO に `'課税10%'` 等を直接セットしないための制約。
3. `000_infra/002_constants.js` — `Constants.getParam()` L147-167 の実装。
第2引数が number なら `Number(val)` 変換される点を確認。
4. `500_import/502_receipt_reader.js` — L125-145 の receipt タブ書き込み箇所。
本仕様では receipt タブは無変更。経費タブ (`32_wrk_expense`) 側への反映方法は
呼び出し元の既存 UI/インポート処理に合わせる(receipt → 32_wrk_expense 転記時に呼ぶ)。
5. `100_config/101_sys_config.js` — `03_sys_params` 初期化ロジックの参照方法を確認。
6. `docs/dev/dev_mas-086_tax_category_suggester.md` — 本開発仕様書。
## 修正対象ファイル
- `000_infra/004_utils.js` — `Utils.suggestTaxCategory()` を新規追加(既存関数の変更禁止)
- (任意)`500_import/502_receipt_reader.js` 等の呼び出し元 — 戻り値をマッピング辞書経由で DTO にセット
- (必須)`03_sys_params` シート — 消費税率 3 パラメータの新規登録
## 実装内容
### 1. `Utils.suggestTaxCategory()` の追加(`004_utils.js`)
`Utils.aiSuggestAccount()` 末尾(L213 の `},`)の直後に以下を追加する:
/**
* 金額情報から消費税の税区分を自動判定する (MAS-086)
* @param {{ taxInclusive: number|null, taxExclusive: number|null, tax: number|null }} amounts
* @returns {'課税10%'|'課税8%'|'非課税'|'対象外'|'判定不能'}
*/
suggestTaxCategory: function(amounts) {
var inc = Utils.parseAmt(amounts.taxInclusive);
var exc = Utils.parseAmt(amounts.taxExclusive);
var tax = Utils.parseAmt(amounts.tax);
// 有効値カウント (0 も有効値として扱う。null/''/NaN は除外)
var validCount = [amounts.taxInclusive, amounts.taxExclusive, amounts.tax]
.filter(function(v) { return v !== null && v !== '' && !isNaN(Number(v)); }).length;
if (validCount < 2) return '判定不能';
// 不足値を他2値から補完
if (amounts.taxExclusive === null || amounts.taxExclusive === '') exc = inc - tax;
if (amounts.tax === null || amounts.tax === '') tax = inc - exc;
if (amounts.taxInclusive === null || amounts.taxInclusive === '') inc = exc + tax;
// 返品等マイナス金額は絶対値で処理(税率の正負は変わらない)
exc = Math.abs(exc); tax = Math.abs(tax);
// ゼロ除算回避
if (exc <= 0) return (tax === 0) ? '非課税' : '対象外';
var rate = tax / exc;
var std = Constants.getParam('CONSUMPTION_TAX_RATE_STANDARD', 0.10);
var red = Constants.getParam('CONSUMPTION_TAX_RATE_REDUCED', 0.08);
var tol = Constants.getParam('CONSUMPTION_TAX_RATE_TOLERANCE', 0.005);
if (Math.abs(rate - std) <= tol) return '課税10%';
if (Math.abs(rate - red) <= tol) return '課税8%';
if (rate === 0) return '非課税';
return '判定不能';
},
### 2. 呼び出し元への反映(`502_receipt_reader.js` 等、既存 UI/インポートに合わせる)
`Utils.suggestTaxCategory()` の戻り値を `InvoiceDTO.税区分` / 経費タブ `税区分` 列に
マッピング辞書経由でセットする:
var suggestion = Utils.suggestTaxCategory({
taxInclusive: row['税込金額'], // 経費タブの列名に合わせて調整
taxExclusive: row['税抜金額'],
tax: row['消費税額']
});
var TAX_CATEGORY_MAP = {
'課税10%': '課税', '課税8%': '課税',
'非課税': '非課税', '対象外': '対象外'
};
if (TAX_CATEGORY_MAP[suggestion]) {
row['税区分'] = TAX_CATEGORY_MAP[suggestion];
// Human-in-the-Loop: 自動提案値であることをセルの背景色等で通知する(UI層の責務)
}
// '判定不能' の場合は既存値を維持(上書きしない)
### 3. `03_sys_params` への 3 パラメータ追加
dev 環境で `03_sys_params` シートを開き、以下 3 行を追加(A列=キー、B列=値):
- `CONSUMPTION_TAX_RATE_STANDARD` / `0.10`
- `CONSUMPTION_TAX_RATE_REDUCED` / `0.08`
- `CONSUMPTION_TAX_RATE_TOLERANCE` / `0.005`
(`101_sys_config.js` の初期化ロジックへの追加は別案件として切り出し可能)
## 制約
- `Utils.suggestTaxCategory()` の追加のみ。`Utils` オブジェクト内の既存関数を変更しない。
- DTO / 経費タブの `税区分` 列に `'課税10%'` 等の内部戻り値を直接セットしない。
有効値は `"課税" | "非課税" | "対象外"` の 3 値のみ(必ず `TAX_CATEGORY_MAP` 経由)。
- 税率のハードコード禁止。必ず `Constants.getParam()` 経由で取得する(将来の税率改定対応)。
- ゼロ除算回避の早期リターンを削除しない(失敗パターン #2 対策)。
- 有効値カウントでは 0 を有効値として扱う(`if (!val)` 使用禁止、`v !== null && v !== '' && !isNaN(Number(v))` で判定)。
## エッジケース
仕様書「エッジケース」テーブルに従う(特にゼロ除算・マイナス金額・入力欠損)。
## 実データ検証
実装前に必ず以下を確認:
- `03_sys_params` に 3 パラメータが登録済みか(未登録なら手動で追加)
- `32_wrk_expense` の `税区分` 列のプルダウン値が `"課税" | "非課税" | "対象外"` の 3 値か
- OCR サンプルの税込/税抜/税額欠損パターンの頻度
## 動作確認
1. `npm run push:dev` で開発環境にデプロイ
2. 開発用スプレッドシートで `03_sys_params` に 3 パラメータが存在することを確認
3. GAS スクリプトエディタから以下のテストを実行(`901_test_runner.js` に追加推奨):
// 10% 標準税率
console.log(Utils.suggestTaxCategory({taxInclusive:1100, taxExclusive:1000, tax:100}));
// → '課税10%'、DTO マップ後は '課税'
// 8% 軽減税率
console.log(Utils.suggestTaxCategory({taxInclusive:1080, taxExclusive:1000, tax:80}));
// → '課税8%'、DTO マップ後は '課税'
// 非課税
console.log(Utils.suggestTaxCategory({taxInclusive:1000, taxExclusive:1000, tax:0}));
// → '非課税'
// 情報不足
console.log(Utils.suggestTaxCategory({taxInclusive:1000, taxExclusive:null, tax:null}));
// → '判定不能'
// 返品(マイナス金額)
console.log(Utils.suggestTaxCategory({taxInclusive:-1100, taxExclusive:-1000, tax:-100}));
// → '課税10%'
// ゼロ除算回避
console.log(Utils.suggestTaxCategory({taxInclusive:0, taxExclusive:0, tax:0}));
// → '非課税'(税抜0 & 税0)
console.log(Utils.suggestTaxCategory({taxInclusive:100, taxExclusive:0, tax:100}));
// → '対象外'(税抜0 で税だけ付く異常ケース)
4. `901_test_runner.js` に上記 7 ケースのテストを追加し、「テスト実行」メニューから全件 PASS を確認
5. 実運用の receipt タブから経費タブへ転記する UI を手動でテスト:
- 提案値が `'課税'` になるケース/ならないケース(`'判定不能'`)をそれぞれ確認
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| ファイル読み込み・挿入位置特定 | なし | 行番号・シグネチャは仕様書で確定済み |
| ロジック実装 | なし | 仕様書で完全定義済み、写経レベル |
| 呼び出し元反映 | **あり** | 既存 UI/インポートフロー特定のため |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 「26タブ」の曖昧さの解消、DTO型制約とOCR出力の突合、ゼロ除算の扱い方針選定、Constants.getParam() 連携方針など複数モジュールの横断判断 |
実装(004_utils.js 追加) | Claude Haiku 4.5 | 仕様書で関数コードが完全定義済み、判断要素なし |
| 実装(呼び出し元反映) | Claude Sonnet 4.6 | 既存 UI/インポートフローの特定と適切なフックポイント判断(中程度の判断) |
| 動作確認 | ユーザー手動 | GASエディタでのテスト実行とスプレッドシート目視確認が必要 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-19 | 初版作成 |
仕様書作成プロンプト
展開して表示
<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(骨格 ~20行) / 2-2(前半 ~300行) / 2-3a(後半a ~200行) / 2-3b(実装プロンプト~変更履歴 ~250行) / 2-4(`<details>`プロンプト全文記録) に分割して実行する。
4. **各 Step で何を書くかを具体指示**: Phase 1 で設計判断を完全に完了させ、Phase 2 実行時に再考しない。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 **S-14「領収書→経費タブ登録時の税区分自動判定」** の開発仕様書を作成してください。
作成後、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
以下を **Read / Grep** で調査し、Phase 2 で設計判断を行わないよう、全固有名詞・行番号・型を確定させること。「名前から推測した瞬間に手を止めて Read」(失敗パターン #18-#20 の直接対策)。
### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` を Grep で **S-14** を検索し、案件名・概要・人間が検討すべき事項を取得する。(案件名の正式表記を必ず確認すること。~~打ち消し線~~ が付いている場合は最新表記を採用する)
### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` を Read し、コーディング規約(特に「データアクセス」「会計ロジック」節)を把握する。
### 1-C: 既存仕様書テンプレートの読み込み
- `docs/dev/dev_mas-075_expense_date_validation.md` を Read し、バリデーション追加系のフォーマットを把握する。
### 1-D: 関連コードの調査(必ず Read で裏取りすること)
| # | 読み込みファイル | 確認すべき内容 |
|:-:|----------------|--------------|
| 1 | `000_infra/002_constants.js` | `Constants.getParam()` の実装(引数・戻り値の型、`03_sys_params` を参照していることを確認)。`SHEET_DEFAULTS` に `32_wrk_expense` (EXP_) / `33_wrk_finance` (FIN_) のデフォルト値が定義されていることを確認。`03_sys_params` に消費税率パラメータが既存登録されているか確認(**未登録なら新規追加が必要**) |
| 2 | `000_infra/004_utils.js` | `Utils.parseAmt()` の実装と引数・戻り値を確認。`Utils.aiSuggestAccount()` の実装パターン(同ファイルへの追加関数の挿入位置・スタイルの参考)を確認。新関数 `Utils.suggestTaxCategory()` の挿入位置(`aiSuggestAccount` の直後を候補)を特定し行番号を記録する |
| 3 | `000_infra/003_contracts.js` | `InvoiceDTO` の `税区分` プロパティの型定義 (`"課税" \| "非課税" \| "対象外"`) を Read で確認。`"非課税"` と `"対象外"` が別値であることを確認する(「非課税/対象外」に統合してよいかは Phase 1 で判断) |
| 4 | `500_import/502_receipt_reader.js` (または `503_receipt_reader.js`)| OCR読み取り処理の呼び出しフロー、税込・税抜・税額をどのように取得しているかを確認。このファイルから InvoiceDTO の `税区分` を設定している箇所があれば行番号を記録する |
| 5 | `100_config/101_sys_config.js` | メニュー定義(`onOpen()`)に税区分関連のメニュー項目があるか確認。`03_sys_params` の初期化ロジックがあればパラメータ追加方法を確認する |
| 6 | `docs/_internal/failure_patterns.md` | #2(ゼロ除算フォールバック)、#3(DDLコード値 vs 実データ乖離)、#21-#24(数式設計の落とし穴)を確認する |
> **【要調査】** 案件名の「26タブ」が指す実際のシートを必ず特定すること。`002_constants.js` の `SHEET_DEFAULTS` や `ID_PREFIX_MAP` を Read し、`26_xxx` という命名のシートが存在するか確認する。存在しない場合は `32_wrk_expense`(EXP_)が対象の可能性が高いため、その旨を仕様書に明記する。
---
## Phase 2: 仕様書の分割作成
**出力先**: `docs/dev/dev_mas-086_tax_category_suggester.md`(ファイル名のIDは大文字 `MAS-086`)
(以下、オリジナルの Phase 2-1 〜 2-4 / Phase 3 指示全文をこの仕様書の作成に使用した。本 `<details>` は仕様書作成プロンプトの再現用であり、実行タスクの完全再現には本ファイル末尾のみで十分である)
</instruction>