最終更新: 2026/06/22 18:56
MAS-145: 法人口座CSV取込・自動マッチング
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-145 |
| カテゴリ | 外部データ取込 |
| Phase | P1.5 |
| 優先度 | ★★★(Phase 1.5 最優先) |
| 所要時間 | 3-4時間 |
| 対象ファイル | 500_import/502_bank_importer.js (新規), 100_config/101_sys_config.js (メニュー追加) |
| 前提案件 | なし(独立着手可能) |
目的
銀行の入出金明細CSVを取り込み、33_wrk_bank の未処理STLと自動マッチングすることで、現在最大のボトルネックである「銀行明細の手入力」を完全排除する。決済日_実績・決済ステータスを自動設定し、月次決算の所要時間を劇的に短縮する。
契約銀行がAPIを提供していないため、CSV取込が恒久的なアプローチとなる(MAS-181 銀行API連携は対象外へ移動済み)。
取込方式のアーキテクチャ決定
比較検討
| 比較軸 | 案A: ステージングシート方式 | 案B: Google Drive CSV方式 |
|---|---|---|
| 操作の簡便さ | ◎ スプレッドシートにCSVを貼り付けるだけ。GAS操作に慣れたユーザーに自然 | △ DriveへのアップロードURL指定が必要。フォルダ構造の設計も必要 |
| エラーハンドリング | ◎ 貼り付け時にデータがシート上に見えるため、目視確認が容易 | △ パースエラー時のフィードバックがUI上で見えにくい |
| 501_cc_importerとの一貫性 | ◎ 34_wrk_card にCSVを貼り付け→マッチング→消込の同一パターン | × 全く異なる取込フロー |
| 既存ワークフローとの親和性 | ◎ クレカ・領収書と同じ「貼り付け→確認→実行」の統一UX | △ Drive操作という新しい導線が必要 |
| 複数銀行の切替 | ○ シートは1枚で、メニューから銀行を選択 | ○ ファイル名規約で銀行を判別可能 |
決定: 案A(ステージングシート方式)を採用
理由: 501_cc_importer.js で確立された「ステージングシート + 2段階処理 + 確認FLG」パターンとの一貫性が最も重要。ユーザーの学習コストを最小化し、操作ミスのリスクを低減する。
銀行別CSVフォーマットのアダプター設計
正規化済みレコード型
銀行CSVの列構成差異を吸収する共通の中間レコード型を定義する。
/**
* 銀行CSV → 正規化済みレコード
* @typedef {Object} NormalizedBankTx
* @property {Date} txDate - 取引日
* @property {string} memo - 摘要(銀行の摘要文をそのまま保持)
* @property {number} deposit - 入金額(0以上)
* @property {number} withdrawal - 出金額(0以上)
* @property {number} balance - 残高
* @property {string} rawRow - 元CSVの行データ(デバッグ・トレース用)
*/
アダプターインターフェース
/**
* 銀行別CSVパーサーの共通インターフェース
* @typedef {Object} BankAdapter
* @property {string} bankName - 銀行表示名
* @property {string} accountKey - 決済口座キー(例: '口座振込_福井銀行')
* @property {function(string[][]): NormalizedBankTx[]} parse
* — シートの2次元配列(ヘッダー含む)を受け取り、正規化レコード配列を返す
*/
初期対応アダプター
| 銀行 | accountKey | CSV列構成(想定) | 備考 |
|---|---|---|---|
| 福井銀行 | 口座振込_福井銀行 | 日付, 摘要, お支払金額, お預り金額, 残高 | メインバンク |
| SBIネット銀行 | 口座振込_SBI | 日付, 内容, 出金額, 入金額, 残高, メモ | サブ口座 |
新しい銀行の追加方法
BANK_ADAPTERS 配列に新しいアダプターオブジェクトを追加するだけ。parse関数でCSVの列マッピングを定義する。
var BANK_ADAPTERS = [
{
bankName: '福井銀行',
accountKey: '口座振込_福井銀行',
parse: function(data) { /* 列マッピング */ }
},
{
bankName: 'SBIネット銀行',
accountKey: '口座振込_SBI',
parse: function(data) { /* 列マッピング */ }
}
];
現在のコード
参照パターン: 501_cc_importer.js の2段階処理
Step 1 — マッチング確認(importCcStatement):
34_wrk_cardからクレカ明細を読み込み33_wrk_bankの未処理STLと2段階マッチング(Pass 1: 確実マッチ / Pass 2: 候補提案)- マッチ結果を
34_wrk_cardのマッチ結果列(確認FLG, 処理結果, マッチ決済ID等)に書き込み - 消込は行わない
Step 2 — 消込実行(applyCcSettlement):
確認FLG=TRUEかつ処理結果=MATCHEDの行のみ処理33_wrk_bankの決済ステータス→消込済,決済日_実績,消込手段→カード明細を更新
参照パターン: マッチ結果列の構造
var MATCH_EXTRA_HEADERS = [
'確認FLG', '処理結果', 'マッチ決済ID(STL)',
'STL決済日_計画', 'STL決済金額', 'STL取引先名', 'STL摘要'
];
33_wrk_bank(BankTxDTO)の列構成
有効フラグ, 決済ID(STL), 消込対象請求ID(INV), 決済日_計画,
立替日, 決済日_実績, 決済ステータス, 入出金区分, 決済口座,
取引先名, 税込金額_決済, 差額(手数料等), 差額処理科目,
組織名, 摘要, 消込手段, 自動仕訳JNL_ID
修正方針
全体アーキテクチャ
36_wrk_bank_import (ステージングシート)
↓ ユーザーが銀行CSVを貼り付け
↓
502_bank_importer.js
├─ Step 1: importBankStatement() — パース + マッチング確認
│ ├─ 銀行アダプターでCSVをパース → NormalizedBankTx[]
│ ├─ 重複チェック(取引日+金額+摘要のハッシュ)
│ ├─ 33_wrk_bank の未処理STLと2段階マッチング
│ └─ マッチ結果を 36_wrk_bank_import のマッチ結果列に表示
│
└─ Step 2: applyBankSettlement() — 確認FLG=TRUE で消込実行
├─ 33_wrk_bank の決済ステータス→消込済, 決済日_実績, 消込手段→銀行CSV
└─ マッチなしの明細は新規STLとして33_wrk_bankに追記(任意)
Step 0: DDL — ステージングシート 36_wrk_bank_import の新設
101_sys_config.js の setupAllSchemas に追加。
'WRK_BANK_IMPORT': {
headers: [
"取引日", "摘要", "出金額", "入金額", "残高",
"銀行名", "取込ハッシュ",
"確認FLG", "処理結果",
"マッチ決済ID(STL)", "STL決済日_計画", "STL決済金額",
"STL取引先名", "STL摘要"
],
color: "#38761d"
}
01_sys_config の configDefaults にも追加:
if (!existKeys.includes('WRK_BANK_IMPORT'))
confSheet.appendRow(['WRK_BANK_IMPORT', '', '36_wrk_bank_import', '銀行CSV取込']);
Step 1: importBankStatement() — パース + マッチング確認
/**
* メニュー: 🏦 銀行CSV マッチング確認
* 36_wrk_bank_import のCSVデータをパースし、33_wrk_bank の未処理STLと
* 自動マッチング。結果をマッチ結果列に表示する。消込は行わない。
*/
function importBankStatement() {
var FUNC = 'importBankStatement';
try {
var ss = getWebSpreadsheet_();
var ui = SpreadsheetApp.getUi();
// --- 銀行選択ダイアログ ---
var bankChoice = ui.prompt(
'🏦 銀行CSV取込',
'銀行を選択してください:\n1: 福井銀行\n2: SBIネット銀行',
ui.ButtonSet.OK_CANCEL
);
if (bankChoice.getSelectedButton() !== ui.Button.OK) return;
var adapterIdx = parseInt(bankChoice.getResponseText()) - 1;
if (adapterIdx < 0 || adapterIdx >= BANK_ADAPTERS.length) {
return ui.alert('🚨 無効な選択です。');
}
var adapter = BANK_ADAPTERS[adapterIdx];
// --- 36_wrk_bank_import ---
var importSheet = Utils.getSheetByKey('WRK_BANK_IMPORT', '36_wrk_bank_import');
if (!importSheet) return ui.alert('🚨 36_wrk_bank_import が見つかりません。');
var importData = importSheet.getDataRange().getValues();
if (importData.length < 2) return ui.alert('🚨 CSVデータがありません。');
var importIdx = buildHeaderIndex_(importData[0]);
// --- CSVパース ---
var txns = adapter.parse(importData);
if (txns.length === 0) return ui.alert('🚨 有効な明細が見つかりません。');
// --- 重複チェック(取込ハッシュ) ---
var hashCol = importIdx['取込ハッシュ'];
var existingHashes = {};
// 33_wrk_bank の摘要+日付+金額から既存ハッシュを構築
// ... 省略(後述のエッジケース参照)
// --- 正規化データを36タブに書き戻し ---
for (var t = 0; t < txns.length; t++) {
var row = t + 2; // ヘッダー行 + 1
var tx = txns[t];
importSheet.getRange(row, importIdx['取引日'] + 1).setValue(tx.txDate);
importSheet.getRange(row, importIdx['摘要'] + 1).setValue(tx.memo);
importSheet.getRange(row, importIdx['出金額'] + 1).setValue(tx.withdrawal);
importSheet.getRange(row, importIdx['入金額'] + 1).setValue(tx.deposit);
importSheet.getRange(row, importIdx['残高'] + 1).setValue(tx.balance);
importSheet.getRange(row, importIdx['銀行名'] + 1).setValue(adapter.bankName);
importSheet.getRange(row, importIdx['取込ハッシュ'] + 1).setValue(
computeTxHash_(tx.txDate, tx.memo, tx.deposit, tx.withdrawal)
);
}
// --- 33_wrk_bank の候補STL抽出 ---
var bankSheet = Utils.getSheetByKey('WRK_BANK', '33_wrk_bank');
var bankData = bankSheet.getDataRange().getValues();
var bankIdx = buildHeaderIndex_(bankData[0]);
var candidates = extractBankCandidateStls_(bankData, bankIdx, adapter.accountKey);
// --- 2段階マッチング ---
var matchCount = 0, suggestCount = 0, unmatchCount = 0;
for (var t = 0; t < txns.length; t++) {
var row = t + 2;
var tx = txns[t];
var txAmt = tx.withdrawal > 0 ? tx.withdrawal : tx.deposit;
var txType = tx.withdrawal > 0 ? '出金' : '入金';
if (txAmt === 0) {
importSheet.getRange(row, importIdx['処理結果'] + 1).setValue('SKIP:金額ゼロ');
continue;
}
// Pass 1: 金額完全一致 + 日付±3日 + 入出金区分一致
var bestIdx = -1;
var bestDateDiff = Infinity;
for (var s = 0; s < candidates.length; s++) {
if (candidates[s].matched) continue;
if (candidates[s].ioType !== txType) continue;
if (Math.abs(candidates[s].amount - txAmt) >= 1) continue; // 金額完全一致(1円未満の誤差許容)
var dateDiff = Math.abs(
(new Date(tx.txDate) - new Date(candidates[s].dueDate)) / 86400000
);
if (dateDiff > 3) continue;
// 同一金額・日付範囲内で最も日付が近いSTLを選択
if (dateDiff < bestDateDiff) { bestDateDiff = dateDiff; bestIdx = s; }
}
if (bestIdx >= 0) {
// 確実マッチ
var m = candidates[bestIdx];
m.matched = true;
writeMatchResult_(importSheet, row, importIdx, 'MATCHED', m);
matchCount++;
} else {
// Pass 2: 金額±10% + 摘要キーワード一致で候補提案
var suggestIdx = -1;
var suggestBest = -1;
for (var s2 = 0; s2 < candidates.length; s2++) {
if (candidates[s2].matched) continue;
if (candidates[s2].ioType !== txType) continue;
var amtDiffRate = Math.abs(txAmt - candidates[s2].amount) / Math.abs(candidates[s2].amount || 1);
var amtScore = amtDiffRate <= 0.10 ? 40 : 0;
var nameScore = isMemoFuzzyMatch_(tx.memo, candidates[s2].vendor, candidates[s2].memo) ? 30 : 0;
var dateScore = 0;
var d2 = Math.abs((new Date(tx.txDate) - new Date(candidates[s2].dueDate)) / 86400000);
if (d2 <= 7) dateScore = 30;
else if (d2 <= 14) dateScore = 15;
var score = amtScore + nameScore + dateScore;
if (score > suggestBest) { suggestBest = score; suggestIdx = s2; }
}
if (suggestIdx >= 0 && suggestBest >= 40) {
writeMatchResult_(importSheet, row, importIdx, 'SUGGEST', candidates[suggestIdx]);
importSheet.getRange(row, 1, 1, importSheet.getMaxColumns()).setBackground('#FCE5CD');
suggestCount++;
} else {
importSheet.getRange(row, importIdx['処理結果'] + 1).setValue('UNMATCHED');
importSheet.getRange(row, 1, 1, importSheet.getMaxColumns()).setBackground('#FCE5CD');
unmatchCount++;
}
}
}
// 結果ダイアログ
ui.alert('🏦 マッチング確認 結果',
'✅ 確実マッチ: ' + matchCount + '件\n' +
'💡 候補提案: ' + suggestCount + '件\n' +
'⚠️ アンマッチ: ' + unmatchCount + '件\n\n' +
'確認後、「確認FLG」にチェックを入れて「🏦 銀行CSV消込実行」を実行してください。',
ui.ButtonSet.OK);
} catch (e) {
console.error(FUNC, e.message, e.stack);
SpreadsheetApp.getUi().alert('🚨 ' + FUNC + ' エラー', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
}
}
Step 2: applyBankSettlement() — 消込実行
/**
* メニュー: 🏦 銀行CSV消込実行
* 36_wrk_bank_import の確認FLG=TRUE 行について、33_wrk_bank のSTLを消込済にする。
*/
function applyBankSettlement() {
var FUNC = 'applyBankSettlement';
try {
var ss = getWebSpreadsheet_();
var ui = SpreadsheetApp.getUi();
var importSheet = Utils.getSheetByKey('WRK_BANK_IMPORT', '36_wrk_bank_import');
if (!importSheet) return ui.alert('🚨 36_wrk_bank_import が見つかりません。');
var importData = importSheet.getDataRange().getValues();
var importIdx = buildHeaderIndex_(importData[0]);
var bankSheet = Utils.getSheetByKey('WRK_BANK', '33_wrk_bank');
var bankData = bankSheet.getDataRange().getValues();
var bankIdx = buildHeaderIndex_(bankData[0]);
var bankMaxCol = bankData[0].length;
// STL_ID → 行番号マップ
var stlRowMap = {};
for (var b = 1; b < bankData.length; b++) {
var stlId = String(bankData[b][bankIdx['決済ID(STL)']]).trim();
if (stlId) stlRowMap[stlId] = b + 1;
}
var processCount = 0;
for (var i = 1; i < importData.length; i++) {
var row = importData[i];
var rowNum = i + 1;
// 確認FLG=TRUE かつ 処理結果=MATCHED or SUGGEST のみ処理
var confirmVal = row[importIdx['確認FLG']];
if (confirmVal !== true) continue;
var result = String(row[importIdx['処理結果']] || '').trim();
if (result === '消込済') continue;
if (result !== 'MATCHED' && result !== 'SUGGEST') continue;
var matchedStlId = String(row[importIdx['マッチ決済ID(STL)']] || '').trim();
if (!matchedStlId || !stlRowMap[matchedStlId]) continue;
var stlRow = stlRowMap[matchedStlId];
var txDate = row[importIdx['取引日']];
// 33_wrk_bank を更新
bankSheet.getRange(stlRow, bankIdx['決済ステータス'] + 1).setValue('消込済');
bankSheet.getRange(stlRow, bankIdx['決済日_実績'] + 1).setValue(txDate);
if (bankIdx['消込手段'] !== undefined) {
bankSheet.getRange(stlRow, bankIdx['消込手段'] + 1).setValue('銀行CSV');
}
// 金額差異がある場合(SUGGEST時)、差額を記録
var stlDataRow = bankData[stlRow - 1];
var stlAmt = Utils.parseAmt(stlDataRow[bankIdx['税込金額_決済']]);
var txAmt = (Number(row[importIdx['出金額']]) || 0) + (Number(row[importIdx['入金額']]) || 0);
var diff = txAmt - stlAmt;
if (Math.abs(diff) >= 1) {
bankSheet.getRange(stlRow, bankIdx['差額(手数料等)'] + 1).setValue(diff);
}
// ハイライト (薄緑)
bankSheet.getRange(stlRow, 1, 1, bankMaxCol).setBackground('#D9EAD3');
// 36タブの処理結果を更新
importSheet.getRange(rowNum, importIdx['処理結果'] + 1).setValue('消込済');
importSheet.getRange(rowNum, 1, 1, importSheet.getMaxColumns()).setBackground('#D9EAD3');
processCount++;
}
if (processCount === 0) {
ui.alert('🏦', '消込対象がありません。\n確認FLGにチェックが入っているMATCHED/SUGGEST行がありません。', ui.ButtonSet.OK);
} else {
ui.alert('🏦 消込実行 結果',
'🎉 ' + processCount + '件のSTLを消込済にしました。\n\nAction B を実行して仕訳を作成してください。',
ui.ButtonSet.OK);
}
} catch (e) {
console.error(FUNC, e.message, e.stack);
SpreadsheetApp.getUi().alert('🚨 ' + FUNC + ' エラー', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
}
}
Step 3: ヘルパー関数
/** 取込ハッシュの生成(重複取込防止用) */
function computeTxHash_(txDate, memo, deposit, withdrawal) {
var d = txDate instanceof Date ? Utilities.formatDate(txDate, Session.getScriptTimeZone(), 'yyyyMMdd') : String(txDate);
return d + '|' + String(memo).trim().substring(0, 30) + '|' + deposit + '|' + withdrawal;
}
/**
* 33_wrk_bank から口座一致の未処理STLを抽出
* extractCandidateStls_ (501_cc_importer.js) の銀行版
*/
function extractBankCandidateStls_(bankData, bankIdx, accountKey) {
var candidates = [];
for (var b = 1; b < bankData.length; b++) {
var flag = bankData[b][bankIdx['有効フラグ']];
if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
var stlStatus = String(bankData[b][bankIdx['決済ステータス']]).trim();
if (stlStatus !== '未処理') continue;
var acct = String(bankData[b][bankIdx['決済口座']]).trim();
if (acct !== accountKey) continue;
candidates.push({
rowIndex: b + 1,
stlId: String(bankData[b][bankIdx['決済ID(STL)']]).trim(),
dueDate: bankData[b][bankIdx['決済日_計画']],
vendor: String(bankData[b][bankIdx['取引先名']] || '').trim(),
amount: Math.abs(Utils.parseAmt(bankData[b][bankIdx['税込金額_決済']])),
memo: String(bankData[b][bankIdx['摘要']] || '').trim(),
ioType: String(bankData[b][bankIdx['入出金区分']] || '').trim(),
matched: false
});
}
return candidates;
}
/** 摘要のファジーマッチ(銀行摘要 ↔ STL取引先名・摘要) */
function isMemoFuzzyMatch_(bankMemo, stlVendor, stlMemo) {
var bm = normalizeMerchant_(bankMemo);
var sv = normalizeMerchant_(stlVendor);
var sm = normalizeMerchant_(stlMemo);
if (!bm) return false;
// 取引先名がCSV摘要に含まれるか
if (sv && sv.length >= 3 && bm.indexOf(sv) !== -1) return true;
if (sv && sv.length >= 3 && sv.indexOf(bm) !== -1) return true;
// STL摘要のキーワードがCSV摘要に含まれるか
if (sm) {
var tokens = sm.replace(/[_\-\/()()\[\]【】.,:;]/g, ' ')
.split(/\s+/).filter(function(t) { return t.length >= 2; });
for (var i = 0; i < tokens.length; i++) {
if (bm.indexOf(tokens[i]) !== -1) return true;
}
}
return false;
}
/** マッチ結果の書き込み(36タブ) */
function writeMatchResult_(sheet, rowNum, idx, resultLabel, stlCandidate) {
sheet.getRange(rowNum, idx['確認FLG'] + 1).setValue(resultLabel === 'MATCHED');
sheet.getRange(rowNum, idx['処理結果'] + 1).setValue(resultLabel);
sheet.getRange(rowNum, idx['マッチ決済ID(STL)'] + 1).setValue(stlCandidate.stlId);
sheet.getRange(rowNum, idx['STL決済日_計画'] + 1).setValue(stlCandidate.dueDate);
sheet.getRange(rowNum, idx['STL決済金額'] + 1).setValue(stlCandidate.amount);
sheet.getRange(rowNum, idx['STL取引先名'] + 1).setValue(stlCandidate.vendor);
sheet.getRange(rowNum, idx['STL摘要'] + 1).setValue(stlCandidate.memo);
if (resultLabel === 'MATCHED') {
sheet.getRange(rowNum, 1, 1, sheet.getMaxColumns()).setBackground('#FFFFFF');
}
}
Step 4: メニュー登録(101_sys_config.js)
onOpen_() の「🔍 消込・マッチング」メニューにセパレータ + 2項目を追加。
ui.createMenu('🔍 消込・マッチング')
.addItem('💳 クレカ明細マッチング確認', 'importCcStatement')
.addItem('💳 クレカ消込実行 (確認FLG=TRUE)', 'applyCcSettlement')
.addSeparator()
.addItem('📄 領収書PDFの読み込み (Drive)', 'importReceiptPdfs')
.addItem('🧾 領収書マッチング確認', 'importReceiptStatement')
.addItem('🧾 領収書消込実行 (確認FLG=TRUE)', 'applyReceiptSettlement')
.addSeparator()
.addItem('🏦 銀行CSV マッチング確認', 'importBankStatement') // ← 追加
.addItem('🏦 銀行CSV消込実行 (確認FLG=TRUE)', 'applyBankSettlement') // ← 追加
.addToUi();
影響範囲
| 変更対象 | 変更内容 | 変更量 |
|---|---|---|
500_import/502_bank_importer.js | 新規作成 | ~250行 |
100_config/101_sys_config.js | DDL追加 + メニュー追加 | ~10行 |
- 既存ファイルへの変更は
101_sys_config.jsのみ 501_cc_importer.jsは変更しない(共通化リファクタリングは別案件)33_wrk_bankのデータ構造は変更なし(既存列を更新するのみ)
注意事項
normalizeMerchant_()は501_cc_importer.jsで定義済み。GASのグローバルスコープで参照可能なため、502_bank_importer.jsから直接呼び出せる。再定義不要- CSVの文字コード: 銀行CSVはShift-JIS (CP932) が多い。スプレッドシートに貼り付ける時点でUTF-8に変換されるため、GAS側での文字コード処理は不要
確認FLGの自動TRUE設定: Pass 1 (MATCHED) では確認FLG=TRUEを自動セット。Pass 2 (SUGGEST) では確認FLG=FALSEでユーザー判断を要求getWebSpreadsheet_()の使用:501_cc_importer.jsと同じくgetWebSpreadsheet_()を使用してスプレッドシートを取得する(Env経由のID参照)- STL金額の絶対値比較: STLの
税込金額_決済は正の数で格納されている。入金(売上回収)・出金(経費支払)の区分は入出金区分列で判定する - 銀行アダプターのCSV列マッピングは仮定: 実際のCSVフォーマットが異なる場合、アダプターの
parse関数を調整する必要がある。初回実装時に実際のCSVファイルを確認すること
エッジケース
| 条件 | 処理 | 理由 |
|---|---|---|
| 1つの銀行明細に複数STLがマッチ可能 | 金額完全一致 + 日付最近のSTLを1つだけマッチ。残りはSUGGEST | 1:1マッチを原則とし、誤マッチを防止 |
| 1つのSTLに複数の銀行明細がマッチ(分割入金) | 初回マッチ時に matched=true でロック。2件目以降はSUGGESTまたはUNMATCHED | 分割入金は手動対応。自動マッチの信頼性を優先 |
| 同じCSVの重複取込 | 取込ハッシュ(日付+摘要先頭30文字+入出金額)で既存データと照合。重複行は SKIP:重複 | 同一CSVの再貼り付けによるデータ重複を防止 |
| 金額ゼロの銀行明細 | SKIP:金額ゼロ | 利息計算の端数や残高照会など、実質的な入出金がない行 |
| マッチ候補なしの銀行明細 | UNMATCHED(オレンジ背景)。STLとして新規追加はしない | 不明な入出金を自動でSTL化するとデータ品質が低下する。手動対応を促す |
| 入金明細(売上回収) | STLの 入出金区分=入金 とマッチ | 出金と同じマッチングロジック。入出金区分の一致で絞り込み |
| 銀行の振込手数料が差し引かれた入金 | 金額不一致でPass 1不成立 → Pass 2で候補提案(金額±10%以内) | 差額は applyBankSettlement で 差額(手数料等) に記録 |
実データ検証(MCP でのデータ確認が必要な場合)
| 確認項目 | 確認方法 | 理由 |
|---|---|---|
33_wrk_bank の 決済口座 の実際の値 | MCP で33タブの決済口座列のユニーク値を取得 | アダプターの accountKey を実データに合わせる必要がある |
| 福井銀行CSVの実際の列構成 | 実際のCSVファイルを確認 | アダプターのparse関数の列マッピングを確定する |
| SBIネット銀行CSVの実際の列構成 | 実際のCSVファイルを確認 | 同上 |
| 33_wrk_bank の未処理STLの件数と金額分布 | MCP で 決済ステータス=未処理 のSTLを集計 | マッチング精度の事前評価 |
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| CLAUDE.md | 33_wrk_bank の集計条件: 消込済 かつ 自動仕訳JNL_ID あり |
| CLAUDE.md | MCP add_rows は使わない。update_cells で末尾行に書き込む |
| 501_cc_importer.js | 2段階マッチ + 確認FLGパターンの実装参照 |
| dev_mas-077_settlement_date_sync.md | STL→INVの決済日転記(Action B連動) |
| dev_mas-158_hc_saas_import.md | SaaSデータ取込の仕様書(類似アーキテクチャ) |
人間が検討すべき事項
| # | 項目 | 詳細 |
|---|---|---|
| 1 | CSVフォーマットの銀行別差異 | 福井銀行・SBIネット銀行の実際のCSV列構成を確認し、アダプターの parse 関数を確定する必要がある。TODO_future.md から転記 |
| 2 | ステージングシートの新設 | 36_wrk_bank_import をDDLに追加。番号36は使用可能か確認(既存は31-35)。TODO_future.md から転記 |
| 3 | マッチ候補なし明細のSTL自動追加 | 本仕様ではUNMATCHED明細のSTL自動追加は行わない(データ品質優先)。運用でニーズがあれば追加検討 |
| 4 | 501_cc_importer.js との共通化リファクタリング | extractCandidateStls_, ensureMatchCols_, normalizeMerchant_ 等の共通ヘルパーを共有モジュールに抽出する案件。MAS-145のスコープ外として、別途 MAS-146 またはリファクタリング案件で対応 |
| 5 | 日付許容誤差の調整 | Pass 1 で ±3日、Pass 2 で ±14日を設定。運用実績に基づいて調整が必要になる可能性がある |
| 6 | 振込手数料の自動判定 | 入金時に振込手数料が差し引かれるケース。差額を自動で「支払手数料」科目に割り当てるかは別案件(MAS-122 銀行残高照合と連動) |
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-145「法人口座CSV取込・自動マッチング」を実装してください。
## 実行前タスク
以下のファイルを読み込んでください:
1. `500_import/501_cc_importer.js` — 2段階マッチ + 確認FLGパターンの参照実装。以下を重点確認:
- `importCcStatement()`: Pass 1/Pass 2 のマッチングロジック
- `applyCcSettlement()`: 確認FLG=TRUE での消込実行パターン
- `MATCH_EXTRA_HEADERS`: マッチ結果列のヘッダー構成
- `extractCandidateStls_()`: 候補STL抽出のフィルタ条件
- `normalizeMerchant_()`: 全角→半角正規化(502からも呼び出す)
2. `000_infra/003_contracts.js` — BankTxDTO の列構成
3. `000_infra/002_constants.js` — ID発番ルール、RPA_DEFAULTS の決済口座名
4. `100_config/101_sys_config.js` — DDL定義(WRK_BANK の headers)とメニュー登録箇所
5. `200_data/202_repository.js` — BankTxRepository の append パターン
6. `CLAUDE.md` — コーディング規約
7. `docs/dev/dev_mas-145_bank_csv_import.md` — 本仕様書
## 修正対象ファイル
- `500_import/502_bank_importer.js` — **新規作成**
- `100_config/101_sys_config.js` — DDL追加 + メニュー追加
## 実装内容
### A: `502_bank_importer.js` の新規作成
仕様書の「修正方針」セクションに記載されたコードを基に実装。
主要関数:
- `importBankStatement()` — Step 1: CSVパース + 2段階マッチング確認
- `applyBankSettlement()` — Step 2: 確認FLG=TRUE の行を消込実行
- `computeTxHash_()` — 重複取込防止用ハッシュ生成
- `extractBankCandidateStls_()` — 33_wrk_bankから口座一致の未処理STLを抽出
- `isMemoFuzzyMatch_()` — 銀行摘要のファジーマッチ
- `writeMatchResult_()` — マッチ結果の書き込み
- `BANK_ADAPTERS` — 銀行別CSVパーサー配列
### B: `101_sys_config.js` の修正
1. setupAllSchemas の SCHEMAS に `WRK_BANK_IMPORT` を追加
2. configDefaults に `WRK_BANK_IMPORT` 行を追加
3. 「🔍 消込・マッチング」メニューに銀行CSV用の2項目を追加
## 制約
- `501_cc_importer.js` は変更しない
- `33_wrk_bank` のスキーマは変更しない(既存列を更新するのみ)
- `normalizeMerchant_()` は `501_cc_importer.js` で定義済みのため再定義しない
- 銀行アダプターのCSV列マッピングは暫定。実際のCSVで調整が必要
## 実データ検証
- 33_wrk_bank の `決済口座` 列のユニーク値を確認し、アダプターの `accountKey` を合わせる
- 実際の銀行CSVの列構成が仕様と異なる場合、アダプターの `parse` を調整
## 動作確認
`npm run push:dev` 後:
1. setupAllSchemas を実行 → `36_wrk_bank_import` タブが作成されること
2. 36タブに銀行CSVデータを貼り付け
3. メニュー「🔍 消込・マッチング」→「🏦 銀行CSV マッチング確認」を実行
4. 銀行選択ダイアログで「1: 福井銀行」を選択
5. **検証**: 36タブにマッチ結果列が表示されること(MATCHED/SUGGEST/UNMATCHED)
6. MATCHED行の確認FLGがTRUEであること。SUGGEST行はFALSEであること
7. 確認FLG=TRUEの行を確認後、「🏦 銀行CSV消込実行」を実行
8. **検証**: 33_wrk_bank の対象STLが `消込済` + `決済日_実績` + `消込手段=銀行CSV` になること
9. Action B を実行 → 仕訳が正常に生成されること
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| ファイル読み込み・構造理解 | あり | 501パターンの把握、BankTxDTOの列構成確認 |
| 銀行アダプター実装 | なし | 仕様書で列マッピング定義済み(暫定) |
| マッチングロジック | あり | Pass1/Pass2の条件調整、エッジケース処理 |
| 消込実行 | なし | 501のapplyCcSettlementパターンの踏襲 |
| DDL・メニュー追加 | なし | 定型作業 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 501パターンの分析、アーキテクチャ決定(ステージング vs Drive)、エッジケース網羅に高い推論力が必要 |
| 実装 | Claude Sonnet 4.6 | 仕様書でコード構造は定義済みだが、銀行アダプターの実CSV対応とマッチングの微調整に中程度の判断力が必要 |
| 動作確認 | ユーザー手動 | 銀行CSVの貼り付け → マッチング確認 → 消込実行の一連の手動操作が必要 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-16 | 初版作成 |