【GAS】スプレッドシートから請求書PDFを一括作成してGmailで送る(インボイス対応の消費税計算つき)
毎月の請求書づくりを Google Apps Script(GAS)で自動化しました。スプレッドシートに明細を入れてメニューを押すと、次の処理がまとめて終わります。
- 取引先ごとに請求書シートを組み立てる
- PDF に変換して Google ドライブの月別フォルダへ保存する
- 添付付きのメールを Gmail の下書きに作る(またはそのまま送る)
この記事では、実装で引っかかりやすかった次の4点を中心に解説します。
- シートを PDF にする方法(
getAsでは余白やサイズを指定できない) - PDF エクスポートの 429 エラー対策
- インボイス制度に沿った消費税の計算と、浮動小数点の落とし穴
- Gmail の下書き作成と本文テンプレートの差し込み
全体の構成
| シート | 役割 |
|---|---|
| 設定 | 自社名、登録番号、振込先、メール文面など |
| 取引先 | 取引先ID、会社名、担当者、メールアドレス |
| 明細 | 請求月、取引先ID、品目、数量、単価、税率 |
| 発行履歴 | 発行した請求書番号、金額、PDF の URL |
処理の流れは次のとおりです。
明細を請求月で絞り込み → 取引先IDごとにまとめる
→ 作業用シートに請求書を組み立てる → PDF に変換 → ドライブに保存
→ Gmail で下書き作成 → 作業用シートを削除 → 履歴に追記
1. シートを PDF にする
SpreadsheetApp.getActive().getAs('application/pdf') ではスプレッドシート全体が PDF になり、用紙サイズや余白も選べません。特定のシートだけを A4・余白指定で出力したい場合は、エクスポート用の URL に OAuth トークンを付けて取得します。
function exportSheetAsPdf_(ss, sh) {
const url = 'https://docs.google.com/spreadsheets/d/' + ss.getId() + '/export' +
'?format=pdf&gid=' + sh.getSheetId() +
'&size=A4&portrait=true&fitw=true&gridlines=false' +
'&printtitle=false&sheetnames=false&pagenum=UNDEFINED&fzr=false' +
'&top_margin=0.6&bottom_margin=0.6&left_margin=0.5&right_margin=0.5';
const params = {
headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
muteHttpExceptions: true,
};
return UrlFetchApp.fetch(url, params).getBlob();
}
ポイントは2つです。
- 書き込んだ直後にエクスポートすると、内容が反映される前の状態で PDF になることがあります。エクスポートの前に
SpreadsheetApp.flush()を呼んでください。 -
gidにシートIDを渡すと、そのシートだけを出力できます。請求書は作業用シートに組み立て、PDF にしたら削除しています。
const sh = ss.insertSheet('_請求書作成用_' + invoice.number);
try {
layoutInvoice_(sh, invoice, settings);
SpreadsheetApp.flush();
return exportSheetAsPdf_(ss, sh);
} finally {
ss.deleteSheet(sh); // 失敗しても作業用シートを残さない
}
2. 連続エクスポートの 429 対策
取引先が多いと、エクスポート URL を短い間隔で叩くことになり、HTTP 429(Too Many Requests)が返ることがあります。1件ごとに1秒待ったうえで、429 と 5xx のときだけ指数バックオフで再試行するようにしました。
for (let attempt = 0; attempt < 5; attempt++) {
const resp = UrlFetchApp.fetch(url, params);
const code = resp.getResponseCode();
if (code === 200) return resp.getBlob();
if (code !== 429 && code < 500) throw new Error('PDFの作成に失敗しました(HTTP ' + code + ')');
Utilities.sleep(2000 * Math.pow(2, attempt));
}
throw new Error('PDFの作成に失敗しました(混雑のため)');
GAS の実行時間の上限は6分なので、1回の実行で処理する件数の目安も決めておくと安心です。
3. インボイス制度に沿った消費税の計算
適格請求書(インボイス)では、消費税の端数処理を「1枚の請求書につき、税率ごとに1回」行うことになっています。明細の1行ごとに切り捨てて合計すると、ルールと合わない金額になることがあります。
そこで、税率ごとに税抜金額を合計してから、税率ごとに1回だけ切り捨てています。
const byRate = {};
items.forEach(function (it) {
const key = String(it.rate);
byRate[key] = byRate[key] || { rate: it.rate, subtotal: 0 };
byRate[key].subtotal += it.amount;
});
const taxes = Object.keys(byRate).map(function (k) {
const b = byRate[k];
// 浮動小数の誤差を避けるため整数のパーセントで計算する
return { rate: b.rate, subtotal: b.subtotal, tax: Math.floor(b.subtotal * Math.round(b.rate * 100) / 100) };
});
subtotal * 0.1 をそのまま切り捨てない理由
最初は Math.floor(subtotal * rate) と書いていましたが、浮動小数点の誤差で 1円ずれる可能性があります。たとえば JavaScript では次のようになります。
0.29 * 100 // 28.999999999999996
Math.floor(0.29 * 100) // 28(期待値は 29)
税率をいったん整数のパーセント(10 や 8)に直し、subtotal * 10 / 100 のように整数同士の計算にしてから割ると、この誤差を避けられます。税抜金額 0〜99,999円 のすべてについて Math.floor(x / 10) と一致することを Node.js で確認しました。
軽減税率(8%)の品目には「※」を付け、10%対象と8%対象それぞれの小計と消費税額を請求書に記載しています。
4. Gmail の下書きと本文の差し込み
件名と本文は設定シートで自由に書けるようにし、{会社名} のようなプレースホルダーを置き換えています。
function fillTemplate_(tpl, vars) {
return String(tpl || '').replace(/\{([^}]+)\}/g, function (all, key) {
return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : all;
});
}
GmailApp.createDraft(client.email, subject, body, {
attachments: [file.getBlob()],
name: settings['メール差出人名'],
});
いきなり送信せず、まず下書きを作るモードを用意しておくと、本番運用で安心して使えます。送信前に人が内容を確認できるからです。
権限について
このスクリプトはスプレッドシート、ドライブ、Gmail、外部 URL への接続(PDF エクスポート用に docs.google.com)の権限を使います。初回実行時に「このアプリは Google で確認されていません」と表示されますが、審査を受けていない自作スクリプトでは必ず表示される画面です。コードの中身を確認したうえで許可してください。
まとめ
- 特定のシートを A4 の PDF にするには、エクスポート URL と
ScriptApp.getOAuthToken()を使う - 連続エクスポートは 429 になりうるので、待ち時間と再試行を入れる
- インボイスの消費税は「税率ごとに合計してから1回だけ端数処理」し、計算は整数で行う
- メールはまず下書きで作り、人が確認してから送る
今回のコードに、初期設定メニュー、発行履歴、支払期限の自動計算、マニュアルを加えて、すぐ使える形にまとめたものを BOOTH で配布しています。
→ https://smauto.booth.pm/items/8952122
無料版配布中!
→ https://smauto.booth.pm/items/8958489