🧾事務自動化ラボ

スプレッドシートで請求書を自動発行する仕組みの作り方(GAS・コピペ可)

明細を入力してメニューを押すだけで、PDFの請求書がDriveに保存される仕組みをGASで作ります。実際に動かして確かめたコードと、つまずいた箇所の対処を残します。

毎月の請求書発行を、テンプレートのコピー → 宛先の書き換え → 金額の打ち直し → PDF書き出し → ファイル名を付けて保存、という手順でやっていると、取引先が5社を超えたあたりから事故が起きやすくなります。

私が実際に踏んだ事故は3つでした。金額の転記ミス、同じ月に同じ相手へ2回送ってしまう二重発行、そしてどの番号まで発行したか分からなくなる。どれも「気をつける」では防げません。手順の問題ではなく、仕組みの問題だからです。

この記事では、Googleスプレッドシートと Google Apps Script(GAS)で、明細を入力してメニューを1回押すだけでPDFの請求書ができる仕組みを作ります。会計ソフトのような月額費用はかかりません。

自分で組まずに動くものが欲しい場合は、請求書1枚をPDF化する無料テンプレート にコードを全文置いています。この記事は、その中で何が起きているかと、自分で作るときにどこで詰まるかの解説です。

全体像

作るのはこういう流れです。

  1. 「請求データ」シートに明細を入力する(1行=1明細)
  2. メニュー「請求書 > 請求書を発行」を押して、請求月を入れる
  3. 取引先ごとに明細が集計され、「請求書」テンプレートに差し込まれる
  4. PDF化されて、Driveの指定フォルダに 20260813_株式会社サンプル商事_50600.pdf の形式で保存される
  5. 「発行履歴」シートに1行記録される

肝は5番の履歴です。ここがあるかないかで、二重発行の防止と番号の管理ができるかが決まります。

シート構成

5枚のシートを使います。

シート 役割
設定 自社名・住所・登録番号・振込先・税率・源泉徴収のON/OFF
取引先マスタ 宛先名・敬称・住所
請求データ 1行=1明細(請求月・取引先・品目・数量・単価・税区分)
請求書 差し込み用のテンプレート。ここがそのままPDFになる
発行履歴 発行日時・請求月・取引先・請求書番号・合計・ファイル名

明細を1行1明細にしておくのが後で効きます。取引先ごと・請求月ごとの集計が、コード側で素直に書けるからです。

差し込みの基本

テンプレートシートに {{取引先名}} のようなプレースホルダを置いて、コードから置換します。

function fillTemplate(sheet, values) {
  const range = sheet.getDataRange();
  const data = range.getValues();
  for (let r = 0; r < data.length; r++) {
    for (let c = 0; c < data[r].length; c++) {
      const cell = data[r][c];
      if (typeof cell !== 'string') continue;
      let out = cell;
      for (const [key, val] of Object.entries(values)) {
        out = out.split('{{' + key + '}}').join(val);
      }
      if (out !== cell) data[r][c] = out;
    }
  }
  range.setValues(data);
}

replace ではなく split().join() を使っているのは、置換する値に正規表現の特殊文字($ など)が入っても壊れないようにするためです。金額を扱うので、ここは地味に効きます。

つまずきポイント:結合セルへの書き込み

テンプレートの見た目を整えるために、E3:F3 のようにセルを結合していることがあります。このとき 結合範囲の左上(アンカー)以外に setValue しても、画面には何も表示されません。値は下層のセルに入るだけです。

画面では入っているように見えても、生成したPDFでは発行日や請求書番号が欠けます。差し込み先は必ず結合範囲の左上を指すと決めておくのが安全です。

PDF化して保存する

スプレッドシートのエクスポートURLを叩いて、返ってきたblobをDriveに保存します。

