はじめに
- 毎月メールで届く請求書PDFを開いて、発行元・金額・支払期限をスプレッドシートに手で打ち込んでいる
- 取引先ごとにレイアウトが違うので、従来のOCRや正規表現では項目をうまく取り出せない
- 転記ミスや二重計上に後から気づき、確認作業にまた時間がかかる
この記事では、Gmailに届いた請求書・領収書(PDF・画像)をGemini APIで読み取り、スプレッドシートに自動で転記する仕組みをGASで作ります。
「AIに読ませて終わり」にはしません。金額の整合性チェック、登録番号の形式チェック、二重登録の検知、「要確認」フラグまで入れた、実務で回せる形にします。
完成イメージ
1時間ごとにGmailを確認し、請求書の添付ファイルを見つけたら下のように1行ずつ追記します。
| 処理日時 | ステータス | 確認理由 | 書類種別 | 発行元 | 登録番号 | 請求書番号 | 発行日 | 支払期限 | 小計 | 消費税 | 合計 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026/10/28 10:00 | OK | 請求書 | 株式会社サンプル商事 | T1234567890123 | INV-0012 | 2026/10/25 | 2026/11/30 | 50000 | 5000 | 55000 | |
| 2026/10/28 10:00 | 要確認 | 小計+消費税が合計と一致しない | 請求書 | 〇〇デザイン | T9876543210987 | 0045 | 2026/10/26 | 2026/11/15 | 30000 | 3000 | 32000 |
人が見るのは「要確認」の行だけで済みます。
処理の流れは次のとおりです。
Gmail検索(未処理の請求書メール)
↓
添付ファイルを取得(PDF / JPEG / PNG)
↓
ファイルのハッシュで登録済みかチェック → 登録済みならスキップ
↓
Gemini API に送信 → JSONで項目を受け取る
↓
検証(金額の整合性・登録番号・日付形式・二重請求)
↓
スプレッドシートに追記(問題があれば「要確認」)
↓
スレッドに処理済みラベルを付ける
準備 / 前提
| 必要なもの | 備考 |
|---|---|
| Googleアカウント | Gmail・スプレッドシート・GASを使う |
| Gemini APIキー | Google AI Studio から発行 |
| 転記先のスプレッドシート | 新規作成でOK |
⚠️ 業務の書類を扱う前に:無料枠と有料枠の違い
Gemini APIには無料枠がありますが、無料枠では送ったデータがGoogleのサービス改善に使われる場合があります。請求書には取引先名や金額が載っているので、業務で使うなら課金を有効にした有料枠を推奨します。
| 無料枠 | 有料枠 | |
|---|---|---|
| 送信データのサービス改善への利用 | あり得る | されない |
| レート制限 | 低い | 高い |
| 費用 | 0円 | 従量課金 |
※ 執筆時点の規約に基づく整理です。利用前に必ず最新の利用規約と料金ページを確認してください。
費用について:請求書1〜2ページ程度なら、Flash系モデルでの1件あたりのコストはかなり小さく収まります。ただし料金表は頻繁に改定されるので、月の処理件数 × 1件あたりのトークン数で事前に試算しておきましょう。
APIキーをスクリプトプロパティに保存する
APIキーはコードに直接書かず、スクリプトプロパティに保存します。
- スプレッドシートを開き、「拡張機能」→「Apps Script」
- 左メニューの「⚙ プロジェクトの設定」→「スクリプト プロパティ」→「プロパティを追加」
- プロパティ名
GEMINI_API_KEY、値に発行したAPIキーを入れて保存
Step 1:設定とシートの準備
以降のコードはすべて同じ コード.gs に順番に貼り付けてください。
// ===== 設定 =====
const CONFIG = {
SHEET_NAME: '請求書', // ★転記先のシート名
// ★請求書メールを探すGmail検索条件(処理済み・エラーのラベルは自動で除外される)
GMAIL_QUERY: 'has:attachment (請求書 OR 領収書 OR invoice) newer_than:30d',
LABEL_DONE: 'invoice-done', // 処理済みスレッドに付けるラベル
LABEL_ERROR: 'invoice-error', // エラーが出たスレッドに付けるラベル
DRIVE_FOLDER_ID: '', // ★添付を保存するDriveフォルダID(空なら保存しない)
MODEL: 'gemini-flash-latest', // ★使用するモデル(Step 2で確認)
MAX_THREADS: 10, // 1回の実行で処理するスレッド数の上限
MAX_FILE_MB: 15, // これより大きい添付はエラー扱い
MAX_RUNTIME_MS: 4.5 * 60 * 1000, // 実行時間の上限(GASの6分制限より手前で止める)
};
const HEADERS = [
'処理日時', 'ステータス', '確認理由',
'書類種別', '発行元', '登録番号', '請求書番号', '発行日', '支払期限',
'小計', '消費税', '合計', '通貨',
'送信元', '件名', 'ファイル名', 'ファイルハッシュ', 'メールリンク', '保存先',
];
// 先頭の0が消えないよう文字列として扱う列
const TEXT_COLUMNS = ['登録番号', '請求書番号', 'ファイルハッシュ'];
function setupSheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(CONFIG.SHEET_NAME) || ss.insertSheet(CONFIG.SHEET_NAME);
sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]).setFontWeight('bold');
sheet.setFrozenRows(1);
getOrCreateLabel_(CONFIG.LABEL_DONE);
getOrCreateLabel_(CONFIG.LABEL_ERROR);
console.log('シートとラベルを準備しました');
}
function getSheet_() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) throw new Error(`シート「${CONFIG.SHEET_NAME}」がありません。setupSheet を実行してください`);
return sheet;
}
function getOrCreateLabel_(name) {
return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
}
function getApiKey_() {
const key = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
if (!key) throw new Error('スクリプトプロパティ GEMINI_API_KEY が設定されていません');
return key;
}
貼り付けたら、関数選択で setupSheet を選んで実行します。初回は権限の承認画面が出るので許可してください。シートにヘッダー行ができ、Gmailにラベルが2つ作られます。
Step 2:使えるモデルを確認する
Geminiのモデルは入れ替わりが早く、古いモデル名は順次提供終了になります。記事のコードをそのまま使って 404 が出るのを避けるため、まず自分のAPIキーで使えるモデルを確認します。
function listModels() {
const res = UrlFetchApp.fetch('https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000', {
headers: { 'x-goog-api-key': getApiKey_() },
muteHttpExceptions: true,
});
if (res.getResponseCode() !== 200) {
throw new Error(`モデル一覧の取得に失敗 (${res.getResponseCode()}): ${res.getContentText()}`);
}
const models = JSON.parse(res.getContentText()).models || [];
models
.filter(m => (m.supportedGenerationMethods || []).includes('generateContent'))
.forEach(m => console.log(`${m.name.replace('models/', '')} / ${m.displayName}`));
}
実行ログに一覧が出ます。CONFIG.MODEL の初期値 gemini-flash-latest は「最新のFlashモデル」を指すエイリアスです。
- まず動かしてみる段階 → エイリアスのままでOK
- 本番運用 → 一覧にあるバージョン付きのモデル名に固定するのがおすすめ(モデルが勝手に切り替わって出力の傾向が変わるのを防ぐため)
Step 3:Gemini APIで1件読み取ってみる
いきなりGmailと連携せず、Driveに置いたPDF1件で読み取り精度を確認します。
ポイントは構造化出力(responseMimeType: 'application/json' + responseJsonSchema)です。返ってくるJSONの形をスキーマで固定できるので、「AIの返答から正規表現で値を抜き出す」といった不安定な処理が要りません。
const EXTRACT_PROMPT = `添付の書類から請求情報を抽出してください。
ルール:
- 書類に書かれていない項目は出力しない(推測で埋めない)
- 金額は数値のみ(カンマ・通貨記号は付けない)
- subtotal は税抜金額。税込金額しか書かれていない場合(内税表記)は subtotal を出力しない
- tax は消費税の合計額(8%と10%が混在する場合は合算)
- total は税込の合計金額
- 日付は YYYY-MM-DD 形式。和暦は西暦に変換する
- registration_number は適格請求書発行事業者の登録番号(T + 13桁の数字)
- currency は ISO 4217 の3文字コード(日本円なら JPY)
- 読み取りに自信がない項目がある場合は needs_review を true にし、理由を review_reason に日本語で書く`;
const INVOICE_SCHEMA = {
type: 'object',
properties: {
document_type: { type: 'string', enum: ['請求書', '領収書', 'その他'], description: '書類の種類' },
vendor_name: { type: 'string', description: '発行元の会社名・店名' },
registration_number: { type: 'string', description: '登録番号(T + 13桁)' },
invoice_number: { type: 'string', description: '請求書番号・領収書番号' },
issue_date: { type: 'string', description: '発行日(YYYY-MM-DD)' },
due_date: { type: 'string', description: '支払期限(YYYY-MM-DD)' },
subtotal: { type: 'number', description: '税抜の小計' },
tax: { type: 'number', description: '消費税額の合計' },
total: { type: 'number', description: '税込の合計金額' },
currency: { type: 'string', description: '通貨コード' },
needs_review: { type: 'boolean', description: '人の確認が必要か' },
review_reason: { type: 'string', description: '確認が必要な理由' },
},
required: ['document_type', 'needs_review'],
};
function callGemini_(bytes, mimeType) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${CONFIG.MODEL}:generateContent`;
const payload = {
contents: [{
parts: [
{ inlineData: { mimeType: mimeType, data: Utilities.base64Encode(bytes) } },
{ text: EXTRACT_PROMPT },
],
}],
generationConfig: {
responseMimeType: 'application/json',
responseJsonSchema: INVOICE_SCHEMA,
},
};
const options = {
method: 'post',
contentType: 'application/json',
headers: { 'x-goog-api-key': getApiKey_() },
payload: JSON.stringify(payload),
muteHttpExceptions: true,
};
const maxAttempts = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = UrlFetchApp.fetch(url, options);
const code = res.getResponseCode();
if (code === 200) {
const candidate = (JSON.parse(res.getContentText()).candidates || [])[0];
if (!candidate) throw new Error('応答に候補がありません: ' + res.getContentText().slice(0, 500));
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
throw new Error(`生成が途中で終了しました(finishReason: ${candidate.finishReason})`);
}
const text = ((candidate.content && candidate.content.parts) || [])
.filter(p => p.text && !p.thought)
.map(p => p.text)
.join('');
return JSON.parse(text);
}
// レート制限・サーバーエラーは待ってからリトライ
if ((code === 429 || code >= 500) && attempt < maxAttempts) {
console.warn(`Gemini API ${code}。${attempt * 10}秒後にリトライします`);
Utilities.sleep(attempt * 10 * 1000);
continue;
}
throw new Error(`Gemini API エラー (${code}): ${res.getContentText().slice(0, 500)}`);
}
}
function validate_(data) {
const reasons = [];
if (data.needs_review) reasons.push(data.review_reason || 'AIが要確認と判定');
if (data.document_type === 'その他') reasons.push('請求書・領収書ではない可能性');
if (typeof data.total !== 'number') reasons.push('合計金額が読み取れない');
const amounts = [data.subtotal, data.tax, data.total];
if (amounts.every(v => typeof v === 'number') && Math.abs(data.subtotal + data.tax - data.total) > 1) {
reasons.push('小計+消費税が合計と一致しない');
}
if (data.registration_number && !/^T\d{13}$/.test(data.registration_number)) {
reasons.push('登録番号の形式が不正');
}
[['issue_date', '発行日'], ['due_date', '支払期限']].forEach(([key, label]) => {
if (data[key] && !/^\d{4}-\d{2}-\d{2}$/.test(data[key])) reasons.push(`${label}の形式が不正`);
});
return reasons;
}
function testExtract() {
const FILE_ID = 'ここにDriveのファイルIDを入れる'; // ★テスト用PDFのファイルID
const blob = DriveApp.getFileById(FILE_ID).getBlob();
const data = callGemini_(blob.getBytes(), blob.getContentType());
console.log(JSON.stringify(data, null, 2));
const reasons = validate_(data);
console.log(reasons.length ? '要確認: ' + reasons.join(' / ') : '検証OK');
}
手元の請求書PDFをGoogleドライブにアップロードし、URLの /d/ と /view の間の文字列(ファイルID)を FILE_ID に入れて testExtract を実行します。
{
"document_type": "請求書",
"vendor_name": "株式会社サンプル商事",
"registration_number": "T1234567890123",
"invoice_number": "INV-0012",
"issue_date": "2026-10-25",
"due_date": "2026-11-30",
"subtotal": 50000,
"tax": 5000,
"total": 55000,
"currency": "JPY",
"needs_review": false
}
このような結果が出れば成功です。取引先ごとに数枚ずつ試して、読み取りの傾向を確認しておきましょう。
なぜ「AIの判定」と「コードの検証」を両方入れるのか
AIの needs_review は「AI自身が自信がない」ケースしか拾えません。読み間違えたのに自信満々というケースはコード側の検証で拾います。
| チェック | 拾えるミスの例 |
|---|---|
| 小計+消費税=合計 | 桁の読み違い、8%/10%の取り違え |
| 登録番号の形式 | 桁数不足、Tの欠落 |
| 日付形式 | 和暦の変換ミス |
| 二重登録(Step 4) | 同じ請求書の再送、別PDFでの二重請求 |
Step 4:Gmailの添付を処理して転記する
いよいよ本体です。Gmailの検索や添付ファイルの扱いの基本は「GASでGmailを自動処理する」で解説しています。
function processInvoiceEmails() {
// 多重起動の防止(前回の実行が長引いた場合)
const lock = LockService.getScriptLock();
if (!lock.tryLock(10 * 1000)) {
console.log('前回の実行が続いているためスキップします');
return;
}
const startTime = Date.now();
try {
const sheet = getSheet_();
const existing = loadExisting_(sheet);
const labelDone = getOrCreateLabel_(CONFIG.LABEL_DONE);
const labelError = getOrCreateLabel_(CONFIG.LABEL_ERROR);
const query = `${CONFIG.GMAIL_QUERY} -label:${CONFIG.LABEL_DONE} -label:${CONFIG.LABEL_ERROR}`;
const threads = GmailApp.search(query, 0, CONFIG.MAX_THREADS);
console.log(`対象スレッド: ${threads.length}件`);
for (const thread of threads) {
if (Date.now() - startTime > CONFIG.MAX_RUNTIME_MS) {
console.log('実行時間の上限に近づいたため、残りは次回に回します');
break;
}
let hasError = false;
for (const message of thread.getMessages()) {
for (const att of message.getAttachments({ includeInlineImages: false })) {
try {
processAttachment_(sheet, existing, message, att);
} catch (e) {
hasError = true;
console.error(`処理失敗: ${message.getSubject()} / ${att.getName()} / ${e.message}`);
}
}
}
thread.addLabel(hasError ? labelError : labelDone);
}
} finally {
lock.releaseLock();
}
}
function processAttachment_(sheet, existing, message, att) {
const mimeType = detectMime_(att);
if (!mimeType) return; // Word・Excelなど対象外の形式は無視
const sizeMb = att.getSize() / 1024 / 1024;
if (sizeMb > CONFIG.MAX_FILE_MB) {
throw new Error(`ファイルサイズが大きすぎます(${sizeMb.toFixed(1)}MB)`);
}
// 同じファイルは二度処理しない(冪等性)
const bytes = att.getBytes();
const hash = computeHash_(bytes);
if (existing.hashes.has(hash)) {
console.log(`登録済みのためスキップ: ${att.getName()}`);
return;
}
const data = callGemini_(bytes, mimeType);
if (data.registration_number) {
data.registration_number = data.registration_number.replace(/[\s-]/g, '');
}
const reasons = validate_(data);
// ファイルは別でも、同じ発行元・同じ番号なら二重請求を疑う
const invoiceKey = makeInvoiceKey_(data.vendor_name, data.registration_number, data.invoice_number);
if (invoiceKey && existing.invoiceKeys.has(invoiceKey)) {
reasons.push('同じ発行元・番号の書類が登録済み(二重請求の可能性)');
}
let savedUrl = '';
if (CONFIG.DRIVE_FOLDER_ID) {
savedUrl = DriveApp.getFolderById(CONFIG.DRIVE_FOLDER_ID).createFile(att.copyBlob()).getUrl();
}
const record = {
'処理日時': new Date(),
'ステータス': reasons.length ? '要確認' : 'OK',
'確認理由': reasons.join(' / '),
'書類種別': data.document_type,
'発行元': data.vendor_name,
'登録番号': data.registration_number,
'請求書番号': data.invoice_number,
'発行日': data.issue_date,
'支払期限': data.due_date,
'小計': data.subtotal,
'消費税': data.tax,
'合計': data.total,
'通貨': data.currency,
'送信元': message.getFrom(),
'件名': message.getSubject(),
'ファイル名': att.getName(),
'ファイルハッシュ': hash,
'メールリンク': `https://mail.google.com/mail/u/0/#all/${message.getId()}`,
'保存先': savedUrl,
};
writeRow_(sheet, record);
existing.hashes.add(hash);
if (invoiceKey) existing.invoiceKeys.add(invoiceKey);
console.log(`登録: ${att.getName()}(${record['ステータス']})`);
}
function detectMime_(att) {
const type = att.getContentType();
const name = att.getName().toLowerCase();
// PDFが application/octet-stream で届くことがあるため拡張子でも判定する
if (type === 'application/pdf' || name.endsWith('.pdf')) return 'application/pdf';
if (type === 'image/jpeg' || /\.jpe?g$/.test(name)) return 'image/jpeg';
if (type === 'image/png' || name.endsWith('.png')) return 'image/png';
return null;
}
function computeHash_(bytes) {
const digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, bytes);
return digest.map(b => ((b + 256) % 256).toString(16).padStart(2, '0')).join('');
}
function makeInvoiceKey_(vendor, regNo, invoiceNo) {
if (!invoiceNo) return '';
return `${regNo || vendor || ''}|${String(invoiceNo).trim()}`;
}
function loadExisting_(sheet) {
const hashes = new Set();
const invoiceKeys = new Set();
const lastRow = sheet.getLastRow();
if (lastRow >= 2) {
const col = name => HEADERS.indexOf(name);
sheet.getRange(2, 1, lastRow - 1, HEADERS.length).getValues().forEach(r => {
if (r[col('ファイルハッシュ')]) hashes.add(String(r[col('ファイルハッシュ')]));
const key = makeInvoiceKey_(r[col('発行元')], r[col('登録番号')], r[col('請求書番号')]);
if (key) invoiceKeys.add(key);
});
}
return { hashes, invoiceKeys };
}
function writeRow_(sheet, record) {
const row = sheet.getLastRow() + 1;
TEXT_COLUMNS.forEach(name => {
sheet.getRange(row, HEADERS.indexOf(name) + 1).setNumberFormat('@');
});
const values = HEADERS.map(h => (record[h] === undefined || record[h] === null) ? '' : record[h]);
sheet.getRange(row, 1, 1, HEADERS.length).setValues([values]);
}
processInvoiceEmails を手動で実行してみましょう。追加の権限(Gmail・Drive)を求められたら許可します。
この設計で押さえているポイントは3つです。
- ハッシュで冪等性を担保:途中で止まって再実行しても、同じファイルが二重に登録されない
-
エラーはスレッド単位でラベル分け:
invoice-errorラベルで絞り込めば、失敗したメールだけ確認できる -
1件の失敗で全体を止めない:添付ごとに
try/catchしているので、壊れたPDFが1つあっても他は処理される
Step 5:トリガーで定期実行する
function createTrigger() {
ScriptApp.getProjectTriggers()
.filter(t => t.getHandlerFunction() === 'processInvoiceEmails')
.forEach(t => ScriptApp.deleteTrigger(t));
ScriptApp.newTrigger('processInvoiceEmails').timeBased().everyHours(1).create();
console.log('1時間ごとのトリガーを設定しました');
}
createTrigger を1回だけ実行すれば完了です。何度実行しても既存のトリガーを消してから作り直すので、重複はしません。
実用例
1. 月次締めの確認作業を「要確認だけ」に絞る
シートのヘッダー行でフィルタを作り、ステータスを「要確認」で絞り込みます。メールリンク列から元のメールに飛んで原本と照合し、修正したらステータスを「OK」に書き換える運用にすれば、全件を目で追う作業がなくなります。
確認が終わったデータは、会計ソフトのCSVインポート形式に合わせて書き出せば、仕訳入力まで手作業なしでつなげられます。
2. 支払期限が近い請求書を毎朝メールで通知する
転記したデータを使って、支払い漏れを防ぎます。
function notifyDueSoon() {
const DAYS_BEFORE = 3; // ★何日前から通知するか
const sheet = getSheet_();
const lastRow = sheet.getLastRow();
if (lastRow < 2) return;
const col = name => HEADERS.indexOf(name);
const today = new Date();
today.setHours(0, 0, 0, 0);
const limit = new Date(today.getTime() + DAYS_BEFORE * 24 * 60 * 60 * 1000);
const lines = sheet.getRange(2, 1, lastRow - 1, HEADERS.length).getValues()
.filter(r => {
const v = r[col('支払期限')];
if (!v) return false;
const due = v instanceof Date ? v : new Date(v);
return !isNaN(due) && due >= today && due <= limit;
})
.map(r => {
const v = r[col('支払期限')];
const due = Utilities.formatDate(v instanceof Date ? v : new Date(v), 'Asia/Tokyo', 'yyyy/MM/dd');
return `・${due} ${r[col('発行元')]} ${r[col('合計')]} ${r[col('通貨')]}`;
});
if (lines.length === 0) return;
MailApp.sendEmail(
Session.getEffectiveUser().getEmail(),
`【支払期限${DAYS_BEFORE}日以内】${lines.length}件`,
lines.join('\n')
);
}
トリガーで「日付ベース・毎朝8〜9時」に設定すれば、毎朝リマインドが届きます。
つまずきやすいポイント
| 症状 | 原因 | 対処 |
|---|---|---|
404 / models/xxx is not found
|
モデル名の誤り、またはモデルの提供終了 |
listModels で使えるモデルを確認し、CONFIG.MODEL を変更 |
429 が頻発する |
レート制限(特に無料枠) |
MAX_THREADS を減らす。リトライは組み込み済みだが、継続するなら有料枠へ |
400 でペイロードサイズのエラー |
添付が大きすぎる |
MAX_FILE_MB を下げる。大きなPDFは Files API 経由でのアップロードを検討 |
finishReason: MAX_TOKENS |
出力が途中で切れた | 明細行まで抽出しようとしていないか確認。項目を絞る |
JSON.parse でエラー |
応答がJSONとして不完全 | 上の finishReason を確認。ログに出る応答本文を見る |
| 実行が6分で強制終了 | GASの実行時間制限 |
MAX_RUNTIME_MS で手前で止めて次回に回す設計になっている。MAX_THREADS も調整 |
| 請求書以外のメールまで拾う | 検索条件が広すぎる |
from:billing@example.com のように送信元で絞る |
| パスワード付きPDFが失敗する | Geminiが中身を読めない |
invoice-error ラベルが付くので手動対応。取引先にパスワードなしでの送付を相談 |
| 請求書番号の先頭の0が消える | 数値として扱われた |
TEXT_COLUMNS で文字列書式にしている。列を追加したらここにも追加 |
| 同じスレッドに後から届いた請求書が処理されない | スレッドに処理済みラベルが付いている | 対象スレッドのラベルを外せば再処理される(ハッシュで重複は防がれる) |
エラーに気づける仕組みも入れておく
トリガー実行のエラーは、放っておくと誰も気づきません。invoice-error ラベルの件数を定期的にチェックする、失敗時にSlackやメールへ通知するなどの仕組みを入れておくと安心です。具体的な作り方は「自動化スクリプトが止まっても誰も気づかない問題:エラー通知と死活監視の作り方」にまとめています。
まとめ
- Gemini APIはPDF・画像を直接読めるので、レイアウトが取引先ごとに違う請求書でも項目を抽出できる
- **構造化出力(
responseJsonSchema)**を使えば、返ってくるJSONの形を固定できる - AIの自己申告(
needs_review)だけに頼らず、金額の整合性・登録番号・日付形式をコードで検証する - ファイルのハッシュで冪等性を確保し、発行元+番号で二重請求を検知する
- モデル名は入れ替わりが早いので、
listModelsで確認してから固定する - 業務の書類を扱うなら、データの扱いを確認したうえで有料枠を使う
帳票・書類作成の自動化のご相談
「毎月の請求書処理を自動化したい」「AIで書類を読み取って既存のシステムに取り込みたい」などのご相談を受け付けています。