毎月の請求書づくり、僕は最初「1件5分くらいだし、まあいいか」と思っていました。でも数えてみると、取引先ごとにテンプレをコピーして、日付と金額を打ち替えて、PDFに書き出して、メールに添付して……を10社ぶんやると、それだけで1時間近く溶けています。しかも打ち間違い(請求番号の重複、支払期限の書き忘れ)が地味に怖い。

このページでは、Googleスプレッドシートに数字を入れてボタンを押すだけで、請求書PDFを作ってメール送信まで終わる仕組みを作ります。使うのは無料のGAS(Google Apps Script)だけ。コードはそのままコピペできる形で全部載せます。

僕(シグ)はAIを使ってこういう業務自動化ツールを作って、自分で動かして確認してから公開しています。この記事のコードもGoogle Apps Script上で動く前提で書いています。

完成イメージ:ビフォーアフター

Before(手作業)

After(自動化)

やることは「数字を入れる」だけになります。

用意するもの(3つ)

  1. スプレッドシート … 請求データを入力する場所
  2. Googleドキュメントのテンプレート … 請求書の見た目(ここに差し込む)
  3. Googleドライブのフォルダ … 作ったPDFの保存先

すべてGoogleアカウントがあれば無料です。

① スプレッドシートのレイアウト

「請求データ」という名前のシートを1枚作り、次のように置きます(セル位置は下のコードと合わせてあります)。

セル内容
B1取引先名株式会社サンプル
B2送信先メールkeiri@example.com
B3請求番号INV-2026-001
B4発行日2026/7/31
B5支払期限2026/8/31

そして 8行目から下に明細を、A列=品目 / B列=数量 / C列=単価 で入力します。

A(品目)B(数量)C(単価)
8Webサイト保守130000
9記事執筆38000

金額(数量×単価)・小計・消費税・合計はコードが自動計算するので、入力は不要です。

② ドキュメントのテンプレート

Googleドキュメントで請求書の体裁を作り、差し替えたい場所に二重の波かっこでプレースホルダーを書いておきます。

請求書

{{取引先名}} 御中

請求番号: {{請求番号}}
発行日: {{発行日}}
お支払期限: {{支払期限}}

【明細】
{{明細}}

小計: {{小計}}
消費税: {{消費税}}
合計: {{合計}}

{{差出人}}

{{明細}} の1行が、明細の各行に置き換わります。ロゴや振込先などの固定情報は、テンプレートに直接書いておけばそのまま出力されます。

GASのコード(そのままコピペ)

スプレッドシートのメニューから 拡張機能 → Apps Script を開き、コード.gs の中身を全部消して、下を貼り付けてください。

/**
 * スプレッドシートの請求データ → 請求書PDF → Gmail送信 まで自動化
 * メニュー「請求書」から実行します。
 */

// ==== 設定(ここだけ自分の環境に合わせる)====
const TEMPLATE_DOC_ID  = 'テンプレートDocのIDを貼る';   // ドキュメントURLの /d/ と /edit の間の文字列
const OUTPUT_FOLDER_ID = '出力フォルダのIDを貼る';       // フォルダURLの末尾の文字列
const SENDER_NAME      = '株式会社サンプル';             // 差出人の表示名
const TAX_RATE         = 0.10;                          // 消費税率(自社の状況に合わせる)

/** スプレッドシートを開くとメニューを追加 */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('請求書')
    .addItem('作成して送信', 'createAndSendInvoice')
    .addItem('PDFだけ作成(送信しない)', 'createInvoicePdfOnly')
    .addToUi();
}

/** 請求書PDFを作って、そのままメール送信する */
function createAndSendInvoice() {
  const inv = buildInvoice_();
  if (!inv.toEmail) throw new Error('送信先メール(B2)が空です。');

  // GmailApp で本文つき・PDF添付のメールを送信
  GmailApp.sendEmail(
    inv.toEmail,
    '【請求書】' + inv.invoiceNo + ' のご送付',
    inv.client + ' 御中\n\n' +
    'いつもお世話になっております。\n' +
    '請求書(No.' + inv.invoiceNo + ')をお送りいたします。\n' +
    'ご確認のほど、よろしくお願いいたします。\n\n' +
    SENDER_NAME,
    { name: SENDER_NAME, attachments: [inv.pdfBlob] }
  );
  SpreadsheetApp.getActiveSpreadsheet().toast('送信しました: ' + inv.toEmail, '完了', 5);
}

/** メール送信せず、PDFの作成だけ(下書き確認用) */
function createInvoicePdfOnly() {
  const inv = buildInvoice_();
  SpreadsheetApp.getActiveSpreadsheet().toast('PDFを作成しました: ' + inv.pdfName, '完了', 5);
}