function exportPdf(ss, sheet, fileName, folder) {
  SpreadsheetApp.flush();
  const url = `https://docs.google.com/spreadsheets/d/${ss.getId()}/export`
    + `?format=pdf&gid=${sheet.getSheetId()}`
    + `&size=A4&portrait=true&fitw=true`
    + `&sheetnames=false&printtitle=false&pagenumbers=false&gridlines=false`
    + `&range=A1:F42`;

  const res = UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
    muteHttpExceptions: true,
  });
  if (res.getResponseCode() !== 200) {
    throw new Error('PDFの生成に失敗しました(HTTP ' + res.getResponseCode() + ')');
  }
  return folder.createFile(res.getBlob().setName(fileName));
}

SpreadsheetApp.flush() を先に呼ぶのが重要です。差し込んだ値がまだシートに反映されていない状態でエクスポートすると、1つ前の内容のPDFができます。

range を指定して印刷範囲を固定すると、空行まで含んだ2ページ目が出るのを防げます。

二重発行を止める

発行履歴を見て、同じ請求月・同じ取引先の記録があれば確認を出します。

function hasHistory(ss, month, clientName) {
  const sheet = ss.getSheetByName('発行履歴');
  const values = sheet.getDataRange().getValues();
  for (let i = 1; i < values.length; i++) {
    if (normalizeMonth(values[i][1]) === month && values[i][2] === clientName) return true;
  }
  return false;
}

normalizeMonth を挟んでいるのは、シートに 2026-07 と入力しても、Googleが日付型として解釈して Date オブジェクトになることがあるからです。文字列比較だけで書くと、ある日突然マッチしなくなります。

function normalizeMonth(v) {
  if (v instanceof Date) {
    return v.getFullYear() + '-' + String(v.getMonth() + 1).padStart(2, '0');
  }
  return String(v).trim();
}

請求書番号でいちばん間違えやすいところ

ここが本題です。請求書番号は INV-202607-01 のように、月ごとに連番を振ります。素直に書くとこうなります。

// これは事故ります
function buildInvoiceNumber(ss, month) {
  const values = ss.getSheetByName('発行履歴').getDataRange().getValues();
  let max = 0;
  for (let i = 1; i < values.length; i++) {
    if (normalizeMonth(values[i][1]) !== month) continue;
    const m = String(values[i][3]).match(/-(\d+)$/);
    if (m && Number(m[1]) > max) max = Number(m[1]);
  }
  return 'INV-' + month.replace('-', '') + '-' + String(max + 1).padStart(2, '0');
}

履歴の最大値+1。一見正しく見えます。実際、1社だけ発行するうちは正しく動きます。

問題は2社を連続で発行したときです。1社目の履歴を appendRow で書き込んだ直後に、2社目が履歴を読み直す。このとき書き込みがまだ反映されておらず、2社目も同じ番号を取ってしまうことがあります(2社とも INV-202607-01 になる、という形で出ます)。

さらに厄介なのは、この重複がローカルのテストでは再現しないことです。シートの読み書きをスタブにしたテストは全部パスし、GASの実環境で複数件を連続発行したときにだけ出ます。

対策は2つ組み合わせます。月ごとの連番をドキュメントプロパティに永続化することと、採番の区間をロックで囲うことです。

function buildInvoiceNumber(ss, month) {
  const key = 'INVOICE_SEQ_' + month;
  const lock = LockService.getDocumentLock();
  lock.waitLock(20000);
  try {
    // 履歴側の最大値も見る(履歴を手で編集された場合の保険)
    const values = ss.getSheetByName('発行履歴').getDataRange().getValues();
    let historyMax = 0;
    for (let i = 1; i < values.length; i++) {
      if (normalizeMonth(values[i][1]) !== month) continue;
      const m = String(values[i][3]).match(/-(\d+)$/);
      if (m && Number(m[1]) > historyMax) historyMax = Number(m[1]);
    }

    const props = PropertiesService.getDocumentProperties();
    const stored = Number(props.getProperty(key)) || 0;
    const next = Math.max(historyMax, stored) + 1;
    props.setProperty(key, String(next));   // 先に予約する

    return 'INV-' + month.replace('-', '') + '-' + String(next).padStart(2, '0');
  } finally {
    lock.releaseLock();
  }
}

ポイントは先に番号を予約してしまうことです。発行に失敗しても予約は戻しません。欠番は出ますが、重複は出ません。請求書において、欠番は説明できますが重複は説明できません。

新しいシートで3回続けて発行して確認しました。1回目が 01 / 02、2回目が 03 / 04、3回目が 05 / 06。実行をまたいでも番号がリセットされず、6行すべて一意になりました。

インボイス制度で必要な項目

適格請求書として扱うには、記載が必要な項目があります。テンプレートに差し込み先を用意していないと、コードは正常に動いているのに要件を満たさない請求書ができあがります。

  • 発行者の氏名または名称と、登録番号
  • 取引年月日(発行日、または「2026年7月分」のような対象期間)
  • 取引内容(品目。軽減税率の対象ならその旨)
  • 税率ごとに区分した対価の合計額と、適用税率
  • 税率ごとに区分した消費税額
  • 交付を受ける事業者の氏名または名称

品目欄と適用税率欄は、用意し忘れやすい箇所です。忘れると、金額は合っているのに要件を満たしていない請求書になります。作り終わったら、生成されたPDFをこの6項目と突き合わせて確認してください。

消費税の端数処理は、1つの請求書につき税率ごとに1回です。明細ごとに端数処理をして合算すると、合計が合わなくなります。

源泉徴収を入れる場合

デザインやライティングなど、源泉徴収の対象になる報酬を請求する場合です。税抜100万円以下の部分は 10.21% を差し引きます。

const withholding = Math.floor(subtotalExcludingTax * 0.1021);
const total = subtotalExcludingTax + tax - withholding;

税抜20,000円なら源泉は2,042円、消費税2,000円を足して合計19,958円。実際に発行して、この数字になることを確かめてください。

作ったあとに必ずやること

コードが書けた時点では、まだ終わっていません。新しいスプレッドシートで、最初から通しで動かしてください。

  • 初期シートの生成 → 5枚できるか
  • 1回目の発行 → 番号は 01 / 02 か
  • 2回目の発行 → 二重発行の警告が出るか、番号は 03 / 04 か
  • 生成されたPDFを開く → 発行日・請求書番号・品目・適用税率・税率別内訳が印字されているか
  • 源泉徴収をONにして再発行 → 金額が合うか

ここに挙げた点は、ロジックのテストだけでは確かめられません。実環境で複数回動かして確認してください。

毎月の発行をトリガーで定期実行させるなら、もう1段あります。トリガーの中で例外が起きても画面には何も表示されないため、止まったこと自体に気づけません。実行ログを残して失敗をメールで受け取る手順は、GASのトリガーが止まっても気づけるようにする に書いています。

自分で組むか会計ソフトに任せるか

ここまで書いておいてなんですが、この仕組みが要らない人もいます。判断の分かれ目は、請求のあとに何が続くかです。

請求書を出して終わりなら、この記事の仕組みで足ります。ただし、そのあと帳簿づけ・確定申告・消費税の申告まで自分でやるなら、請求と会計が別々になっている分だけ二度手間になります。会計ソフト側で請求書を作れば、売掛金の計上まで一続きになります。

私の判断基準はこうです。

  • 取引先が数社で、確定申告は白色または簡易な青色 → この記事の仕組みで足りる
  • インボイス登録済みで、消費税の申告がある → 会計ソフトに寄せたほうが、集計の手間と間違いが減る

まとめ

  • 明細は1行1明細。集計はコード側でやる
  • 差し込み先は結合セルの左上を指す
  • PDF化の前に flush()
  • 請求書番号は永続カウンタ+ロックで予約する。欠番は許して重複を防ぐ
  • インボイスの必須6項目は、テンプレートに差し込み先があるか先に確認する
  • 新しいシートで通しで動かすまで完成ではない

なお、ここまでの構成のうち1枚ずつ発行する部分は、コードを全文公開しています。→ 請求書PDFを1クリックで作る無料テンプレート

← 記事一覧へ