/** 請求データを読み、テンプレに差し込んでPDFを作る(共通処理) */
function buildInvoice_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('請求データ');
  if (!sheet) throw new Error('「請求データ」シートが見つかりません。シート名を確認してください。');

  // --- ヘッダー項目(B列)---
  const client    = sheet.getRange('B1').getValue();
  const toEmail   = sheet.getRange('B2').getValue();
  const invoiceNo = sheet.getRange('B3').getValue();
  const issueDate = sheet.getRange('B4').getValue();
  const dueDate   = sheet.getRange('B5').getValue();

  // --- 明細(8行目以降: A=品目 B=数量 C=単価)---
  const numRows = Math.max(sheet.getLastRow() - 7, 0);
  const raw = numRows > 0 ? sheet.getRange(8, 1, numRows, 3).getValues() : [];
  const items = raw.filter(function (r) { return r[0] !== '' && r[0] !== null; });
  if (items.length === 0) throw new Error('明細(8行目以降)が入力されていません。');

  // --- 金額を計算 ---
  let subtotal = 0;
  const rows = items.map(function (r) {
    const qty = Number(r[1]) || 0;
    const price = Number(r[2]) || 0;
    const amount = qty * price;
    subtotal += amount;
    return { name: r[0], qty: qty, price: price, amount: amount };
  });
  const tax = Math.floor(subtotal * TAX_RATE);
  const total = subtotal + tax;

  // --- 表示用の整形 ---
  const tz = Session.getScriptTimeZone();
  const fmtDate = function (d) {
    return (d instanceof Date) ? Utilities.formatDate(d, tz, 'yyyy年MM月dd日') : String(d);
  };
  const yen = function (n) { return '¥' + Number(n).toLocaleString('ja-JP'); };
  const lines = rows.map(function (r) {
    return r.name + '  ' + r.qty + ' × ' + yen(r.price) + ' = ' + yen(r.amount);
  }).join('\n');

  // --- テンプレDocを複製して差し込み ---
  const folder = DriveApp.getFolderById(OUTPUT_FOLDER_ID);
  const pdfName = '請求書_' + invoiceNo + '_' + client;
  const copy = DriveApp.getFileById(TEMPLATE_DOC_ID).makeCopy(pdfName, folder);
  const doc = DocumentApp.openById(copy.getId());
  const body = doc.getBody();

  body.replaceText('{{取引先名}}', String(client));
  body.replaceText('{{請求番号}}', String(invoiceNo));
  body.replaceText('{{発行日}}', fmtDate(issueDate));
  body.replaceText('{{支払期限}}', fmtDate(dueDate));
  body.replaceText('{{差出人}}', SENDER_NAME);
  body.replaceText('{{明細}}', lines);
  body.replaceText('{{小計}}', yen(subtotal));
  body.replaceText('{{消費税}}', yen(tax));
  body.replaceText('{{合計}}', yen(total));
  doc.saveAndClose();

  // --- PDFに変換してフォルダに保存 ---
  const pdfBlob = copy.getAs('application/pdf').setName(pdfName + '.pdf');
  folder.createFile(pdfBlob);
  copy.setTrashed(true); // 中間の複製Docは捨てる(PDFだけ残す)

  return {
    client: client, toEmail: toEmail, invoiceNo: invoiceNo,
    pdfBlob: pdfBlob, pdfName: pdfName
  };
}

IDの入れ方

コード上部の TEMPLATE_DOC_IDOUTPUT_FOLDER_ID を、自分のURLから書き換えます。

動かし方

  1. 上のコードを貼って保存し、いったんスプレッドシートを再読み込みします。
  2. メニューに「請求書」が増えるので、請求書 → 作成して送信を押します。
  3. 初回だけ「承認が必要です」と出ます。自分のアカウントで内容を確認して許可すればOK(自分のドライブとGmailを操作するための許可です)。
  4. まずは自分宛のメールでテストしてから、本番の取引先に使うのがおすすめです。

つまづきやすいポイント

「自分でやるのは大変そう」と感じたら

ここまで読んで「仕組みは分かったけど、テンプレ作りや承認まわりでつまずきそう」「うちは請求のパターンが特殊で、そのままだと当てはまらない」という方もいると思います。

僕は、こういうスプレッドシート×GASの業務自動化やWebアプリを受託で作っています。御社の請求フロー(明細の形・取引先ごとの様式・締め日・振込先の出し分けなど)に合わせて、動く状態でお渡しします。AIを使うぶん速く・安く作れますが、要件のヒアリングと動作確認・修正は僕が責任を持って行います。

「この作業、自動化できる?」の相談だけでも歓迎です。まずはお気軽にどうぞ。